import { useCallback, useEffect, useRef, useState } from 'react'
import { UtteranceCanvas } from './utteranceCanvas'
import { CUTSCENE_COMPONENT_KEYS, DialoguePreview } from './narrative'

type StoryNodeType = 'cutscene' | 'dialogue' | 'level' | 'det_gate' | 'llm_gate' | 'merit' | 'phone'
type Terminal = { id: string; terminalKey: string; label: string; toNodeId: string | null; sortOrder: number; npcId: string | null }
type StoryNode = { id: string; nodeType: StoryNodeType; label: string; hasUtterances: boolean; xpos: number; ypos: number; levelTemplateVersionId: string | null; componentKey: string | null; musicAssetId: string | null; musicVolume: number; awardsFlag: string | null; terminals: Terminal[] }
type AudioAsset = { id: string; originalName: string; mimeType: string }
type Npc = { id: string; name: string; phoneNumber: string | null }
type Graph = { mysteryId: string; entryNodeId: string | null; nodes: StoryNode[] }
type LevelTemplate = { versionId: string; slug: string; name: string; version: number }

// Vertical layout: input on top, output terminals along the bottom; flow runs downward.
const NODE_W = 200, NODE_H = 88
const TYPES: { type: StoryNodeType; label: string }[] = [
  { type: 'cutscene', label: 'Cutscene' }, { type: 'dialogue', label: 'Dialogue' }, { type: 'level', label: 'Level' },
  { type: 'det_gate', label: 'Det gate' }, { type: 'llm_gate', label: 'LLM gate' },
  { type: 'phone', label: 'Phone' }, { type: 'merit', label: 'Merit' },
]
const outPort = (node: StoryNode, index: number) => ({ x: node.xpos + NODE_W * (index + 0.5) / Math.max(1, node.terminals.length), y: node.ypos + NODE_H })
const inPort = (node: StoryNode) => ({ x: node.xpos + NODE_W / 2, y: node.ypos })

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 MysteryGraphEditor({ mysteryId, title, onClose, setStatus }: { mysteryId: string; title: string; onClose: () => void; setStatus: (message: string) => void }) {
  const [graph, setGraph] = useState<Graph | null>(null)
  const [templates, setTemplates] = useState<LevelTemplate[]>([])
  const [audioAssets, setAudioAssets] = useState<AudioAsset[]>([])
  const [npcs, setNpcs] = useState<Npc[]>([])
  const [view, setView] = useState({ x: 60, y: 60, zoom: 1 })
  const [selectedId, setSelectedId] = useState<string | null>(null)
  const [wiringFrom, setWiringFrom] = useState<string | null>(null)
  const [utterancesNode, setUtterancesNode] = useState<StoryNode | null>(null)
  const canvasRef = useRef<HTMLDivElement>(null)
  const drag = useRef<{ kind: 'pan' | 'node'; id?: string; startX: number; startY: number; origX: number; origY: number } | null>(null)

  const reload = useCallback(async () => {
    try { setGraph(await api<Graph>(`/api/admin/mysteries/${mysteryId}/graph`, 'GET')) }
    catch (error) { setStatus(String((error as Error).message || error)) }
  }, [mysteryId, setStatus])
  useEffect(() => {
    void reload()
    api<LevelTemplate[]>('/api/admin/level-templates', 'GET').then(setTemplates).catch(() => {})
    api<AudioAsset[]>('/api/admin/assets', 'GET').then(list => setAudioAssets(list.filter(a => a.mimeType.startsWith('audio/')))).catch(() => {})
    api<Npc[]>('/api/admin/npcs', 'GET').then(setNpcs).catch(() => {})
  }, [reload])

  const centerInBoard = () => {
    const rect = canvasRef.current?.getBoundingClientRect()
    const cx = rect ? rect.width / 2 : 300, cy = rect ? rect.height / 2 : 200
    return { x: (cx - view.x) / view.zoom, y: (cy - view.y) / view.zoom }
  }

  const addNode = async (nodeType: StoryNodeType) => {
    const at = centerInBoard()
    try { const node = await api<StoryNode>(`/api/admin/mysteries/${mysteryId}/nodes`, 'POST', { nodeType, xpos: Math.round(at.x), ypos: Math.round(at.y) }); await reload(); setSelectedId(node.id) }
    catch (error) { setStatus(String((error as Error).message || error)) }
  }
  const patchNode = async (id: string, body: Record<string, unknown>) => { try { await api(`/api/admin/story-nodes/${id}`, 'PATCH', body); await reload() } catch (error) { setStatus(String((error as Error).message || error)) } }
  const wire = async (terminalId: string, toNodeId: string | null) => { try { await api(`/api/admin/story-terminals/${terminalId}`, 'PATCH', { toNodeId }); await reload() } catch (error) { setStatus(String((error as Error).message || error)) } }

  // Pointer: pan on background, drag on node header.
  const onPointerDown = (event: React.PointerEvent) => {
    if (wiringFrom) { setWiringFrom(null); return }
    drag.current = { kind: 'pan', startX: event.clientX, startY: event.clientY, origX: view.x, origY: view.y }
    setSelectedId(null)
  }
  const onNodePointerDown = (event: React.PointerEvent, node: StoryNode) => {
    event.stopPropagation()
    ;(event.target as HTMLElement).setPointerCapture?.(event.pointerId)
    drag.current = { kind: 'node', id: node.id, startX: event.clientX, startY: event.clientY, origX: node.xpos, origY: node.ypos }
    setSelectedId(node.id)
  }
  const onPointerMove = (event: React.PointerEvent) => {
    const state = drag.current
    if (!state) return
    const dx = event.clientX - state.startX, dy = event.clientY - state.startY
    if (state.kind === 'pan') setView(v => ({ ...v, x: state.origX + dx, y: state.origY + dy }))
    else setGraph(g => g && { ...g, nodes: g.nodes.map(n => n.id === state.id ? { ...n, xpos: state.origX + dx / view.zoom, ypos: state.origY + dy / view.zoom } : n) })
  }
  const onPointerUp = async () => {
    const state = drag.current; drag.current = null
    if (state?.kind === 'node' && state.id) {
      const node = graph?.nodes.find(n => n.id === state.id)
      if (node) { try { await api(`/api/admin/story-nodes/${state.id}`, 'PATCH', { xpos: Math.round(node.xpos), ypos: Math.round(node.ypos) }) } catch { /* position persists next reload */ } }
    }
  }
  const onWheel = (event: React.WheelEvent) => {
    const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return
    const px = event.clientX - rect.left, py = event.clientY - rect.top
    const 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) } })
  }

  const onNodeClick = (event: React.MouseEvent, node: StoryNode) => {
    event.stopPropagation()
    if (wiringFrom) { void wire(wiringFrom, node.id); setWiringFrom(null); return }
    setSelectedId(node.id)
  }

  if (!graph) return <div className="graph-loading">Loading graph…</div>
  const selected = graph.nodes.find(n => n.id === selectedId) || null
  const nodeById = new Map(graph.nodes.map(n => [n.id, n]))

  return <div className="graph-editor">
    {utterancesNode && <UtteranceCanvas nodeId={utterancesNode.id} nodeLabel={utterancesNode.label} terminals={utterancesNode.terminals}
      onClose={() => { setUtterancesNode(null); void reload() }} setStatus={setStatus} />}
    <div className="graph-toolbar">
      <button className="graph-back" onClick={onClose}>← Mysteries</button>
      <strong>{title}</strong>
      <span className="graph-add-label">Add:</span>
      {TYPES.map(t => <button key={t.type} className="graph-add" onClick={() => addNode(t.type)}>{t.label}</button>)}
      {wiringFrom && <span className="graph-wiring">Click a target node to wire · 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={onPointerDown} 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">
            {graph.nodes.flatMap(node => node.terminals.filter(t => t.toNodeId && nodeById.has(t.toNodeId)).map(t => {
              const target = nodeById.get(t.toNodeId!)!
              const from = outPort(node, node.terminals.indexOf(t)), to = inPort(target)
              const dy = Math.max(40, Math.abs(to.y - from.y) / 2)
              const d = `M${from.x},${from.y} C${from.x},${from.y + dy} ${to.x},${to.y - dy} ${to.x},${to.y}`
              return [
                <path key={t.id + 'hit'} className="wire-hit" d={d} onClick={event => { event.stopPropagation(); void wire(t.id, null) }} />,
                <path key={t.id} className="graph-wire" d={d} />,
              ]
            }))}
          </svg>
          {graph.nodes.map(node => <div key={node.id} className={`gnode type-${node.nodeType}${node.id === selectedId ? ' selected' : ''}${graph.entryNodeId === node.id ? ' entry' : ''}`}
            style={{ left: node.xpos, top: node.ypos, width: NODE_W, height: NODE_H }} onPointerDown={event => event.stopPropagation()} onClick={event => onNodeClick(event, node)}
            onDoubleClick={event => { event.stopPropagation(); if (node.nodeType === 'dialogue' || node.hasUtterances) setUtterancesNode(node) }}>
            <div className="ginput" />
            <div className="gnode-head" onPointerDown={event => onNodePointerDown(event, node)}>
              <span className="gnode-type">{node.nodeType}</span>
              <span className="gnode-label">{node.label}</span>
              {graph.entryNodeId === node.id && <span className="gnode-entry">▶</span>}
            </div>
            <div className="gnode-sub">{nodeSummary(node, templates)}</div>
            <div className="gnode-outs">
              {node.terminals.map(t => <div key={t.id} className="gout">
                <span className="gout-label">{t.label || t.terminalKey}</span>
                <button className={`gport${t.toNodeId ? ' wired' : ''}${wiringFrom === t.id ? ' active' : ''}`} title="Click to start a wire"
                  onClick={event => { event.stopPropagation(); setWiringFrom(from => from === t.id ? null : t.id) }} />
              </div>)}
            </div>
          </div>)}
          {selected && (selected.nodeType === 'dialogue' || selected.hasUtterances) &&
            <div className="graph-node-preview" style={{ left: selected.xpos + NODE_W + 28, top: selected.ypos }} onClick={event => event.stopPropagation()}>
              <DialoguePreview nodeId={selected.id} />
            </div>}
        </div>
        {graph.nodes.length === 0 && <div className="graph-empty">Empty graph — add a node to begin.</div>}
      </div>

      {selected && <aside className="graph-inspector">
        <NodeInspector key={selected.id} node={selected} graph={graph} templates={templates} audioAssets={audioAssets} npcs={npcs}
          onEditUtterances={() => setUtterancesNode(selected)}
          onPatch={body => patchNode(selected.id, body)}
          onSetEntry={async () => { try { await api(`/api/admin/mysteries/${mysteryId}/entry`, 'PUT', { nodeId: selected.id }); await reload() } catch (error) { setStatus(String((error as Error).message || error)) } }}
          onDelete={async () => { if (!window.confirm('Delete this node?')) return; try { await api(`/api/admin/story-nodes/${selected.id}`, 'DELETE'); setSelectedId(null); await reload() } catch (error) { setStatus(String((error as Error).message || error)) } }}
          onAddTerminal={async () => { const key = window.prompt('Terminal key (e.g. proceed)'); if (!key) return; try { await api(`/api/admin/story-nodes/${selected.id}/terminals`, 'POST', { terminalKey: key, label: key }); await reload() } catch (error) { setStatus(String((error as Error).message || error)) } }}
          onTerminalPatch={async (id, body) => { try { await api(`/api/admin/story-terminals/${id}`, 'PATCH', body); await reload() } catch (error) { setStatus(String((error as Error).message || error)) } }}
          onTerminalDelete={async id => { try { await api(`/api/admin/story-terminals/${id}`, 'DELETE'); await reload() } catch (error) { setStatus(String((error as Error).message || error)) } }} />
      </aside>}
    </div>
  </div>
}

