/**
 * Offline store — mutation queue + vision (Gemini) retry queue.
 *
 * This store is the backbone of the tech `/j` (mobile) workflow: techs work
 * in basements, elevators, and under couches where LTE drops for seconds to
 * minutes. We can't afford to lose a scan or a "job completed" tap, so both
 * mutations AND vision photos are persisted to IndexedDB and retried in the
 * background when connectivity returns.
 *
 * Two queues, different retry strategies:
 *
 *   ┌─ queue (ERPNext mutations) ──────────────────────────────────────┐
 *   │   { type: 'create'|'update', doctype, name?, data, ts, id }      │
 *   │   flush on `online` event → replay createDoc/updateDoc.          │
 *   │   Failed items stay queued until next online flip.               │
 *   └──────────────────────────────────────────────────────────────────┘
 *
 *   ┌─ visionQueue (Gemini photo OCR) ─────────────────────────────────┐
 *   │   { id, image (base64), ts, status }                             │
 *   │   Retries are time-driven (scheduleVisionRetry), not connectivity│
 *   │   -driven, because `navigator.onLine` lies in weak-signal zones  │
 *   │   (reports true on a captive 2-bar LTE that can't actually       │
 *   │   reach msg.gigafibre.ca). First retry at 5s, backoff to 30s.    │
 *   │                                                                   │
 *   │   Successful scans land in `scanResults` and the `useScanner`    │
 *   │   composable merges them back into the UI via a watcher.         │
 *   └──────────────────────────────────────────────────────────────────┘
 *
 * IndexedDB keys (idb-keyval, no schema):
 *   - `offline-queue`   → mutation queue
 *   - `vision-queue`    → pending photos
 *   - `vision-results`  → completed scans waiting for the UI to consume
 *   - `cache-{key}`     → generic read cache (used for read-through patterns)
 *
 * Ported from apps/field/src/stores/offline.js as part of the field→ops
 * unification (see docs/architecture/overview.md §"Legacy Retirement Plan").
 */

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { get, set } from 'idb-keyval'
import { createDoc, updateDoc } from 'src/api/erp'
import { scanBarcodes } from 'src/api/ocr'

