import { useEffect, useMemo, useRef, useState, type FC } from 'react'
import { audio } from './audio'
import { BarricelliLuggageMerit } from './merits'
import type { CutscenePresentation } from './narrativeContract'

export type RuntimeUtterance = { id: string; utterer: 'npc' | 'player'; speaker: { name: string; role: string }; poseUrl: string | null; text: string; childIds: string[]; terminalKey: string | null; awardsFlag?: string | null }
export type RuntimeNode = { id: string; kind: 'cutscene' | 'dialogue' | 'level' | 'merit'; label: string; componentKey?: string | null; levelSlug?: string | null; musicUrl?: string | null; musicVolume?: number; awardsFlag?: string | null; presentation?: CutscenePresentation | null; utterances?: RuntimeUtterance[]; rootId?: string | null }
export type PlaythroughSummary = { id: string; mysterySlug: string; levelSlug: string | null; status: string }
export type PlaythroughState = { playthrough: PlaythroughSummary; node: RuntimeNode | null }

function usePrefersReducedMotion() {
  const [reduced, setReduced] = useState(() => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false)
  useEffect(() => {
    const query = window.matchMedia?.('(prefers-reduced-motion: reduce)')
    if (!query) return
    const listener = (event: MediaQueryListEvent) => setReduced(event.matches)
    query.addEventListener('change', listener)
    return () => query.removeEventListener('change', listener)
  }, [])
  return reduced
}

export function SplashScreen({ hasResume, busy, status, onNewGame, onResume }: {
  hasResume: boolean; busy: boolean; status?: string; onNewGame: () => void; onResume: () => void
}) {
  return <div className="splash">
    <div className="splash-plate">
      <div className="seal">GU</div>
      <h1 className="splash-title">PRINCIPAL INVESTIGATOR</h1>
      <p className="splash-sub">Glitch University</p>
      <div className="splash-actions">
        {hasResume && <button className="splash-button" disabled={busy} onClick={onResume}>RESUME</button>}
        <button className="splash-button primary" disabled={busy} onClick={onNewGame}>NEW GAME</button>
      </div>
      <small className="splash-status">{busy ? 'OPENING CASE FILE…' : status || 'GLITCH UNIVERSITY NETWORK TERMINAL'}</small>
    </div>
  </div>
}

// Bespoke cutscene components, keyed by a node's component_key (mirrors the exhibit registry).
type CutsceneComponentProps = { label: string; presentation?: CutscenePresentation | null; onComplete: () => void }

const GlassHarbourDiversion: FC<CutsceneComponentProps> = ({ onComplete }) => (
  <div className="cutscene-card title-card" onClick={onComplete}>
    <div className="title-card-inner">
      <small>Greyhaven file 87-10</small>
      <h1>The Glass Harbour Diversion</h1>
      <button className="cutscene-begin" onClick={event => { event.stopPropagation(); onComplete() }}>Begin ▸</button>
    </div>
  </div>
)

const CaseAdjudication: FC<CutsceneComponentProps> = ({ label, presentation, onComplete }) => {
  const reducedMotion = usePrefersReducedMotion()
  const [phase, setPhase] = useState<'reviewing' | 'finding' | 'stamped'>(reducedMotion ? 'stamped' : 'reviewing')
  const finding = presentation?.kind === 'case-adjudication' ? presentation.finding : 'SUPPORTED BY THE SUBMITTED EVIDENCE'
  const [findingCharacters, setFindingCharacters] = useState(reducedMotion ? finding.length : 0)
  const claims = presentation?.kind === 'case-adjudication' ? presentation.claims : [{ statement: label, evidence: [] }]
  const stampText = presentation?.kind === 'case-adjudication' ? presentation.stampText : 'CASE VERIFIED'

  useEffect(() => {
    if (reducedMotion) return
    const findingTimer = window.setTimeout(() => setPhase('finding'), 650)
    const stampTimer = window.setTimeout(() => { setPhase('stamped'); audio.sfx('sting') }, 2850)
    return () => { window.clearTimeout(findingTimer); window.clearTimeout(stampTimer) }
  }, [reducedMotion])

  useEffect(() => {
    if (phase === 'reviewing' || findingCharacters >= finding.length) return
    if (reducedMotion) { setFindingCharacters(finding.length); return }
    const timer = window.setTimeout(() => {
      setFindingCharacters(count => Math.min(finding.length, count + 1))
      if (findingCharacters % 3 === 0) audio.type()
    }, 22)
    return () => window.clearTimeout(timer)
  }, [phase, finding, findingCharacters, reducedMotion])

  return <div className={`case-adjudication phase-${phase}`} role="dialog" aria-label="Case adjudication">
    <div className="adjudication-veil" aria-hidden="true"/>
    <article className="adjudication-paper">
      <header><span>GLITCH UNIVERSITY</span><b>GUPI EVIDENCE REVIEW</b><small>{presentation?.kind === 'case-adjudication' ? presentation.reportTitle : label}</small></header>
      <div className="adjudication-rule"/>
      {claims.map((claim, claimIndex) => <section className="adjudication-claim" key={`${claimIndex}:${claim.statement}`}>
        <h2>CLAIM {String(claimIndex + 1).padStart(2, '0')}</h2>
        <blockquote>{claim.statement}</blockquote>
        <h3>EVIDENCE REVIEWED</h3>
        {claim.evidence.length ? claim.evidence.map(item => <div className="adjudication-evidence" key={`${item.displayNumber}:${item.title}`}>
          <b>EXHIBIT {item.displayNumber} — ACCEPTED</b>
          <span>{item.title}{item.publishedAt ? ` · ${item.publishedAt.slice(0, 10)}` : ''}</span>
          {item.sourceCitation && <small>{item.sourceCitation}</small>}
        </div>) : <div className="adjudication-evidence"><b>ACCEPTED REPORT ON FILE</b></div>}
      </section>)}
      <div className="adjudication-finding"><span>FINDING</span><strong>{finding.slice(0, findingCharacters)}{phase === 'finding' && findingCharacters < finding.length ? '▍' : ''}</strong></div>
      {presentation?.kind === 'case-adjudication' && <footer><span>INVESTIGATOR: {presentation.investigatorName}</span><span>FILED: {presentation.submittedAt.slice(0, 10)}</span></footer>}
      <div className="adjudication-stamp" aria-hidden={phase !== 'stamped'}>{stampText}</div>
      {phase === 'stamped' && <button type="button" onClick={onComplete}>RECEIVE MERIT <span>▸</span></button>}
    </article>
  </div>
}

