// consent.js — cookie / privacy consent state (v0.13.0, SPEC §14.5).
//
// The framework's analytics SDK gating (roadmap item #13, target v0.15.0)
// will read from this module. v0.13.0 ships the storage + the banner +
// the on-change pub/sub; no analytics SDK ships yet.
//
// Shape of a consent record:
//
//     { essential: true, analytics: bool, other: bool, recorded_at: string | null }
//
// `essential` is always true at the API surface; it's included for
// symmetry. `recorded_at` is null when the user has not yet made a
// choice — the banner is shown until it's non-null.
//
// Precedence:
//   - Anonymous viewer: localStorage is the only source.
//   - Authenticated viewer: on sign-in, the server row (if any) overrides
//     local; if the server has no row, the local choice is uploaded.
//
// The fan-out is intentionally tiny — three flags. The banner writes
// once; subscribers re-read on demand via `getConsent()` and can
// register `onConsentChange(cb)` to be notified of subsequent updates.
//
// IMPORTANT: don't import this from analytics SDKs that themselves
// set cookies on load. Read consent first, then conditionally `import()`
// the SDK module — that's the contract item #13 will follow.

const LS_KEY = 'rfc-app.cookie-consent.v1'

const DEFAULT = Object.freeze({
  essential: true,
  analytics: false,
  other: false,
  recorded_at: null,
})

const listeners = new Set()

function readLocal() {
  try {
    const raw = localStorage.getItem(LS_KEY)
    if (!raw) return null
    const parsed = JSON.parse(raw)
    if (!parsed || typeof parsed !== 'object') return null
    return normalize(parsed)
  } catch {
    return null
  }
}

function writeLocal(record) {
  try {
    localStorage.setItem(LS_KEY, JSON.stringify(normalize(record)))
  } catch {
    // localStorage may be unavailable (private mode, disabled storage).
    // In that case we behave as if no choice was ever made — the banner
    // shows on every load. Acceptable per §14.5: the user can still
    // refuse to consent on each visit.
  }
}

function normalize(record) {
  return {
    essential: true,
    analytics: !!record.analytics,
    other: !!record.other,
    recorded_at: record.recorded_at || null,
  }
}

// In-memory snapshot. Initialised lazily on first read so the module
// import order doesn't matter; refreshed by `setConsent` and
// `hydrateFromServer`.
let _snapshot = null

function snapshot() {
  if (_snapshot == null) {
    _snapshot = readLocal() || { ...DEFAULT }
  }
  return _snapshot
}

function emit() {
  for (const cb of listeners) {
    try { cb(snapshot()) } catch {}
  }
}

/** Read the current consent record. Always returns a normalized object;
 *  `recorded_at: null` means the user has not yet chosen. */
export function getConsent() {
  return snapshot()
}

/** True if the user has made a choice. The banner uses this to decide
 *  whether to render itself on load. */
export function hasChosen() {
  return snapshot().recorded_at != null
}

/** Subscribe to consent updates. Returns an unsubscribe function. */
export function onConsentChange(cb) {
  listeners.add(cb)
  return () => listeners.delete(cb)
}

/** Write a new choice locally and emit. Returns the new snapshot. The
 *  server-side persistence path is handled separately by the banner /
 *  settings surface via the API client; this helper is for both anon
 *  and authenticated callers because localStorage is the always-on
 *  layer (the server row is a backup that survives sign-out). */
export function setConsent({ analytics = false, other = false } = {}) {
  const next = normalize({
    analytics,
    other,
    recorded_at: new Date().toISOString(),
  })
  _snapshot = next
  writeLocal(next)
  emit()
  return next
}

/** Adopt a server-side record as authoritative. Called by the banner /
 *  settings surface after sign-in when the server returns a non-null
 *  recorded_at. Updates local + memory + emits to subscribers. */
export function hydrateFromServer(record) {
  if (!record || !record.recorded_at) return snapshot()
  const next = normalize(record)
  _snapshot = next
  writeLocal(next)
  emit()
  return next
}

/** Reset local state — used by the settings "Change" affordance to
 *  re-prompt the banner. Does not touch the server row; the user must
 *  re-confirm a choice and the banner uploads on save. */
export function clearLocal() {
  try { localStorage.removeItem(LS_KEY) } catch {}
  _snapshot = { ...DEFAULT }
  emit()
  return _snapshot
}

// Cross-tab sync: if another tab writes the key, mirror the change here.
// Wrapped in a guard so SSR / non-browser test contexts don't blow up.
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
  window.addEventListener('storage', e => {
    if (e.key !== LS_KEY) return
    _snapshot = readLocal() || { ...DEFAULT }
    emit()
  })
}
