// Offline skrive-kø, opprinnelig for scoreregistrering (ADR-028), utvidet
// 2026-08-14 til også å dekke kommentar-/bildeposting i rundefeeden (se
// round-messages.tsx). Ren IndexedDB, ingen avhengighet til service
// workeren -- gir umiddelbar, presis UI-tilbakemelding ("lagret lokalt,
// venter på synk") direkte fra kalleren, som er enklere og mer testbart
// enn å prøve å gjøre det samme inni en SW fetch-handler. Bevisst IKKE
// Background Sync API: iOS Safari støtter den ikke i det hele tatt, og en
// stor andel av klubb-/vennegjeng-brukerne er trolig på iPhone -- et
// enkelt window.online-lytter-mønster (se session-scorecard.tsx) fungerer
// overalt.

const DB_NAME = "teecup-offline"
const DB_VERSION = 1
const STORE = "pending-writes"

export type QueueEntry = {
  id: number
  url: string
  method: string
  body: unknown
  matchId: string
  createdAt: number
  // Identifiserer HVILKEN rad på serveren denne skrivingen gjelder --
  // default url (uendret oppførsel for kallere med én rad per URL, f.eks.
  // round_hole). NØDVENDIG å sette eksplisitt når flere ulike rader deler
  // samme URL (f.eks. scoring.py sin /hole-scores, som tar imot ALLE hull
  // i en match på nøyaktig samme endepunkt) -- ellers ville versjons-
  // kjedingen i flushQueue under blandet sammen versjonsnummer mellom
  // urelaterte rader (se ADR-060).
  resourceKey?: string
  // Multipart-skrivinger (kommentar+bilde til rundefeeden, 2026-08-14) --
  // `body` er da et FLATT objekt av string/Blob-felt i stedet for en
  // JSON-serialiserbar verdi. IndexedDB sin structured-clone-algoritme
  // lagrer Blob-verdier (og dermed File, som arver fra Blob) NATIVT --
  // ingen egen blob-lagringsstruktur trengs. Uendret oppførsel
  // (JSON.stringify + Content-Type: application/json) når feltet mangler.
  isMultipart?: boolean
}

function openDb(): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, DB_VERSION)
    req.onupgradeneeded = () => {
      const db = req.result
      if (!db.objectStoreNames.contains(STORE)) {
        const store = db.createObjectStore(STORE, { keyPath: "id", autoIncrement: true })
        store.createIndex("matchId", "matchId", { unique: false })
      }
    }
    req.onsuccess = () => resolve(req.result)
    req.onerror = () => reject(req.error)
  })
}

export async function enqueueWrite(
  entry: Omit<QueueEntry, "id" | "createdAt">,
): Promise<number> {
  const db = await openDb()
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE, "readwrite")
    const req = tx.objectStore(STORE).add({ ...entry, createdAt: Date.now() })
    req.onsuccess = () => resolve(req.result as number)
    req.onerror = () => reject(req.error)
  })
}

export async function listQueue(matchId: string): Promise<QueueEntry[]> {
  const db = await openDb()
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE, "readonly")
    const req = tx.objectStore(STORE).index("matchId").getAll(matchId)
    req.onsuccess = () =>
      resolve((req.result as QueueEntry[]).sort((a, b) => a.createdAt - b.createdAt))
    req.onerror = () => reject(req.error)
  })
}

async function removeFromQueue(id: number): Promise<void> {
  const db = await openDb()
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE, "readwrite")
    tx.objectStore(STORE).delete(id)
    tx.oncomplete = () => resolve()
    tx.onerror = () => reject(tx.error)
  })
}

export async function queueCount(matchId: string): Promise<number> {
  return (await listQueue(matchId)).length
}

export type FlushOutcome = { entry: QueueEntry; ok: boolean; message?: string }

// Sender køede skrivinger i rekkefølge, ETT om gangen (ikke parallelt) --
// server-siden er en upsert per hull, så rekkefølgen har betydning hvis
// samme hull ble endret flere ganger offline. Stopper ved første ekte
// nettverksfeil (resten blir liggende til neste forsøk); en DEFINITIV
// HTTP-feil (f.eks. 409 "matchen er avgjort") fjernes fra køen og
// rapporteres tilbake i stedet for å bli hengende for alltid.
export async function flushQueue(matchId: string): Promise<FlushOutcome[]> {
  const entries = await listQueue(matchId)
  const outcomes: FlushOutcome[] = []
  // Kjeder expected_version fremover for flere køede skrivinger til SAMME
  // RESSURS i én flush-runde (2026-08-10, ADR-057 versjonssjekk; nøkkel
  // generalisert til resourceKey 2026-08-10, ADR-060). Uten dette ville en
  // spillers EGNE påfølgende offline-redigeringer av samme rad avvist
  // hverandre som falske konflikter: kun den FØRSTE køede oppføringen har
  // en expected_version som fortsatt stemmer med serveren -- lokal state
  // (og dermed enqueue-tidspunktets versjon) oppdateres aldri mellom to
  // sekvensielle avspillinger i samme batch, så oppføring 2 ville ellers
  // sendt den samme (nå utdaterte) versjonen som 1.
  //
  // NØKKELEN er `resourceKey ?? url`, IKKE bare url -- for round_hole er
  // url alene entydig (én URL per hull), men scoring.py sine /hole-scores/
  // /hole-results tar imot ALLE hull i en match på nøyaktig samme URL. Uten
  // en mer presis nøkkel ville versjonen fra hull 3 sin skriving blitt
  // brukt som "forventet versjon" for en påfølgende, helt urelatert
  // skriving til hull 7 -- verre enn ingen kjeding i det hele tatt.
  const latestVersionByKey = new Map<string, number>()
  for (const entry of entries) {
    const key = entry.resourceKey ?? entry.url
    let body: unknown = entry.body
    if (
      !entry.isMultipart &&
      body !== null &&
      typeof body === "object" &&
      "expected_version" in body &&
      latestVersionByKey.has(key)
    ) {
      body = { ...body, expected_version: latestVersionByKey.get(key) }
    }
    let res: Response
    try {
      if (entry.isMultipart) {
        const formData = new FormData()
        for (const [k, v] of Object.entries(body as Record<string, string | Blob | undefined | null>)) {
          if (v === undefined || v === null) continue
          formData.append(k, v)
        }
        res = await fetch(entry.url, { method: entry.method, credentials: "include", body: formData })
      } else {
        res = await fetch(entry.url, {
          method: entry.method,
          headers: { "Content-Type": "application/json" },
          credentials: "include",
          body: JSON.stringify(body),
        })
      }
    } catch {
      break // fortsatt offline -- stopp, resten prøves igjen senere
    }
    if (res.ok) {
      const updated: { version?: number } | null = await res.json().catch(() => null)
      if (updated && typeof updated.version === "number") {
        latestVersionByKey.set(key, updated.version)
      }
      await removeFromQueue(entry.id)
      outcomes.push({ entry, ok: true })
    } else {
      const respBody: { detail?: { message?: string } } | null = await res.json().catch(() => null)
      await removeFromQueue(entry.id)
      outcomes.push({ entry, ok: false, message: respBody?.detail?.message ?? `Feil ${res.status}` })
    }
  }
  return outcomes
}