const CUTSCENE_REGISTRY: Record<string, FC<CutsceneComponentProps>> = {
  'glass-harbour-diversion': GlassHarbourDiversion,
  'case-adjudication': CaseAdjudication,
}
export const CUTSCENE_COMPONENT_KEYS = Object.keys(CUTSCENE_REGISTRY)

// Merit ceremony components, keyed by a merit node's component_key (e.g. a 3D
// award model). The achievement itself is granted server-side on arrival; this is
// purely the presentation of receiving it.
const MERIT_REGISTRY: Record<string, FC<{ label: string; onComplete: () => void }>> = { 'barricelli-luggage': BarricelliLuggageMerit }
export const MERIT_COMPONENT_KEYS = Object.keys(MERIT_REGISTRY)

export function MeritHost({ componentKey, label, awardsFlag, onComplete }: { componentKey: string | null | undefined; label: string; awardsFlag?: string | null; onComplete: () => void }) {
  const Component = componentKey ? MERIT_REGISTRY[componentKey] : undefined
  if (Component) return <Component label={label} onComplete={onComplete} />
  return <div className="cutscene-card merit-card" onClick={onComplete}>
    <div className="title-card-inner">
      <small className="merit-eyebrow">◆ MERIT AWARDED ◆</small>
      <h1>{label}</h1>
      {awardsFlag && <p className="merit-flag">🏅 {awardsFlag}</p>}
      <button className="cutscene-begin" onClick={event => { event.stopPropagation(); onComplete() }}>Accept ▸</button>
    </div>
  </div>
}

export function CutsceneHost({ componentKey, label, presentation, onComplete }: { componentKey: string | null | undefined; label: string; presentation?: CutscenePresentation | null; onComplete: () => void }) {
  const Component = componentKey ? CUTSCENE_REGISTRY[componentKey] : undefined
  if (Component) return <Component label={label} presentation={presentation} onComplete={onComplete} />
  return <div className="cutscene-card title-card" onClick={onComplete}>
    <div className="title-card-inner">
      <h1>{label}</h1>
      <small className="cutscene-missing">{componentKey ? `component "${componentKey}" not registered` : 'no component set'}</small>
      <button className="cutscene-begin" onClick={event => { event.stopPropagation(); onComplete() }}>Continue ▸</button>
    </div>
  </div>
}

