// ─────────────────────────────────────────────────────────────
// libreDecision v2 — local-first persistence (IndexedDB via idb-keyval).
// One key per collective: `ld2:<collectiveId>` holds a Bundle-shaped state.
// Seeds and user imports go through the SAME importBundle() path.
// Sync-ready invariants live in types/domain.ts — this layer stays dumb.
// ─────────────────────────────────────────────────────────────

import { get, set, del, keys } from 'idb-keyval'
import type { Bundle, Collective, Id, ISODate } from '~/types/domain'

const KEY_PREFIX = 'ld2:'
const ACTIVE_KEY = 'ld2-active'

/** In-memory state of one collective — exactly a Bundle minus export metadata. */
export type CollectiveState = Omit<Bundle, 'schemaVersion' | 'exportedAt'>

export function emptyState(collective: Collective): CollectiveState {
  return {
    collective,
    people: [],
    circles: [],
    decisions: [],
    concerns: [],
    objections: [],
    advices: [],
    assents: [],
    mandates: [],
    docs: [],
    clauses: [],
    versions: [],
    protocols: [],
    sessions: [],
    votes: [],
  }
}

// ── Load / save ──────────────────────────────────────────────

export async function listCollectiveIds(): Promise<Id[]> {
  const all = await keys()
  return all
    .filter((k): k is string => typeof k === 'string' && k.startsWith(KEY_PREFIX))
    .map(k => k.slice(KEY_PREFIX.length))
}

export async function loadState(collectiveId: Id): Promise<CollectiveState | undefined> {
  return await get<CollectiveState>(KEY_PREFIX + collectiveId)
}

const pendingSaves = new Map<Id, ReturnType<typeof setTimeout>>()

/** Debounced write (500 ms) — every mutation calls this; last write wins. */
export function saveStateDebounced(state: CollectiveState): void {
  const id = state.collective.id
  const existing = pendingSaves.get(id)
  if (existing) clearTimeout(existing)
  pendingSaves.set(
    id,
    setTimeout(() => {
      pendingSaves.delete(id)
      void set(KEY_PREFIX + id, toRaw(state))
    }, 500),
  )
}

export async function saveStateNow(state: CollectiveState): Promise<void> {
  const id = state.collective.id
  const existing = pendingSaves.get(id)
  if (existing) {
    clearTimeout(existing)
    pendingSaves.delete(id)
  }
  await set(KEY_PREFIX + id, toRaw(state))
}

export async function deleteCollective(collectiveId: Id): Promise<void> {
  await del(KEY_PREFIX + collectiveId)
  const active = await getActiveCollectiveId()
  if (active === collectiveId) await set(ACTIVE_KEY, null)
}

export async function getActiveCollectiveId(): Promise<Id | null> {
  return (await get<Id | null>(ACTIVE_KEY)) ?? null
}

export async function setActiveCollectiveId(id: Id | null): Promise<void> {
  await set(ACTIVE_KEY, id)
}

// ── Export / import — the single bundle path ─────────────────

export function toBundle(state: CollectiveState, exportedAt: ISODate): Bundle {
  return { schemaVersion: 2, exportedAt, ...toRaw(state) }
}

export async function sha256Hex(text: string): Promise<string> {
  const data = new TextEncoder().encode(text)
  const digest = await crypto.subtle.digest('SHA-256', data)
  return Array.from(new Uint8Array(digest))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('')
}

export interface ImportIssue {
  level: 'error' | 'warning'
  message: string
}

/** Structural validation — never throws, returns readable French issues. */
export function validateBundle(raw: unknown): { bundle?: Bundle; issues: ImportIssue[] } {
  const issues: ImportIssue[] = []
  if (typeof raw !== 'object' || raw === null) {
    return { issues: [{ level: 'error', message: 'Ce fichier ne contient pas un collectif lisible.' }] }
  }
  const b = raw as Partial<Bundle>
  if (b.schemaVersion !== 2)
    issues.push({ level: 'error', message: 'Version de fichier inconnue (schemaVersion ≠ 2).' })
  if (!b.collective?.id || !b.collective?.slug || !b.collective?.name)
    issues.push({ level: 'error', message: 'Le collectif du fichier est incomplet (id, slug ou nom manquant).' })
  for (const key of [
    'people', 'circles', 'decisions', 'concerns', 'objections', 'advices', 'assents',
    'mandates', 'docs', 'clauses', 'versions', 'protocols', 'sessions', 'votes',
  ] as const) {
    if (!Array.isArray(b[key]))
      issues.push({ level: 'error', message: `Collection manquante ou invalide : ${key}.` })
  }
  // Invariant: every collective carries a Consent protocol (import validates it).
  if (Array.isArray(b.protocols) && !b.protocols.some(p => p.method === 'consent'))
    issues.push({
      level: 'warning',
      message: 'Aucun protocole de consentement — les chemins collectifs retomberont sur « décider sur avis » jusqu\'à sa création.',
    })
  if (issues.some(i => i.level === 'error')) return { issues }
  return { bundle: b as Bundle, issues }
}

export interface ImportResult {
  state?: CollectiveState
  issues: ImportIssue[]
  collided: boolean
}

/**
 * Import a bundle as a new local collective.
 * Foreign bundles (exportedAt present, not one of ours) get a lineage stamp — « essaimé de … ».
 */
export async function importBundle(json: string, opts?: { asSeed?: boolean }): Promise<ImportResult> {
  let raw: unknown
  try {
    raw = JSON.parse(json)
  } catch {
    return { issues: [{ level: 'error', message: 'Fichier illisible : ce n\'est pas du JSON valide.' }], collided: false }
  }
  const { bundle, issues } = validateBundle(raw)
  if (!bundle) return { issues, collided: false }

  const existingIds = await listCollectiveIds()
  const collided = existingIds.includes(bundle.collective.id)

  const { schemaVersion: _v, exportedAt, ...stateRest } = bundle
  const state: CollectiveState = stateRest

  if (!opts?.asSeed) {
    state.collective = {
      ...state.collective,
      lineage: {
        sourceSlug: bundle.collective.slug,
        exportedAt,
        sha256: await sha256Hex(json),
      },
    }
  }
  return { state, issues, collided }
}

/** Deep-clone to plain JSON — strips Pinia/Vue reactivity proxies before IndexedDB. */
function toRaw<T>(value: T): T {
  return JSON.parse(JSON.stringify(value)) as T
}
