import { useCallback, useEffect, useLayoutEffect, useRef, useState, type ReactElement } from 'react'
import { DialoguePreview } from './narrative'

type Utterer = 'npc' | 'player'
type Utterance = {
  id: string; nodeId: string; utterer: Utterer; npcId: string | null; poseKey: string | null; text: string
  parentUtteranceId: string | null; terminalId: string | null
  xpos: number; ypos: number; sortOrder: number
}
type Terminal = { id: string; terminalKey: string; label: string }
type Npc = { id: string; name: string; poses: { poseKey: string; url: string }[] }

// Vertical layout: input on top, output on the bottom; exit sinks in a row below.
const UW = 220, UH_DEFAULT = 72, SINK_W = 150

async function api<T>(url: string, method: string, body?: unknown): Promise<T> {
  const response = await fetch(url, { method, headers: body ? { 'content-type': 'application/json' } : undefined, body: body ? JSON.stringify(body) : undefined })
  if (!response.ok) throw new Error((await response.json().catch(() => ({}))).error || `Request failed (${response.status})`)
  return response.json().catch(() => ({} as T))
}

export function UtteranceCanvas({ nodeId, nodeLabel, terminals, onClose, setStatus }: {
  nodeId: string; nodeLabel: string; terminals: Terminal[]; onClose: () => void; setStatus: (message: string) => void
}) {
  const [utterances, setUtterances] = useState<Utterance[]>([])
  const [npcs, setNpcs] = useState<Npc[]>([])
  const [view, setView] = useState({ x: 40, y: 40, zoom: 1 })
  const [selectedId, setSelectedId] = useState<string | null>(null)
  const [wiringFrom, setWiringFrom] = useState<{ id: string } | null>(null)
  const canvasRef = useRef<HTMLDivElement>(null)
  const drag = useRef<{ id: string; startX: number; startY: number; origX: number; origY: number } | 'pan' | null>(null)
  const panRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null)
  // Cards auto-expand to their text, so measure heights to place the bottom output
  // port (offsetHeight is layout px, unaffected by the canvas's transform: scale).
  const cardRefs = useRef(new Map<string, HTMLDivElement>())
  const [heights, setHeights] = useState<Record<string, number>>({})

  const [revision, setRevision] = useState(0)
  const reload = useCallback(async () => {
    try { setUtterances(await api<Utterance[]>(`/api/admin/story-nodes/${nodeId}/utterances`, 'GET')); setRevision(r => r + 1) }
    catch (error) { setStatus(String((error as Error).message || error)) }
  }, [nodeId, setStatus])
  useEffect(() => { void reload(); api<Npc[]>('/api/admin/npcs', 'GET').then(setNpcs).catch(() => {}) }, [reload])

  useLayoutEffect(() => {
    const next: Record<string, number> = {}
    let changed = Object.keys(heights).length !== cardRefs.current.size
    for (const [id, el] of cardRefs.current) { next[id] = el.offsetHeight; if (heights[id] !== next[id]) changed = true }
    if (changed) setHeights(next)
  }, [utterances, heights])

  // Undo stack of inverse operations (connection edits, Tab creation).
  const undoRef = useRef<Array<() => Promise<void>>>([])
  const pushUndo = (fn: () => Promise<void>) => { undoRef.current.push(fn); if (undoRef.current.length > 40) undoRef.current.shift() }
  const doUndo = async () => {
    const fn = undoRef.current.pop()
    if (!fn) { setStatus('Nothing to undo'); return }
    try { await fn(); await reload() } catch (error) { setStatus(String((error as Error).message || error)) }
  }

  // Keyboard (ignored while typing in a field): Ctrl/Cmd+Z undoes; Tab adds a child under
  // the selected utterance (one child = linear, a second makes them player options);
  // 1/2 set the selected utterance's speaker.
  const keyActionRef = useRef((_event: KeyboardEvent) => {})
  keyActionRef.current = (event: KeyboardEvent) => {
    const tag = (document.activeElement?.tagName || '').toLowerCase()
    if (tag === 'input' || tag === 'textarea' || tag === 'select') return
    if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z') { event.preventDefault(); void doUndo(); return }
    if (!selectedId) return
    if (event.key === 'Tab') {
      event.preventDefault()
      const parent = utterances.find(u => u.id === selectedId)
      if (!parent) return
      const siblings = utterances.filter(u => u.parentUtteranceId === parent.id)
      void (async () => {
        try {
          const created = await api<Utterance>(`/api/admin/story-nodes/${nodeId}/utterances`, 'POST',
            { utterer: 'player', xpos: Math.round(parent.xpos + siblings.length * 240), ypos: Math.round(parent.ypos + 130), text: '' })
          await api(`/api/admin/utterances/${created.id}`, 'PATCH', { parentUtteranceId: parent.id })
          // 2+ children ⇒ player options; a lone child stays a linear NPC next line.
          if (siblings.length + 1 >= 2) for (const child of [...siblings, created]) await api(`/api/admin/utterances/${child.id}`, 'PATCH', { utterer: 'player', npcId: null })
          else await api(`/api/admin/utterances/${created.id}`, 'PATCH', { utterer: 'npc' })
          pushUndo(() => api(`/api/admin/utterances/${created.id}`, 'DELETE'))
          await reload(); setSelectedId(parent.id) // keep the parent selected to add more options
        } catch (error) { setStatus(String((error as Error).message || error)) }
      })()
    } else if (event.key === '1') void patch(selectedId, { utterer: 'npc' })
    else if (event.key === '2') void patch(selectedId, { utterer: 'player', npcId: null })
  }
  useEffect(() => {
    const handler = (event: KeyboardEvent) => keyActionRef.current(event)
    window.addEventListener('keydown', handler)
    return () => window.removeEventListener('keydown', handler)
  }, [])

  const patch = async (id: string, body: Record<string, unknown>, silent = false) => {
    try { await api(`/api/admin/utterances/${id}`, 'PATCH', body); if (!silent) await reload() }
    catch (error) { setStatus(String((error as Error).message || error)) }
  }
  const add = async (utterer: Utterer) => {
    const rect = canvasRef.current?.getBoundingClientRect()
    const cx = ((rect ? rect.width / 2 : 250) - view.x) / view.zoom, cy = ((rect ? rect.height / 2 : 200) - view.y) / view.zoom
    try { const u = await api<Utterance>(`/api/admin/story-nodes/${nodeId}/utterances`, 'POST', { utterer, xpos: Math.round(cx), ypos: Math.round(cy) }); await reload(); setSelectedId(u.id) }
    catch (error) { setStatus(String((error as Error).message || error)) }
  }

  const onBgPointerDown = (event: React.PointerEvent) => {
    if (wiringFrom) { setWiringFrom(null); return }
    panRef.current = { startX: event.clientX, startY: event.clientY, origX: view.x, origY: view.y }; drag.current = 'pan'; setSelectedId(null)
  }
  const onCardPointerDown = (event: React.PointerEvent, u: Utterance) => {
    ;(event.target as HTMLElement).setPointerCapture?.(event.pointerId)
    drag.current = { id: u.id, startX: event.clientX, startY: event.clientY, origX: u.xpos, origY: u.ypos }; setSelectedId(u.id)
  }
  const onPointerMove = (event: React.PointerEvent) => {
    if (drag.current === 'pan' && panRef.current) { const p = panRef.current; setView(v => ({ ...v, x: p.origX + event.clientX - p.startX, y: p.origY + event.clientY - p.startY })); return }
    if (drag.current && drag.current !== 'pan') { const d = drag.current; setUtterances(list => list.map(u => u.id === d.id ? { ...u, xpos: d.origX + (event.clientX - d.startX) / view.zoom, ypos: d.origY + (event.clientY - d.startY) / view.zoom } : u)) }
  }
  const onPointerUp = async () => {
    const state = drag.current; drag.current = null; panRef.current = null
    if (state && state !== 'pan') { const u = utterances.find(x => x.id === state.id); if (u) await patch(u.id, { xpos: Math.round(u.xpos), ypos: Math.round(u.ypos) }, true) }
  }
  const onWheel = (event: React.WheelEvent) => {
    const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return
    const px = event.clientX - rect.left, py = event.clientY - rect.top, factor = event.deltaY < 0 ? 1.1 : 1 / 1.1
    setView(v => { const zoom = Math.min(2, Math.max(0.35, v.zoom * factor)); return { zoom, x: px - (px - v.x) * (zoom / v.zoom), y: py - (py - v.y) * (zoom / v.zoom) } })
  }

  // A card's children are what come after it: dragging its port to another card makes
  // that card a child. One child ⇒ solid (linear next line); two or more ⇒ dotted
  // (player options). A card can instead exit the node by wiring to a terminal sink.
  const link = (id: string, body: Record<string, unknown>, undoBody: Record<string, unknown>) => {
    pushUndo(() => api(`/api/admin/utterances/${id}`, 'PATCH', undoBody)); void patch(id, body)
  }
  const targetCard = (u: Utterance) => {
    if (!wiringFrom) { setSelectedId(u.id); return }
    const source = wiringFrom.id; setWiringFrom(null)
    if (source === u.id) return
    link(u.id, { parentUtteranceId: source }, { parentUtteranceId: u.parentUtteranceId })
  }
  const targetSink = (terminalId: string) => {
    const source = wiringFrom ? utterances.find(x => x.id === wiringFrom.id) : null; setWiringFrom(null)
    if (source) link(source.id, { terminalId }, { terminalId: source.terminalId })
  }

  const byId = new Map(utterances.map(u => [u.id, u]))
  const childCount = new Map<string, number>()
  for (const u of utterances) if (u.parentUtteranceId) childCount.set(u.parentUtteranceId, (childCount.get(u.parentUtteranceId) || 0) + 1)
  const npcName = (id: string | null) => npcs.find(n => n.id === id)?.name || 'NPC'
  const selected = utterances.find(u => u.id === selectedId) || null
  const cardH = (u: Utterance) => heights[u.id] ?? UH_DEFAULT
  const topPort = (u: Utterance) => ({ x: u.xpos + UW / 2, y: u.ypos })
  const bottomPort = (u: Utterance) => ({ x: u.xpos + UW / 2, y: u.ypos + cardH(u) })
  // Exit sinks dock in a row below the utterances so flow reads top-to-bottom.
  const sinkRowY = (utterances.length ? Math.max(...utterances.map(u => u.ypos + cardH(u))) : 120) + 56
  const sinkPos = (i: number) => ({ x: 40 + i * (SINK_W + 24), y: sinkRowY })
  const sinkInput = (i: number) => ({ x: sinkPos(i).x + SINK_W / 2, y: sinkRowY })
  const curve = (a: { x: number; y: number }, b: { x: number; y: number }) => { const dy = Math.max(30, Math.abs(b.y - a.y) / 2); return `M${a.x},${a.y} C${a.x},${a.y + dy} ${b.x},${b.y - dy} ${b.x},${b.y}` }

  return <div className="utterance-overlay">
    <div className="graph-toolbar">
      <button className="graph-back" onClick={onClose}>← Graph</button>
      <strong>{nodeLabel || 'Dialogue'} · utterances</strong>
      <span className="graph-add-label">Add:</span>
      <button className="graph-add" onClick={() => add('npc')}>NPC line</button>
      <button className="graph-add" onClick={() => add('player')}>Player choice</button>
      {wiringFrom && <span className="graph-wiring">Click the next card (2+ ⇒ options) or an exit ⇥ · click empty to cancel</span>}
      <span className="graph-zoom">{Math.round(view.zoom * 100)}%</span>
    </div>
    <div className="graph-main">
      <div ref={canvasRef} className={`graph-canvas${wiringFrom ? ' wiring' : ''}`} onPointerDown={onBgPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} onWheel={onWheel}>
        <div className="graph-world" style={{ transform: `translate(${view.x}px,${view.y}px) scale(${view.zoom})` }}>
          <svg className="graph-wires" width="6000" height="6000">
            {utterances.flatMap(u => {
              const wires: ReactElement[] = []
              const wire = (key: string, d: string, cls: string, onDelete: () => void) => {
                wires.push(<path key={key + 'hit'} className="wire-hit" d={d} onClick={event => { event.stopPropagation(); onDelete() }} />)
                wires.push(<path key={key} className={`graph-wire ${cls}`} d={d} />)
              }
              if (u.parentUtteranceId && byId.has(u.parentUtteranceId)) {
                const parent = byId.get(u.parentUtteranceId)!
                const cls = (childCount.get(parent.id) || 0) >= 2 ? 'option' : ''
                wire(u.id + 'p', curve(bottomPort(parent), topPort(u)), cls, () => link(u.id, { parentUtteranceId: null }, { parentUtteranceId: parent.id }))
              }
              if (u.terminalId) { const idx = terminals.findIndex(t => t.id === u.terminalId); if (idx >= 0) wire(u.id + 't', curve(bottomPort(u), sinkInput(idx)), 'exit', () => link(u.id, { terminalId: null }, { terminalId: u.terminalId })) }
              return wires
            })}
          </svg>

          {terminals.map((t, i) => { const p = sinkPos(i); return <div key={t.id} className="usink" style={{ left: p.x, top: p.y, width: SINK_W }} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); targetSink(t.id) }}>
            <div className="uinput" />
            <span className="usink-label">⇥ {t.label || t.terminalKey}</span>
          </div> })}

          {utterances.map(u => <div key={u.id} ref={el => { if (el) cardRefs.current.set(u.id, el); else cardRefs.current.delete(u.id) }}
            className={`ucard u-${u.utterer}${u.id === selectedId ? ' selected' : ''}`} style={{ left: u.xpos, top: u.ypos, width: UW }}
            onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); targetCard(u) }}>
            <div className="uinput" />
            <div className="ucard-head" onPointerDown={e => onCardPointerDown(e, u)}>
              <span className="ucard-badge">{u.utterer === 'npc' ? npcName(u.npcId) : 'PLAYER'}</span>
              {u.utterer === 'npc' && u.poseKey && <span className="ucard-pose">{u.poseKey}</span>}
            </div>
            <div className="ucard-text">{u.text || <em>(empty)</em>}</div>
            <button className={`uport flow${(childCount.get(u.id) || 0) > 0 || u.terminalId ? ' wired' : ''}${wiringFrom?.id === u.id ? ' active' : ''}`}
              title="Connect to the next line(s) or an exit ⇥ · 1 = linear, 2+ = options"
              onClick={e => { e.stopPropagation(); setWiringFrom(w => w?.id === u.id ? null : { id: u.id }) }} />
          </div>)}
        </div>
        {utterances.length === 0 && <div className="graph-empty">No utterances yet — add an NPC line or player choice.</div>}
        <div className="utterance-preview-dock" onPointerDown={event => event.stopPropagation()}>
          <DialoguePreview nodeId={nodeId} revision={revision} startId={selected ? (selected.utterer === 'player' ? selected.parentUtteranceId : selected.id) : null} />
        </div>
      </div>

      {selected && <aside className="graph-inspector">
        <div className="inspector-body">
          <div className="inspector-head"><span className={`gnode-type type-${selected.utterer === 'npc' ? 'dialogue' : 'level'}`}>{selected.utterer}</span></div>
          {selected.utterer === 'npc' && <>
            <label className="ins-field"><span>Speaker</span>
              <select value={selected.npcId || ''} onChange={e => patch(selected.id, { npcId: e.target.value || null, poseKey: null })}>
                <option value="">— choose NPC —</option>
                {npcs.map(n => <option key={n.id} value={n.id}>{n.name}</option>)}
              </select></label>
            <label className="ins-field"><span>Pose</span>
              <select value={selected.poseKey || ''} onChange={e => patch(selected.id, { poseKey: e.target.value || null })}>
                <option value="">— default / none —</option>
                {(npcs.find(n => n.id === selected.npcId)?.poses || []).map(p => <option key={p.poseKey} value={p.poseKey}>{p.poseKey}</option>)}
              </select></label>
          </>}
          <label className="ins-field"><span>{selected.utterer === 'npc' ? 'Line' : 'Choice text'}</span>
            <textarea className="ins-text" defaultValue={selected.text} onBlur={e => e.target.value !== selected.text && patch(selected.id, { text: e.target.value })} /></label>
          <div className="ins-links">
            <div>Next: {(childCount.get(selected.id) || 0) > 0 ? `${childCount.get(selected.id)} ${(childCount.get(selected.id) || 0) >= 2 ? 'options' : 'line'}` : selected.terminalId ? `⇥ ${terminals.find(t => t.id === selected.terminalId)?.label || 'exit'}` : '— none —'}
              {selected.terminalId && <button className="ins-unwire" onClick={() => link(selected.id, { terminalId: null }, { terminalId: selected.terminalId })}>⊘</button>}</div>
            {selected.parentUtteranceId && <div>Follows another utterance <button className="ins-unwire" onClick={() => link(selected.id, { parentUtteranceId: null }, { parentUtteranceId: selected.parentUtteranceId })}>⊘</button></div>}
          </div>
          <button className="ins-delete" onClick={async () => { if (!window.confirm('Delete utterance?')) return; try { await api(`/api/admin/utterances/${selected.id}`, 'DELETE'); setSelectedId(null); await reload() } catch (error) { setStatus(String((error as Error).message || error)) } }}>Delete utterance</button>
        </div>
      </aside>}
    </div>
  </div>
}
