/**
 * useScanner — camera-capture + Gemini Vision composable.
 *
 * Two capture modes, one pipeline:
 *   - processPhoto(file)         → barcode/serial extraction    (ScanPage, /j)
 *   - scanEquipmentLabel(file)   → structured ONT/ONU label     (equipment
 *                                    linking, ClientDetailPage photos)
 *
 * Both resize the photo twice:
 *   - 400px  for the on-screen thumbnail
 *   - 1600px @ q=0.92 for Gemini (text readability > filesize)
 *
 * Resilience (barcode mode only):
 *   If Gemini doesn't answer within SCAN_TIMEOUT_MS (weak LTE, basement,
 *   service cold-start), the photo is queued in IndexedDB via the offline
 *   store and retried in the background. The tech sees a "scan en attente"
 *   chip, keeps scanning the next equipment, and the late result is pushed
 *   back into `barcodes` via a reactive watcher on `offline.scanResults`.
 *
 * Equipment-label mode does NOT queue — it's typically invoked on a desktop
 * or strong wifi (indoor install, office) where the extra complexity of
 * background retry isn't worth it, and callers want a synchronous answer
 * (to pre-fill an equipment form).
 *
 * Merged from apps/ops/src/composables/useScanner.js (which had the
 * equipment-label branch) and apps/field/src/composables/useScanner.js
 * (which had the resilient timeout + offline queue). See
 * docs/ARCHITECTURE.md §"Legacy Retirement Plan" — field is being folded
 * into ops at /j and must not lose offline capability in the process.
 *
 * @param {object} options
 * @param {(code: string) => void} [options.onNewCode] — fires for each
 *   newly detected code, whether the scan was synchronous OR delivered
 *   later from the offline queue. Typical use: trigger an ERPNext lookup
 *   and Quasar notify.
 */

import { ref, watch } from 'vue'
import { scanBarcodes, scanEquipmentLabel as apiScanEquipmentLabel } from 'src/api/ocr'
import { useOfflineStore } from 'src/stores/offline'

const SCAN_TIMEOUT_MS = 8000

