// ── Sync engine ──────────────────────────────────────────────────────────────
// Orchestrates push (queue → Supabase) and pull (Supabase → local cache).
// Browser-only. Never import this from server components or API routes.

import { createClient } from '@/lib/supabase/client'
import { getDB, getStoreByName } from './db'
import { getAllOfflineConfigs, getOfflineConfig } from './registry'
import { getAllPending, markCompleted, markFailed } from './queue'
import { markCacheFetched, getLastFetchedAt } from './local-store'
import type { SyncResult, SyncConflict } from './types'
import type { SupabaseClient } from '@supabase/supabase-js'

// ── Module → entity → table mapping ─────────────────────────────────────────
// Used by the public pullRemoteData(module, entityType) API.

const MODULE_ENTITY_TABLE: Record<string, Record<string, string>> = {
  assessment: {
    responses:        'assessment_responses',
    list:             'assessments',
    questions:        'assessment_questions',
  },
  planning: {
    assignments:      'assignments',
    consultants:      'consultants',
    appointmentTypes: 'appointment_types',
  },
}

// Fields that should be stripped from payloads before pushing to Supabase
const LOCAL_ONLY_FIELDS = new Set(['_syncStatus'])

function stripLocalFields(payload: Record<string, unknown>): Record<string, unknown> {
  const out: Record<string, unknown> = {}
  for (const [k, v] of Object.entries(payload)) {
    if (!LOCAL_ONLY_FIELDS.has(k)) out[k] = v
  }
  return out
}

// ── Mutex — prevent concurrent sync runs ─────────────────────────────────────

let _isSyncing = false
export function isSyncing(): boolean { return _isSyncing }

// ── Conflict helpers ──────────────────────────────────────────────────────────

async function recordConflict(conflict: Omit<SyncConflict, 'id'>): Promise<void> {
  await getDB().syncConflicts.add(conflict as SyncConflict)
}

async function updateLocalRecordStatus(
  storeName: string,
  primaryKey: string,
  recordId: string,
  syncStatus: 'synced' | 'conflict',
): Promise<void> {
  const store = getStoreByName(storeName)
  await store.where(primaryKey).equals(recordId).modify({ _syncStatus: syncStatus })
}

async function putLocalRecord(
  storeName: string,
  record: Record<string, unknown>,
  syncStatus: 'synced' | 'conflict',
): Promise<void> {
  const store = getStoreByName(storeName)
  await store.put({ ...record, _syncStatus: syncStatus } as Parameters<typeof store.put>[0])
}

// ── LWW conflict resolution ───────────────────────────────────────────────────

function resolveConflict(
  local: Record<string, unknown>,
  server: Record<string, unknown>,
): 'local' | 'server' {
  const localTs  = local.updated_at  ? new Date(local.updated_at  as string).getTime() : 0
  const serverTs = server.updated_at ? new Date(server.updated_at as string).getTime() : 0
  return serverTs > localTs ? 'server' : 'local'
}

// ── Push — queue → Supabase ───────────────────────────────────────────────────

/**
 * Process all pending queue entries in FIFO order.
 * Returns a partial SyncResult (pushed/failed/conflicts; pulled is always 0).
 */
export async function startSync(): Promise<SyncResult> {
  const result: SyncResult = { pushed: 0, pulled: 0, failed: 0, conflicts: 0 }
  if (_isSyncing) return result

  _isSyncing = true
  try {
    const pending = await getAllPending()
    if (pending.length === 0) return result

    const supabase = createClient() as SupabaseClient

    for (const entry of pending) {
      const config = getOfflineConfig(entry.table)
      if (!config) {
        await markFailed(entry.id!, `No offline config registered for table "${entry.table}"`)
        result.failed++
        continue
      }

      try {
        let supabaseError: string | null = null
        let serverRow: Record<string, unknown> | null = null
        const cleanPayload = entry.payload ? stripLocalFields(entry.payload) : undefined

        switch (entry.operation) {
          case 'insert': {
            const { error } = await supabase.from(entry.table).insert(cleanPayload!)
            if (error) supabaseError = error.message
            break
          }

          case 'update': {
            const { error } = await supabase
              .from(entry.table)
              .update(cleanPayload!)
              .eq(config.primaryKey, entry.recordId!)
            if (error) supabaseError = error.message
            break
          }

          case 'upsert': {
            const opts = entry.conflictTarget
              ? { onConflict: entry.conflictTarget }
              : undefined
            const { data, error } = await supabase
              .from(entry.table)
              .upsert(cleanPayload!, opts)
              .select()
              .single()
            if (error) supabaseError = error.message
            else serverRow = data as Record<string, unknown>
            break
          }

          case 'delete': {
            const { error } = await supabase
              .from(entry.table)
              .delete()
              .eq(config.primaryKey, entry.recordId!)
            if (error) supabaseError = error.message
            break
          }
        }

        if (supabaseError) {
          await markFailed(entry.id!, supabaseError)
          result.failed++
          continue
        }

        // Conflict detection for upsert — Last Write Wins on updated_at
        if (serverRow && entry.payload && entry.operation === 'upsert') {
          const localPayload = entry.payload as Record<string, unknown>
          const winner = resolveConflict(localPayload, serverRow)
          const recordId = entry.recordId ?? String(entry.payload[config.primaryKey])

          if (winner === 'server') {
            await recordConflict({
              table:         entry.table,
              recordId,
              localVersion:  localPayload,
              serverVersion: serverRow,
              resolvedWith:  'server',
              occurredAt:    new Date().toISOString(),
            })
            await putLocalRecord(config.storeName, serverRow, 'conflict')
            result.conflicts++
          } else {
            await updateLocalRecordStatus(config.storeName, config.primaryKey, recordId, 'synced')
          }
        } else if (entry.operation !== 'delete' && entry.payload) {
          const recordId = entry.recordId ?? String(entry.payload[config.primaryKey])
          if (recordId) {
            await updateLocalRecordStatus(config.storeName, config.primaryKey, recordId, 'synced')
          }
        }

        await markCompleted(entry.id!)
        result.pushed++

      } catch (err) {
        const msg = err instanceof Error ? err.message : 'Unknown error'
        await markFailed(entry.id!, msg)
        result.failed++
      }
    }
  } finally {
    _isSyncing = false
  }

  return result
}

