/**
 * public/digest.ts (W3 quick-wins c) — "while you were away" reconnect banner.
 *
 * On (re)connect, fetch GET /digest?since=<last-seen> and, if anything happened
 * while away (finished / waiting / stuck), show ONE compact dismissible banner.
 * The last-seen watermark is stored per-device in localStorage and advanced to
 * the digest's generatedAt after each render so it never re-nags for old news.
 *
 * Best-effort: any fetch/parse failure → no banner (never throws). All text is
 * set via textContent (SEC-H5) — session titles are attacker-influenced.
 */

import type { DigestResult } from '../src/types.js'

const LAST_SEEN_KEY = 'web-terminal:digest-last-seen'

/* ── DOM helper ──────────────────────────────────────────────────────────────── */

function el<K extends keyof HTMLElementTagNameMap>(
  tag: K,
  cls?: string,
  text?: string,
): HTMLElementTagNameMap[K] {
  const node = document.createElement(tag)
  if (cls) node.className = cls
  if (text !== undefined) node.textContent = text
  return node
}

/* ── last-seen watermark (per-device) ────────────────────────────────────────── */

/** Read the stored last-seen epoch-ms, or 0 (everything is new). Never throws. */
export function getLastSeen(): number {
  try {
    const raw = localStorage.getItem(LAST_SEEN_KEY)
    if (raw === null) return 0
    const n = Number(raw)
    return Number.isFinite(n) && n >= 0 ? n : 0
  } catch {
    return 0
  }
}

/** Persist the last-seen epoch-ms watermark. Best-effort. */
export function setLastSeen(ms: number): void {
  try {
    localStorage.setItem(LAST_SEEN_KEY, String(Math.floor(ms)))
  } catch {
    // storage unavailable (private mode) — the banner just re-shows next time
  }
}

/* ── normalize (never trust the API shape) ───────────────────────────────────── */

function num(o: Record<string, unknown>, key: string): number {
  const v = o[key]
  return typeof v === 'number' && Number.isFinite(v) ? v : 0
}

/** Coerce an untrusted GET /digest response into a DigestResult, or null. */
export function normalizeDigest(raw: unknown): DigestResult | null {
  if (raw === null || typeof raw !== 'object') return null
  const o = raw as Record<string, unknown>
  if (typeof o['generatedAt'] !== 'number' || !Number.isFinite(o['generatedAt'])) return null
  return {
    since: num(o, 'since'),
    generatedAt: o['generatedAt'],
    total: num(o, 'total'),
    finished: num(o, 'finished'),
    needsInput: num(o, 'needsInput'),
    stuck: num(o, 'stuck'),
    working: num(o, 'working'),
    totalCostUsd: num(o, 'totalCostUsd'),
    sessions: Array.isArray(o['sessions']) ? (o['sessions'] as DigestResult['sessions']) : [],
  }
}

/* ── fetch ───────────────────────────────────────────────────────────────────── */

/** Fetch the digest since `since`. null on any error (best-effort). */
export async function fetchDigest(since: number): Promise<DigestResult | null> {
  try {
    if (typeof fetch === 'undefined') return null
    const res = await fetch(`/digest?since=${encodeURIComponent(String(since))}`)
    if (!res.ok) return null
    return normalizeDigest(await res.json())
  } catch {
    return null
  }
}

/* ── render (pure) ───────────────────────────────────────────────────────────── */

/** Count of things worth surfacing (finished / waiting / stuck). */
export function digestHighlightCount(d: DigestResult): number {
  return d.finished + d.needsInput + d.stuck
}

/** Compact human summary, e.g. "2 finished · 1 waiting · 1 stuck". */
export function digestSummary(d: DigestResult): string {
  const parts: string[] = []
  if (d.finished > 0) parts.push(`${d.finished} finished`)
  if (d.needsInput > 0) parts.push(`${d.needsInput} waiting for input`)
  if (d.stuck > 0) parts.push(`${d.stuck} stuck`)
  return parts.join(' · ')
}

/**
 * Build the banner element for a digest, or null when nothing is worth showing.
 * `onDismiss` is wired to the × button. All text via textContent (SEC-H5).
 */
export function renderDigestBanner(d: DigestResult, onDismiss: () => void): HTMLElement | null {
  if (digestHighlightCount(d) === 0) return null

  const banner = el('div', 'wya-banner')
  banner.setAttribute('role', 'status')
  banner.append(el('span', 'wya-title', 'While you were away'))
  banner.append(el('span', 'wya-summary', digestSummary(d)))
  if (d.totalCostUsd > 0) {
    banner.append(el('span', 'wya-cost', `$${d.totalCostUsd.toFixed(2)} total`))
  }

  const dismiss = el('button', 'wya-dismiss', '✕')
  dismiss.title = 'Dismiss'
  dismiss.setAttribute('aria-label', 'Dismiss')
  dismiss.addEventListener('click', onDismiss)
  banner.append(dismiss)

  return banner
}

/* ── mount ───────────────────────────────────────────────────────────────────── */

/**
 * Fetch the digest since the stored last-seen and, if anything happened, prepend
 * a dismissible banner to `host`. Advances the last-seen watermark to the
 * digest's generatedAt so it doesn't re-nag on the next reconnect. Best-effort:
 * a fetch failure shows no banner. Returns the banner element (or null).
 */
export async function mountDigest(host: HTMLElement): Promise<HTMLElement | null> {
  const since = getLastSeen()
  const d = await fetchDigest(since)
  if (d === null) return null // best-effort — no banner on failure

  // Advance the watermark now so a refresh (without a dismiss) doesn't re-nag.
  setLastSeen(d.generatedAt)

  const banner = renderDigestBanner(d, () => {
    setLastSeen(d.generatedAt)
    banner?.remove()
  })
  if (banner === null) return null

  host.prepend(banner)
  return banner
}