export const useOfflineStore = defineStore('offline', () => {
  // ─── Mutation queue ──────────────────────────────────────────────
  const queue = ref([])
  const syncing = ref(false)
  const online = ref(navigator.onLine)
  const pendingCount = computed(() => queue.value.length)

  // ─── Vision queue ────────────────────────────────────────────────
  const visionQueue = ref([])  // { id, image (base64), ts, status }
  const scanResults = ref([])  // { id, barcodes: string[], ts }
  const pendingVisionCount = computed(() => visionQueue.value.length)
  let retryTimer = null
  let visionSyncing = false

  // Listen to connectivity changes. We kick off BOTH queues on `online`
  // because a reconnect is the cheapest signal we have that things might
  // work now — worst case the retries fail again and we stay queued.
  window.addEventListener('online', () => {
    online.value = true
    syncQueue()
    syncVisionQueue()
  })
  window.addEventListener('offline', () => { online.value = false })

  async function loadQueue () {
    try {
      const stored = await get('offline-queue')
      queue.value = stored || []
    } catch { queue.value = [] }
  }

  async function saveQueue () {
    // Pinia refs aren't structured-clonable directly (proxies); JSON
    // round-trip is the simplest way to get a plain copy for IndexedDB.
    await set('offline-queue', JSON.parse(JSON.stringify(queue.value)))
  }

  async function loadVisionQueue () {
    try {
      visionQueue.value = (await get('vision-queue')) || []
      scanResults.value = (await get('vision-results')) || []
    } catch {
      visionQueue.value = []
      scanResults.value = []
    }
    // If we're restoring a non-empty queue (app was closed with pending
    // scans), give the network 5s to settle before the first retry.
    if (visionQueue.value.length) scheduleVisionRetry(5000)
  }

  async function saveVisionQueue () {
    await set('vision-queue', JSON.parse(JSON.stringify(visionQueue.value)))
  }

  async function saveScanResults () {
    await set('vision-results', JSON.parse(JSON.stringify(scanResults.value)))
  }

  /**
   * Enqueue a mutation to be synced later.
   * @param {{ type: 'create'|'update', doctype: string, name?: string, data: object }} action
   */
  async function enqueue (action) {
    action.ts = Date.now()
    action.id = action.ts + '-' + Math.random().toString(36).slice(2, 8)
    queue.value.push(action)
    await saveQueue()
    if (online.value) syncQueue()
    return action
  }

  async function syncQueue () {
    if (syncing.value || queue.value.length === 0) return
    syncing.value = true
    const failed = []
    for (const action of [...queue.value]) {
      try {
        if (action.type === 'create') {
          await createDoc(action.doctype, action.data)
        } else if (action.type === 'update') {
          await updateDoc(action.doctype, action.name, action.data)
        }
      } catch {
        failed.push(action)
      }
    }
    queue.value = failed
    await saveQueue()
    syncing.value = false
  }

  /**
   * Enqueue a photo whose Gemini scan couldn't complete (timeout / offline).
   * Called by useScanner when scanBarcodes exceeds SCAN_TIMEOUT_MS or throws
   * a network error. Returns the queued entry so the caller can display a
   * "scan en attente" chip in the UI.
   *
   * @param {{ image: string }} opts — base64 (data URI) of the optimized image
   */
  async function enqueueVisionScan ({ image }) {
    const entry = {
      id: Date.now() + '-' + Math.random().toString(36).slice(2, 8),
      image,
      ts: Date.now(),
      status: 'queued',
    }
    visionQueue.value.push(entry)
    await saveVisionQueue()
    scheduleVisionRetry(5000)
    return entry
  }

  /**
   * Retry each queued photo. Success → move to scanResults, fail → stay
   * queued with a bumped retry schedule. We drive retries off the queue
   * itself, not off `online`, because navigator.onLine can report true
   * even on weak LTE that can't reach the hub.
   */
  async function syncVisionQueue () {
    if (visionSyncing) return
    if (retryTimer) { clearTimeout(retryTimer); retryTimer = null }
    if (visionQueue.value.length === 0) return
    visionSyncing = true
    const remaining = []
    try {
      for (const entry of [...visionQueue.value]) {
        try {
          entry.status = 'syncing'
          const result = await scanBarcodes(entry.image)
          scanResults.value.push({
            id: entry.id,
            barcodes: result.barcodes || [],
            ts: Date.now(),
          })
        } catch {
          entry.status = 'queued'
          remaining.push(entry)
        }
      }
      visionQueue.value = remaining
      await Promise.all([saveVisionQueue(), saveScanResults()])
      if (remaining.length) scheduleVisionRetry(30000)
    } finally {
      visionSyncing = false
    }
  }

  function scheduleVisionRetry (delay) {
    if (retryTimer) return
    retryTimer = setTimeout(() => {
      retryTimer = null
      syncVisionQueue()
    }, delay)
  }

  /**
   * Consumer (ScanPage / TechScanPage) calls this after merging a result
   * into the UI so the same serial doesn't reappear next time the page
   * mounts from persisted state.
   */
  async function consumeScanResult (id) {
    scanResults.value = scanResults.value.filter(r => r.id !== id)
    await saveScanResults()
  }

  // ─── Generic read cache (used by list pages for offline browse) ──
  async function cacheData (key, data) {
    await set('cache-' + key, { data, ts: Date.now() })
  }

  async function getCached (key) {
    try {
      const entry = await get('cache-' + key)
      return entry?.data || null
    } catch { return null }
  }

  // Kick off initial loads (fire-and-forget — refs start empty and fill
  // in once IndexedDB resolves, which is fine for the UI).
  loadQueue()
  loadVisionQueue()

  return {
    // mutation queue
    queue, syncing, online, pendingCount, enqueue, syncQueue,
    // vision queue
    visionQueue, scanResults, pendingVisionCount,
    enqueueVisionScan, syncVisionQueue, consumeScanResult,
    // read cache
    cacheData, getCached, loadQueue,
  }
})