// Walk a dialogue node's utterance tree: play NPC lines, present player options at a
// branch, follow a chosen option to the next line or out through its exit terminal.
export function DialoguePlayer({ node, onExit, onAward, onCapture, inline, startId }: { node: { utterances: RuntimeUtterance[]; rootId: string | null }; onExit: (terminalKey?: string) => void; onAward?: (utteranceId: string) => void; onCapture?: (text: string, utteranceId: string) => void; inline?: boolean; startId?: string | null }) {
  const byId = useMemo(() => new Map(node.utterances.map(u => [u.id, u])), [node.utterances])
  const [currentId, setCurrentId] = useState<string | null>(startId ?? node.rootId)
  const onAwardRef = useRef(onAward)
  onAwardRef.current = onAward
  // In preview, clicking an utterance card jumps the walk to that line.
  useEffect(() => { if (startId !== undefined) setCurrentId(startId ?? node.rootId) }, [startId, node.rootId])
  const [charCount, setCharCount] = useState(0)
  const reduced = usePrefersReducedMotion()
  const current = currentId ? byId.get(currentId) ?? null : null
  const fullText = current?.text ?? ''
  const done = charCount >= fullText.length
  const children = current ? current.childIds.map(id => byId.get(id)).filter((c): c is RuntimeUtterance => Boolean(c)) : []
  const options = children.filter(c => c.utterer === 'player')
  const showChoices = done && options.length > 0

  useEffect(() => {
    if (!current) { onExit(); return }
    if (reduced) { setCharCount(fullText.length); return }
    setCharCount(0)
    const id = window.setInterval(() => setCharCount(count => (count >= fullText.length ? count : count + 1)), 18)
    return () => window.clearInterval(id)
  }, [currentId, fullText, reduced]) // eslint-disable-line react-hooks/exhaustive-deps

  // Typewriter clatter as characters are revealed (every other non-space char).
  useEffect(() => {
    if (inline || charCount === 0 || charCount > fullText.length) return
    const ch = fullText[charCount - 1]
    if (ch && ch !== ' ' && charCount % 2 === 0) audio.type()
  }, [charCount]) // eslint-disable-line react-hooks/exhaustive-deps

  // Grant a line's authored achievement when it becomes current (play mode only).
  useEffect(() => {
    if (inline || !currentId) return
    const utterance = byId.get(currentId)
    if (utterance?.awardsFlag) onAwardRef.current?.(utterance.id)
  }, [currentId, inline, byId])

  const pick = (choice: RuntimeUtterance) => {
    if (!inline) audio.sfx('choice')
    if (!inline && choice.awardsFlag) onAwardRef.current?.(choice.id)
    if (choice.childIds.length > 0) setCurrentId(choice.childIds[0])
    else onExit(choice.terminalKey ?? undefined)
  }
  const proceedRef = useRef(() => {})
  proceedRef.current = () => {
    if (!current) { onExit(); return }
    if (!done) { setCharCount(fullText.length); return }
    if (children.length === 0) { onExit(current.terminalKey ?? undefined); return }
    if (options.length > 0) return // a branch — wait for a choice
    if (!inline) audio.sfx('advance')
    setCurrentId(children[0].id) // linear next line
  }
  useEffect(() => {
    if (inline) return // preview advances by click only, so it never steals the editor's keys
    const onKey = (event: KeyboardEvent) => {
      if (!showChoices && (event.key === ' ' || event.key === 'Enter' || event.key === 'ArrowRight')) { event.preventDefault(); proceedRef.current() }
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [showChoices, inline])

  if (!current) return null
  return <div className={`dialogue${inline ? ' inline' : ''}`} role="dialog" aria-label="Dialogue" onClick={() => { if (!showChoices) proceedRef.current() }}>
    <div className="dialogue-portrait">{current.poseUrl && <img src={current.poseUrl} alt={current.speaker.name} />}</div>
    <div className="dialogue-scrim" aria-hidden />
    <div className="dialogue-box">
      <div className="dialogue-panel">
        <div className="dialogue-speaker"><strong>{current.speaker.name}</strong>{current.speaker.role && <em>{current.speaker.role}</em>}
          {onCapture && !inline && current.utterer === 'npc' && done && <button className="dialogue-capture" title="Copy to notebook" onClick={event => { event.stopPropagation(); onCapture(current.text, current.id) }}>✎ Note this</button>}
        </div>
        <p className="dialogue-text">{fullText.slice(0, charCount)}<span className="dialogue-caret" aria-hidden>{done ? '' : '▍'}</span></p>
        {showChoices
          ? <div className="dialogue-choices">{options.map(option => <button key={option.id} onClick={event => { event.stopPropagation(); pick(option) }}>{option.text || '(choice)'}</button>)}</div>
          : <div className="dialogue-advance">{done ? 'CONTINUE ▸' : ''}</div>}
      </div>
    </div>
  </div>
}

// A live, scaled-down mini player for the editor — runs the real DialoguePlayer
// against the same server resolver, so it reflects the current authored dialogue.
export function DialoguePreview({ nodeId, startId, revision, onClose }: { nodeId: string; startId?: string | null; revision?: number; onClose?: () => void }) {
  const [tree, setTree] = useState<{ utterances: RuntimeUtterance[]; rootId: string | null } | null>(null)
  const [playKey, setPlayKey] = useState(0)
  useEffect(() => {
    fetch(`/api/admin/story-nodes/${nodeId}/dialogue`).then(response => response.ok ? response.json() : null).then(setTree).catch(() => setTree(null))
  }, [nodeId, revision])
  return <div className="dialogue-preview" onPointerDown={event => event.stopPropagation()}>
    <div className="dialogue-preview-bar"><span>preview</span>
      <button title="Restart" onClick={event => { event.stopPropagation(); setPlayKey(key => key + 1) }}>↻</button>
      {onClose && <button title="Close" onClick={event => { event.stopPropagation(); onClose() }}>×</button>}
    </div>
    <div className="dialogue-preview-stage">
      {tree?.rootId
        ? <div className="dialogue-preview-scale"><DialoguePlayer key={playKey} inline node={tree} startId={startId} onExit={() => setPlayKey(key => key + 1)} /></div>
        : <div className="dialogue-preview-empty">no utterances yet</div>}
    </div>
  </div>
}