// ── Pull — Supabase → local cache ─────────────────────────────────────────────

/**
 * Pull a single table by name into its local store.
 * Uses config.pullSelect if specified (e.g. for joined queries).
 * Exported so the generic repository can call it directly.
 */
export async function pullByTableName(table: string, since?: string): Promise<number> {
  const config = getOfflineConfig(table)
  if (!config) throw new Error(`No offline config for table: ${table}`)

  const supabase = createClient() as SupabaseClient
  const selectStr = config.pullSelect ?? '*'
  let query = supabase.from(table).select(selectStr)
  if (since && config.incrementalSync !== false) {
    query = query.gte('updated_at', since) as typeof query
  }

  const { data, error } = await query
  if (error) throw new Error(error.message)

  const rawRows = (data ?? []) as unknown as Record<string, unknown>[]

  // D6 — Pending-guard: do not overwrite locally-pending records with server
  // data. A pending record has a queued write that has not yet reached Supabase;
  // clobbering it with older server data would cause silent data loss.
  const store = getStoreByName(config.storeName)
  const rows: Record<string, unknown>[] = []
  for (const row of rawRows) {
    const recordId = row[config.primaryKey] as string | undefined
    if (recordId) {
      const local = (await store.get(recordId)) as Record<string, unknown> | undefined
      if (local && local._syncStatus === 'pending') {
        continue  // Skip — local pending write takes precedence
      }
    }
    rows.push({ ...row, _syncStatus: 'synced' as const })
  }

  if (rows.length > 0) {
    await store.bulkPut(rows as unknown as Parameters<typeof store.bulkPut>[0])
  }

  await markCacheFetched(`${table}:all`, config.ttlMs)
  return rows.length
}

/**
 * Pull the latest data for a module/entity pair into local storage.
 *
 * @param module     e.g. 'assessment', 'planning'
 * @param entityType e.g. 'questions', 'assignments'
 * @param since      Optional ISO timestamp — only fetch rows updated after this
 */
export async function pullRemoteData(
  module: string,
  entityType: string,
  since?: string,
): Promise<number> {
  const table = MODULE_ENTITY_TABLE[module]?.[entityType]
  if (!table) throw new Error(`Unknown module/entity: ${module}/${entityType}`)
  return pullByTableName(table, since)
}

// ── Full sync ─────────────────────────────────────────────────────────────────

/**
 * Pull all registered entities from Supabase, then push all pending queue entries.
 * This is the primary entry point for auto-sync and manual "sync now" triggers.
 */
export async function fullSync(): Promise<SyncResult> {
  if (_isSyncing) {
    return { pushed: 0, pulled: 0, failed: 0, conflicts: 0 }
  }

  const result: SyncResult = { pushed: 0, pulled: 0, failed: 0, conflicts: 0 }

  for (const config of getAllOfflineConfigs()) {
    try {
      const since = await getLastFetchedAt(`${config.table}:all`) ?? undefined
      result.pulled += await pullByTableName(config.table, since)
    } catch {
      // Non-fatal — continue with other entities
    }
  }

  const pushResult = await startSync()
  result.pushed    = pushResult.pushed
  result.failed    = pushResult.failed
  result.conflicts = pushResult.conflicts

  return result
}

// ── Utilities ─────────────────────────────────────────────────────────────────

export async function getSyncConflicts(limit = 50): Promise<SyncConflict[]> {
  return getDB().syncConflicts.orderBy('occurredAt').reverse().limit(limit).toArray()
}

export async function clearSyncConflicts(): Promise<void> {
  await getDB().syncConflicts.clear()
}