function nodeSummary(node: StoryNode, templates: LevelTemplate[]) {
  if (node.nodeType === 'level') return templates.find(t => t.versionId === node.levelTemplateVersionId)?.name || '⚠ no level chosen'
  if (node.nodeType === 'merit') return node.awardsFlag ? `🏅 ${node.awardsFlag}` : '⚠ no achievement'
  if (node.nodeType === 'phone') return `${node.terminals.length} contact${node.terminals.length === 1 ? '' : 's'}`
  if (node.nodeType === 'cutscene' || node.nodeType === 'det_gate' || node.nodeType === 'llm_gate') return node.componentKey || '⚠ no component'
  if (node.nodeType === 'dialogue') return node.hasUtterances ? 'utterances' : 'no utterances'
  return ''
}

function NodeInspector({ node, graph, templates, audioAssets, npcs, onPatch, onSetEntry, onDelete, onAddTerminal, onTerminalPatch, onTerminalDelete, onEditUtterances }: {
  node: StoryNode; graph: Graph; templates: LevelTemplate[]; audioAssets: AudioAsset[]; npcs: Npc[]
  onPatch: (body: Record<string, unknown>) => void; onSetEntry: () => void; onDelete: () => void
  onAddTerminal: () => void; onTerminalPatch: (id: string, body: Record<string, unknown>) => void; onTerminalDelete: (id: string) => void; onEditUtterances: () => void
}) {
  const [label, setLabel] = useState(node.label)
  const [componentKey, setComponentKey] = useState(node.componentKey || '')
  const [awardsFlag, setAwardsFlag] = useState(node.awardsFlag || '')
  const [volume, setVolume] = useState(node.musicVolume)
  const nodeName = (id: string | null) => id ? (graph.nodes.find(n => n.id === id)?.label || '—') : '— unwired —'
  const usesComponent = node.nodeType === 'cutscene' || node.nodeType === 'det_gate' || node.nodeType === 'llm_gate' || node.nodeType === 'merit'
  return <div className="inspector-body">
    <div className="inspector-head"><span className="gnode-type">{node.nodeType}</span>{graph.entryNodeId !== node.id && <button className="ins-entry" onClick={onSetEntry}>Set entrypoint</button>}</div>
    <label className="ins-field"><span>Label</span><input value={label} onChange={e => setLabel(e.target.value)} onBlur={() => label !== node.label && onPatch({ label })} /></label>
    {node.nodeType === 'level' && <label className="ins-field"><span>Level template</span>
      <select value={node.levelTemplateVersionId || ''} onChange={e => onPatch({ levelTemplateVersionId: e.target.value || null })}>
        <option value="">— choose —</option>
        {templates.map(t => <option key={t.versionId} value={t.versionId}>{t.name} (v{t.version})</option>)}
      </select></label>}
    {usesComponent && <label className="ins-field"><span>Component key{node.nodeType === 'cutscene' && componentKey && !CUTSCENE_COMPONENT_KEYS.includes(componentKey) && <b className="ins-warn"> · not registered</b>}</span>
      <input list={node.nodeType === 'cutscene' ? 'cutscene-components' : undefined} value={componentKey} placeholder={node.nodeType === 'cutscene' ? 'glass-harbour-diversion' : 'det_gate_lvl_1'} onChange={e => setComponentKey(e.target.value)} onBlur={() => componentKey !== (node.componentKey || '') && onPatch({ componentKey })} />
      {node.nodeType === 'cutscene' && <datalist id="cutscene-components">{CUTSCENE_COMPONENT_KEYS.map(k => <option key={k} value={k} />)}</datalist>}
    </label>}
    {node.nodeType === 'merit' && <label className="ins-field"><span>Awards achievement{node.awardsFlag ? '' : <b className="ins-warn"> · required</b>}</span>
      <input value={awardsFlag} placeholder="barricelli_luggage" onChange={e => setAwardsFlag(e.target.value)} onBlur={() => awardsFlag !== (node.awardsFlag || '') && onPatch({ awardsFlag: awardsFlag || null })} /></label>}
    {node.nodeType === 'phone' && <p className="ins-hint">Add a terminal per contact, pick the NPC you reach by dialing their number, then wire it to the dialogue that answers.</p>}
    {(node.nodeType === 'dialogue' || node.nodeType === 'cutscene') && <label className="ins-check"><input type="checkbox" checked={node.hasUtterances} onChange={e => onPatch({ hasUtterances: e.target.checked })} /> Has utterances</label>}
    {(node.nodeType === 'dialogue' || node.hasUtterances) && <button className="ins-utterances" onClick={onEditUtterances}>Edit utterances →</button>}
    <label className="ins-field"><span>Scene music</span>
      <select value={node.musicAssetId || ''} onChange={e => onPatch({ musicAssetId: e.target.value || null })}>
        <option value="">— none / inherit —</option>
        {audioAssets.map(asset => <option key={asset.id} value={asset.id}>{asset.originalName}</option>)}
      </select>
    </label>
    {node.musicAssetId && <label className="ins-field"><span>Volume {volume}%</span>
      <input type="range" min={0} max={100} value={volume} onChange={e => setVolume(Number(e.target.value))}
        onPointerUp={() => volume !== node.musicVolume && onPatch({ musicVolume: volume })} onBlur={() => volume !== node.musicVolume && onPatch({ musicVolume: volume })} />
    </label>}

    <div className="ins-terminals-head"><span>Output terminals</span><button onClick={onAddTerminal}>+ Add</button></div>
    {node.terminals.map(t => <div key={t.id} className="ins-terminal">
      <input defaultValue={t.label} onBlur={e => e.target.value !== t.label && onTerminalPatch(t.id, { label: e.target.value })} />
      {node.nodeType === 'phone' && <select className="ins-terminal-npc" value={t.npcId || ''} onChange={e => onTerminalPatch(t.id, { npcId: e.target.value || null })}>
        <option value="">— NPC —</option>
        {npcs.map(n => <option key={n.id} value={n.id}>{n.name}{n.phoneNumber ? ` · ${n.phoneNumber}` : ' · (no number)'}</option>)}
      </select>}
      <span className="ins-terminal-to">→ {nodeName(t.toNodeId)}</span>
      {t.toNodeId && <button className="ins-unwire" title="Unwire" onClick={() => onTerminalPatch(t.id, { toNodeId: null })}>⊘</button>}
      <button className="ins-term-del" title="Delete terminal" onClick={() => onTerminalDelete(t.id)}>×</button>
    </div>)}

    <button className="ins-delete" onClick={onDelete}>Delete node</button>
  </div>
}
