import { useCallback, useEffect, useMemo, useState } from 'react'
import { Anchor, Download, Headphones, Disc3 } from 'lucide-react'
import type { Decision, DecisionSet, JudgeSet, Verdict } from '@/types'
import { TrackRow } from './TrackRow'
import { VERDICTS } from './verdict'

type Source = 'head' | 'full'
type Calls = Record<string, { verdict: Verdict; comment: string; tS?: number }>

/** Which set to judge: ?set=<gig>, else the only one we ship. */
const gigParam = new URLSearchParams(location.search).get('set') ?? 'opal-festival-2026'
const KEY = `pv_setjudge_${gigParam}`

/**
 * The set-judge: one pass down a whole gig, a verdict + a comment per track,
 * exported as decisions.json.
 *
 * The reason it exists is that this call was being made in a music player with
 * the answers landing in a chat log — so the same track got re-auditioned weeks
 * later with nobody able to say why it was cut. Ear-feedback is the scarce
 * input in this pipeline; this makes it an artifact.
 *
 * Calls persist to localStorage on every keystroke. An hour of listening lost to
 * a refresh is not a bug you get to make twice.
 */
export default function SetJudge() {
  const [data, setData] = useState<JudgeSet | null>(null)
  const [err, setErr] = useState<string | null>(null)
  const [calls, setCalls] = useState<Calls>(() => {
    try { return JSON.parse(localStorage.getItem(KEY) || '{}') } catch { return {} }
  })
  const [focus, setFocus] = useState(0)
  const [source, setSource] = useState<Source>('full')
  const [t, setT] = useState(0)

  useEffect(() => {
    fetch(`/judge-${gigParam}.json`)
      .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`${r.status} ${r.statusText}`))))
      .then(setData)
      .catch((e) => setErr(String(e.message ?? e)))
  }, [])

  useEffect(() => { localStorage.setItem(KEY, JSON.stringify(calls)) }, [calls])

  const tracks = data?.tracks ?? []
  const call = (id: string) => calls[id] ?? { verdict: 'pending' as Verdict, comment: '' }

  const setVerdict = useCallback((id: string, v: Verdict, tS?: number) => {
    setCalls((c) => ({ ...c, [id]: { ...(c[id] ?? { comment: '' }), verdict: v, tS } }))
  }, [])
  const setComment = useCallback((id: string, comment: string) => {
    setCalls((c) => ({ ...c, [id]: { verdict: 'pending', ...(c[id] ?? {}), comment } }))
  }, [])

  const decided = useMemo(
    () => tracks.filter((x) => (calls[x.id]?.verdict ?? 'pending') !== 'pending').length,
    [tracks, calls],
  )
  const tally = useMemo(() => {
    const m: Record<string, number> = { go: 0, maybe: 0, nogo: 0, pending: 0 }
    for (const x of tracks) m[calls[x.id]?.verdict ?? 'pending']++
    return m
  }, [tracks, calls])

  // Keyboard: the whole point of a judging pass is not touching the mouse.
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      const el = e.target as HTMLElement
      if (el?.tagName === 'TEXTAREA' || el?.tagName === 'INPUT') {
        if (e.key === 'Escape') el.blur()
        return
      }
      const cur = tracks[focus]
      if (!cur) return
      const k = e.key.toLowerCase()
      if (k === 'j' || e.key === 'ArrowDown') { e.preventDefault(); setFocus((i) => Math.min(tracks.length - 1, i + 1)) }
      else if (k === 'k' || e.key === 'ArrowUp') { e.preventDefault(); setFocus((i) => Math.max(0, i - 1)) }
      else if (k === 'g') setVerdict(cur.id, 'go', t)
      else if (k === 'm') setVerdict(cur.id, 'maybe', t)
      else if (k === 'n') setVerdict(cur.id, 'nogo', t)
      else if (k === '0') setVerdict(cur.id, 'pending')
      else if (k === 'c') {
        e.preventDefault()
        document.querySelector<HTMLTextAreaElement>('li textarea')?.focus()
      }
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [tracks, focus, t, setVerdict])

  const exportDecisions = () => {
    if (!data) return
    const out: DecisionSet = {
      gig: data.gig,
      variant: source === 'full' ? data.variant : data.variant,
      decidedAt: new Date().toISOString(),
      decisions: tracks.map<Decision>((x) => ({
        trackId: x.id,
        n: x.n,
        title: x.title,
        verdict: call(x.id).verdict,
        comment: call(x.id).comment,
        tS: call(x.id).tS ?? null,
      })),
    }
    const blob = new Blob([JSON.stringify(out, null, 2)], { type: 'application/json' })
    const a = document.createElement('a')
    a.href = URL.createObjectURL(blob)
    a.download = `decisions-${data.gig}.json`
    a.click()
    URL.revokeObjectURL(a.href)
  }

  if (err) {
    return (
      <div className="grid min-h-svh place-items-center px-6 text-center">
        <div>
          <p className="text-sm text-blocked">Couldn&apos;t load /judge-{gigParam}.json — {err}</p>
          <p className="mt-2 text-xs text-ink-faint">
            Build it: <span className="font-mono">python3 armada/tide-table/build_judge_set.py
            armada/tide-table/judge_specs/&lt;gig&gt;.json</span>
          </p>
        </div>
      </div>
    )
  }
  if (!data) {
    return (
      <div className="grid min-h-svh place-items-center text-ink-muted">
        <p className="animate-pulse text-sm">loading the bridge…</p>
      </div>
    )
  }

  return (
    <div className="mx-auto flex min-h-svh max-w-4xl flex-col gap-4 px-4 py-5">
      <header className="flex flex-wrap items-end justify-between gap-3 border-b border-hairline pb-4">
        <div className="min-w-0">
          <p className="flex items-center gap-1.5 text-[11px] uppercase tracking-widest text-ink-faint">
            <Anchor size={12} /> L&apos;Armada · set judge
          </p>
          <h1 className="truncate text-2xl font-semibold tracking-tight">
            {data.title} <span className="text-ink-muted">— {data.date}</span>
          </h1>
          <p className="mt-0.5 text-xs text-ink-faint">{data.calibration}</p>
        </div>

        <div className="flex items-center gap-3">
          <div className="flex rounded-md border border-hairline p-0.5 text-xs">
            {([['head', Headphones, '16 s'], ['full', Disc3, 'full']] as const).map(
              ([v, Icon, label]) => (
                <button
                  key={v}
                  onClick={() => setSource(v)}
                  className={`flex items-center gap-1.5 rounded px-2.5 py-1 transition-colors ${
                    source === v ? 'bg-ink text-surface' : 'text-ink-muted hover:text-ink'
                  }`}
                >
                  <Icon size={12} /> {label}
                </button>
              ),
            )}
          </div>
          <button
            onClick={exportDecisions}
            disabled={!decided}
            className="flex items-center gap-1.5 rounded-md bg-raised px-3 py-1.5 text-xs
                       font-medium hover:bg-overlay disabled:opacity-30"
          >
            <Download size={13} /> decisions.json
          </button>
        </div>
      </header>

      {/* progress — the tally is the state of the pass, at a glance */}
      <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs">
        <span className="text-ink-muted tnum">
          {decided}/{tracks.length} judged
        </span>
        {VERDICTS.map((v) => (
          <span key={v.key} className="flex items-center gap-1 tnum" style={{ color: v.color }}>
            <span aria-hidden>{v.glyph}</span>
            {tally[v.key] ?? 0} {v.label.toLowerCase()}
          </span>
        ))}
        <span className="ml-auto font-mono text-[11px] text-ink-faint">
          j/k move · g go · m maybe · n no-go · c comment
        </span>
      </div>

      {source === 'head' && (
        <p className="rounded-md border border-hairline bg-raised px-3 py-2 text-[11px] text-ink-muted">
          Heads are each track&apos;s first 16 s — verified bit-exact against the track, so
          you are hearing 15 <em>intros</em>. Good for a running-order scan, thin evidence for a
          ship/cut call. The orbit rail always covers the whole track.
        </p>
      )}

      <ul className="flex flex-col gap-1">
        {tracks.map((track, i) => (
          <TrackRow
            key={track.id}
            track={track}
            roleGroups={data.roleGroups}
            activeDb={data.activeDb}
            litFloorDb={data.litFloorDb}
            expanded={i === focus}
            source={source}
            verdict={call(track.id).verdict}
            comment={call(track.id).comment}
            currentTime={i === focus ? t : 0}
            onExpand={() => setFocus(i)}
            onTime={setT}
            onVerdict={(v) => setVerdict(track.id, v, t)}
            onComment={(c) => setComment(track.id, c)}
          />
        ))}
      </ul>

      <footer className="mt-auto border-t border-hairline pt-3 text-[11px] text-ink-faint">
        {data.note && <p className="mb-1">{data.note}</p>}
        Calls persist locally as you make them; export writes the decisions.json the release
        step reads. Orbit labels come from each track&apos;s <span className="font-mono">.tidal</span>{' '}
        score, activity is measured from the stems.
      </footer>
    </div>
  )
}