export function useScanner (options = {}) {
  const onNewCode = options.onNewCode || (() => {})

  const barcodes = ref([])       // { value, region }[] — max MAX_BARCODES
  const scanning = ref(false)    // true while a Gemini call is in flight
  const error = ref(null)
  const lastPhoto = ref(null)    // data URI of last thumbnail (400px)
  const photos = ref([])         // { url, ts, codes, queued }[] — full history

  // Field's default cap was 3 (phone screen estate); ops historically
  // allowed 5 (equipment labels have more identifiers). Keep 5 here
  // since equipment-label mode is an ops-only feature.
  const MAX_BARCODES = 5

  const offline = useOfflineStore()

  // Pick up any scans that completed while the composable was unmounted
  // (e.g. tech queued a photo in the basement, phone locked, signal
  // returned while the page was gone, now they reopen ScanPage).
  for (const result of offline.scanResults) {
    mergeCodes(result.barcodes || [], 'queued')
    offline.consumeScanResult(result.id)
  }

  // Watch for sync completions during the lifetime of this scanner.
  // Vue auto-disposes the watcher when the host component unmounts.
  watch(
    () => offline.scanResults.length,
    () => {
      for (const result of [...offline.scanResults]) {
        mergeCodes(result.barcodes || [], 'queued')
        offline.consumeScanResult(result.id)
      }
    }
  )

  function addCode (code, region) {
    if (barcodes.value.length >= MAX_BARCODES) return false
    if (barcodes.value.find(b => b.value === code)) return false
    barcodes.value.push({ value: code, region })
    onNewCode(code)
    return true
  }

  function mergeCodes (codes, region) {
    const added = []
    for (const code of codes) {
      if (addCode(code, region)) added.push(code)
    }
    return added
  }

  /**
   * Process a photo for generic barcode/serial extraction.
   * Resilient: on timeout/network error the photo is queued for retry.
   */
  async function processPhoto (file) {
    if (!file) return []
    error.value = null
    scanning.value = true

    let aiImage = null
    const photoIdx = photos.value.length
    let found = []

    try {
      const thumbUrl = await resizeImage(file, 400)
      lastPhoto.value = thumbUrl
      photos.value.push({ url: thumbUrl, ts: Date.now(), codes: [], queued: false })

      // Keep high-res for text readability (small serial fonts).
      aiImage = await resizeImage(file, 1600, 0.92)

      const result = await scanBarcodesWithTimeout(aiImage, SCAN_TIMEOUT_MS)
      found = mergeCodes(result.barcodes || [], 'photo')
      photos.value[photoIdx].codes = found

      if (found.length === 0) {
        error.value = 'Aucun code détecté — rapprochez-vous ou améliorez la mise au point'
      }
    } catch (e) {
      if (aiImage && isRetryable(e)) {
        await offline.enqueueVisionScan({ image: aiImage })
        if (photos.value[photoIdx]) photos.value[photoIdx].queued = true
        error.value = 'Réseau faible — scan en attente. Reprise automatique au retour du signal.'
      } else {
        error.value = e.message || 'Erreur'
      }
    } finally {
      scanning.value = false
    }

    return found
  }

  /**
   * Process a photo for structured equipment-label extraction.
   *
   * Returns the Gemini response directly:
   *   { brand, model, serial_number, mac_address, gpon_sn, hw_version,
   *     equipment_type, barcodes: string[] }
   *
   * Side-effect: pushes `serial_number` + any `barcodes` into the same
   * `barcodes` ref as processPhoto(), so a UI that uses both modes shares
   * one list.
   *
   * Intentionally NOT resilient (no timeout, no queue) — equipment
   * linking is a desktop/wifi flow, and callers want a sync answer.
   */
  async function scanEquipmentLabel (file) {
    if (!file) return null
    error.value = null
    scanning.value = true
    try {
      const thumbUrl = await resizeImage(file, 400)
      lastPhoto.value = thumbUrl
      const aiImage = await resizeImage(file, 1600, 0.92)
      const data = await apiScanEquipmentLabel(aiImage)

      if (data?.barcodes?.length) mergeCodes(data.barcodes, 'equipment')
      if (data?.serial_number) addCode(data.serial_number, 'equipment')

      if (!data?.serial_number && !data?.barcodes?.length) {
        error.value = 'Aucun identifiant détecté — rapprochez-vous ou améliorez la mise au point'
      }
      return data
    } catch (e) {
      error.value = e.message || 'Erreur'
      return null
    } finally {
      scanning.value = false
    }
  }

  /** Race scanBarcodes against a timeout. Used only for barcode mode. */
  async function scanBarcodesWithTimeout (image, ms) {
    return await Promise.race([
      scanBarcodes(image),
      new Promise((_, reject) => setTimeout(
        () => reject(new Error('ScanTimeout')),
        ms,
      )),
    ])
  }

  /** Retryable = worth queueing in IndexedDB for later. */
  function isRetryable (e) {
    const msg = (e?.message || '').toLowerCase()
    return msg.includes('scantimeout')
      || msg.includes('failed to fetch')
      || msg.includes('networkerror')
      || msg.includes('load failed')
      || e?.name === 'TypeError'  // fetch throws TypeError on network error
  }

  /** Resize a File to a max dimension, return as base64 data URI. */
  function resizeImage (file, maxDim, quality = 0.85) {
    return new Promise((resolve, reject) => {
      const img = new Image()
      img.onload = () => {
        let { width, height } = img
        if (width > maxDim || height > maxDim) {
          const ratio = Math.min(maxDim / width, maxDim / height)
          width = Math.round(width * ratio)
          height = Math.round(height * ratio)
        }
        const canvas = document.createElement('canvas')
        canvas.width = width
        canvas.height = height
        canvas.getContext('2d').drawImage(img, 0, 0, width, height)
        resolve(canvas.toDataURL('image/jpeg', quality))
      }
      img.onerror = reject
      img.src = URL.createObjectURL(file)
    })
  }

  function removeBarcode (value) {
    barcodes.value = barcodes.value.filter(b => b.value !== value)
  }

  function clearBarcodes () {
    barcodes.value = []
    error.value = null
    lastPhoto.value = null
    photos.value = []
  }

  return {
    barcodes, scanning, error, lastPhoto, photos,
    processPhoto, scanEquipmentLabel, removeBarcode, clearBarcodes,
  }
}
