/* ============================================================
   Albion Cockpit — simulation engine + App
   ============================================================ */
const { useState: useS, useEffect: useE, useRef: useR, useLayoutEffect } = React;

const BASE_YEAR_SEC = 120;          // 1 simulated year per 120s at 1x
const DAY_PER_SEC = 365 / BASE_YEAR_SEC;
const TOK_DUR = { work: 1700, sign: 1950, escalation: 1350 };

function fmtTokens(n) {
  return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
}

function App() {
  const D = window.AlbionData;
  const U = window.AlbionUI;
  const U2 = window.AlbionUI2;

  const [playing, setPlaying] = useS(false);
  const [speed, setSpeed] = useS(1);
  const [simDay, setSimDay] = useS(0);
  const [statuses, setStatuses] = useS({});
  const [pulses, setPulses] = useS(new Set());
  const [feed, setFeed] = useS([]);
  const [tokens, setTokens] = useS([]);
  const [producedDocs, setProduced] = useS([]);
  const [nodeDocs, setNodeDocs] = useS({});
  const [escArmed, setEscArmed] = useS(false);
  const [litConn, setLitConn] = useS({});
  const [openArtifact, setOpenArtifact] = useS(null);
  const [openAgent, setOpenAgent] = useS(null);
  const [selected, setSelected] = useS(null);
  const [injectOpen, setInjectOpen] = useS(false);

  // live data loaded from the server
  const [loaded, setLoaded] = useS(false);
  const [loadError, setLoadError] = useS(null);
  const [artifacts, setArtifacts] = useS({});
  const [injects, setInjects] = useS([]);
  const [org, setOrg] = useS(null);
  const [scorecards, setScorecards] = useS([]);
  const [agentConfigs, setAgentConfigs] = useS({});
  const [ledgerConfig, setLedgerConfig] = useS(null);
  const [raceModel, setRaceModel] = useS(null);
  const [raceOpen, setRaceOpen] = useS(false);
  const [bankMetrics, setBankMetrics] = useS(null);
  const [bankOpen, setBankOpen] = useS(false);
  const [scoreboardOpen, setScoreboardOpen] = useS(false);
  const [askOpen, setAskOpen] = useS(false);
  const [redTeam, setRedTeam] = useS([]);
  const [redTeamOpen, setRedTeamOpen] = useS(false);
  const [constitution, setConstitution] = useS(null);
  const [conOpen, setConOpen] = useS(false);
  const [shocks, setShocks] = useS([]);
  const [forkOpen, setForkOpen] = useS(false);
  const [guideOpen, setGuideOpen] = useS(false);
  const [health, setHealth] = useS(null);
  const [liveOn, setLiveOn] = useS(() => (typeof localStorage !== 'undefined' && localStorage.getItem('ab_live') === '1'));
  const [llmStats, setLlmStats] = useS(null);
  const eventsRef = useR([]);
  const runIdRef = useR(null);

  useE(() => {
    (async () => {
      try {
        const latest = await fetch('/api/run/latest').then(r => (r.ok ? r.json() : Promise.reject(new Error('no run'))));
        const snap = await fetch('/api/run/' + latest.id).then(r => r.json());
        runIdRef.current = latest.id;
        eventsRef.current = snap.presentationEvents || [];
        setArtifacts(snap.artifacts || {});
        const inj = await fetch('/api/injects').then(r => (r.ok ? r.json() : []));
        setInjects(inj);
        const orgLayout = await fetch('/api/org').then(r => (r.ok ? r.json() : null));
        setOrg(orgLayout);
        const sc = await fetch('/api/scorecards/' + latest.id).then(r => (r.ok ? r.json() : []));
        setScorecards(sc);
        const cfg = await fetch('/api/agents').then(r => (r.ok ? r.json() : []));
        setAgentConfigs(Object.fromEntries(cfg.map(c => [c.id, c])));
        const ld = await fetch('/api/ledger').then(r => (r.ok ? r.json() : null));
        setLedgerConfig(ld);
        const race = await fetch('/api/race').then(r => (r.ok ? r.json() : null));
        setRaceModel(race);
        const metrics = await fetch('/api/metrics').then(r => (r.ok ? r.json() : null));
        setBankMetrics(metrics);
        const rt = await fetch('/api/redteam').then(r => (r.ok ? r.json() : []));
        setRedTeam(rt);
        const con = await fetch('/api/constitution').then(r => (r.ok ? r.json() : null));
        setConstitution(con);
        const shk = await fetch('/api/shocks').then(r => (r.ok ? r.json() : []));
        setShocks(shk);
        const hl = await fetch('/api/health').then(r => (r.ok ? r.json() : null));
        setHealth(hl);
        setLoaded(true);
      } catch (e) {
        setLoadError(String((e && e.message) || e));
      }
    })();
  }, []);

  // poll live LLM call stats (a cheap counter read; no model cost)
  useE(() => {
    let alive = true;
    const tick = () => fetch('/api/llm-stats').then(r => (r.ok ? r.json() : null)).then(s => { if (alive) setLlmStats(s); }).catch(() => {});
    tick();
    const id = setInterval(tick, 4000);
    return () => { alive = false; clearInterval(id); };
  }, []);

  // refs mirroring state for the rAF loop
  const playingRef = useR(false), speedRef = useR(1), dayFloatRef = useR(0), simDayRef = useR(0);
  const tokensRef = useR([]), firedRef = useR(new Set()), pulseTimers = useR([]);
  const lastTsRef = useR(0), rafRef = useR(0), uidRef = useR(0);
  const tlFillRef = useR(null), tlHeadRef = useR(null), tokenLayerRef = useR(null);

  useE(() => { playingRef.current = playing; }, [playing]);
  useE(() => { speedRef.current = speed; }, [speed]);

  const uid = () => ++uidRef.current;

  /* ---------- positioning ---------- */
  function positionToken(t) {
    const path = document.getElementById(t.connector);
    const el = document.getElementById('tok-' + t.id);
    if (!path || !el) return;
    const L = path.getTotalLength();
    const p = Math.min(1, t.elapsed / t.dur);
    const pt = path.getPointAtLength(p * L);
    el.style.left = pt.x + 'px';
    el.style.top = pt.y + 'px';
  }
  useLayoutEffect(() => { tokensRef.current.forEach(positionToken); }, [tokens]);

  /* ---------- fire a scripted/inject event ---------- */
  function fireEvent(e, opts = {}) {
    const live = !!opts.live;
    const dayForStamp = opts.day != null ? opts.day : e.day;

    if (e.set) setStatuses(s => ({ ...s, ...e.set }));

    if (e.pulse) {
      setPulses(p => { const n = new Set(p); e.pulse.forEach(id => n.add(id)); return n; });
      const tm = setTimeout(() => {
        setPulses(p => { const n = new Set(p); e.pulse.forEach(id => n.delete(id)); return n; });
      }, 2400);
      pulseTimers.current.push(tm);
    }

    if (e.mark && e.artifact) {
      setProduced(d => d.includes(e.artifact) ? d : [...d, e.artifact]);
      setNodeDocs(m => ({ ...m, [e.mark]: e.artifact }));
    }

    // feed row
    const row = {
      uid: uid(),
      time: live ? (D.stamp(dayForStamp) + ' \u00b7 NOW') : D.stamp(e.day),
      from: e.from, to: e.to, title: e.title, detail: e.detail,
      feedType: e.feedType, artifact: e.artifact || null,
    };
    setFeed(f => [row, ...f]);

    // token
    if (e.token) {
      const t = {
        id: uid(), connector: e.token.c, tone: e.token.tone, label: e.token.label,
        reverse: !!e.token.reverse, elapsed: 0,
        dur: e.token.dur || TOK_DUR[e.token.tone] || 1700, live,
      };
      tokensRef.current = [...tokensRef.current, t];
      setTokens([...tokensRef.current]);
      setLitConn(l => ({ ...l, [t.connector]: true }));
      if (t.connector === 'audit-prod-esc') setEscArmed(true);
    }
  }

  /* ---------- rebuild state for scrubbing ---------- */
  function rebuildTo(target) {
    pulseTimers.current.forEach(clearTimeout); pulseTimers.current = [];
    tokensRef.current = []; setTokens([]);
    firedRef.current = new Set();
    let st = {}, docs = [], nmap = {}, rows = [];
    eventsRef.current.forEach(e => {
      if (e.day <= target) {
        firedRef.current.add(e.id);
        if (e.set) st = { ...st, ...e.set };
        if (e.mark && e.artifact) { if (!docs.includes(e.artifact)) docs.push(e.artifact); nmap[e.mark] = e.artifact; }
        rows.unshift({
          uid: uid(), time: D.stamp(e.day), from: e.from, to: e.to,
          title: e.title, detail: e.detail, feedType: e.feedType, artifact: e.artifact || null,
        });
      }
    });
    setStatuses(st); setProduced(docs); setNodeDocs(nmap); setFeed(rows);
    setPulses(new Set()); setEscArmed(false); setLitConn({});
    dayFloatRef.current = target; simDayRef.current = target; setSimDay(target);
  }

  /* ---------- main loop ---------- */
  useE(() => {
    function frame(ts) {
      const last = lastTsRef.current; lastTsRef.current = ts;
      const dt = last ? Math.min(0.05, (ts - last) / 1000) : 0;

      // advance tokens
      let completed = [];
      tokensRef.current.forEach(t => {
        if (t.live || playingRef.current) t.elapsed += dt * 1000;
        positionToken(t);
        if (t.elapsed >= t.dur) completed.push(t);
      });
      if (completed.length) {
        const ids = completed.map(t => t.id);
        tokensRef.current = tokensRef.current.filter(t => !ids.includes(t.id));
        setTokens([...tokensRef.current]);
        setLitConn(l => { const n = { ...l }; completed.forEach(t => delete n[t.connector]); return n; });
        if (completed.some(t => t.connector === 'audit-prod-esc')) setEscArmed(false);
      }

      // advance clock + fire events
      if (playingRef.current) {
        let d = dayFloatRef.current + dt * DAY_PER_SEC * speedRef.current;
        if (d >= 365) { d = 365; setPlaying(false); }
        dayFloatRef.current = d;
        eventsRef.current.forEach(e => { if (!firedRef.current.has(e.id) && e.day <= d) { firedRef.current.add(e.id); fireEvent(e); } });
        const idn = Math.min(365, Math.floor(d));
        if (idn !== simDayRef.current) { simDayRef.current = idn; setSimDay(idn); }
      }

      // playhead
      const ratio = Math.min(1, dayFloatRef.current / 364);
      if (tlFillRef.current) tlFillRef.current.style.width = (ratio * 100) + '%';
      if (tlHeadRef.current) tlHeadRef.current.style.left = (ratio * 100) + '%';

      rafRef.current = requestAnimationFrame(frame);
    }
    rafRef.current = requestAnimationFrame(frame);
    return () => cancelAnimationFrame(rafRef.current);
  }, []);

  /* ---------- controls ---------- */
  function togglePlay() {
    if (!playing && dayFloatRef.current >= 364) rebuildTo(0);
    setPlaying(p => !p);
    setInjectOpen(false);
  }
  function handleScrub(day) { setPlaying(false); rebuildTo(day); }
  function toggleLive() {
    if (!health || !health.configured) return;
    setLiveOn(v => { const nv = !v; try { localStorage.setItem('ab_live', nv ? '1' : '0'); } catch (e) {} return nv; });
  }
  const liveActive = Boolean(health && health.configured && liveOn);
  function handleNode(a) {
    setSelected(a.id);
    setOpenArtifact(null);
    setOpenAgent(a.id); // open the agent profile (config + their documents)
  }
  function pickInject(inj) {
    setInjectOpen(false);
    const day = simDayRef.current;
    const runId = runIdRef.current;
    if (!runId) return;
    if (inj.id === 'dear-ceo') {
      // genuinely-live inject: stream over SSE (real agent, scripted fallback)
      const wantLive = Boolean(health && health.configured && liveOn);
      const es = new EventSource('/api/run/' + runId + '/inject/' + inj.id + '/live?day=' + day + (wantLive ? '&live=1' : ''));
      es.onmessage = (m) => {
        const data = JSON.parse(m.data);
        if (data.done) { es.close(); return; }
        if (data._artifact) {
          setArtifacts(a => ({ ...a, [data._artifact.ref]: data._artifact }));
          const { _artifact, ...ev } = data;
          fireEvent(ev, { live: true, day });
        } else {
          fireEvent(data, { live: true, day });
        }
      };
      es.onerror = () => es.close();
    } else {
      fetch('/api/run/' + runId + '/inject/' + inj.id, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ day }),
      })
        .then(r => r.json())
        .then(events => events.forEach(ev => fireEvent(ev, { live: true, day })));
    }
  }

  // keyboard
  useE(() => {
    function onKey(ev) {
      if (ev.target.tagName === 'INPUT') return;
      if (ev.code === 'Space') { ev.preventDefault(); togglePlay(); }
      else if (ev.code === 'ArrowRight') handleScrub(Math.min(364, simDayRef.current + 12));
      else if (ev.code === 'ArrowLeft') handleScrub(Math.max(0, simDayRef.current - 12));
      else if (ev.code === 'Escape') { setOpenArtifact(null); setOpenAgent(null); setInjectOpen(false); setSelected(null); setAskOpen(false); setRedTeamOpen(false); setConOpen(false); setForkOpen(false); setGuideOpen(false); }
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [playing]);

  return (
    <div className="cockpit" onClick={() => injectOpen && setInjectOpen(false)}>
      <div onClick={e => e.stopPropagation()}>
        <U.TopBar
          playing={playing} speed={speed} day={simDay} events={loaded ? eventsRef.current : []}
          onPlay={togglePlay} onSpeed={setSpeed} onScrub={handleScrub}
          onInject={() => setInjectOpen(o => !o)} injectOpen={injectOpen}
          tlFillRef={tlFillRef} tlHeadRef={tlHeadRef}
        />
      </div>

      <nav className="cockpit-menu" onClick={e => e.stopPropagation()}>
        <span className="cm-label">DEMO</span>
        <button className="cm-item cm-guide" onClick={() => setGuideOpen(true)} title="Guide — what this is and how to drive it">Guide</button>
        {scorecards.length > 0 && (
          <button className="cm-item" onClick={() => setScoreboardOpen(true)} title="Chief of Agents — workforce performance">Agent Scores</button>
        )}
        {bankMetrics && (
          <button className="cm-item" onClick={() => setBankOpen(true)} title="State of the Bank — live vital signs">State of the Bank</button>
        )}
        {raceModel && (
          <button className="cm-item" onClick={() => setRaceOpen(true)} title="Race the Bank — agentic vs traditional">Race the Bank</button>
        )}
        {loaded && (
          <button className="cm-item" onClick={() => setAskOpen(true)} title="Ask the Bank Anything — routed to the relevant executive">Ask the Bank</button>
        )}
        {redTeam.length > 0 && (
          <button className="cm-item" onClick={() => setRedTeamOpen(true)} title="Red-Team Console — attack the bank's agents">Red-Team</button>
        )}
        {constitution && (
          <button className="cm-item" onClick={() => setConOpen(true)} title="The Constitution — the shared charter every agent inherits">Constitution</button>
        )}
        {shocks.length > 0 && (
          <button className="cm-item" onClick={() => setForkOpen(true)} title="Fork the Bank — branch the forward model under a shock">Fork the Bank</button>
        )}
        <span className="cm-spacer" />
        {llmStats && llmStats.calls > 0 && (
          <span className="cm-llm" title="Live LLM calls this session (server-cumulative)">
            ⚡ {llmStats.calls} {llmStats.calls === 1 ? 'call' : 'calls'} · {fmtTokens(llmStats.inputTokens + llmStats.outputTokens)} tok · {(llmStats.lastMs / 1000).toFixed(1)}s
          </span>
        )}
        {health && (
          <button
            className={'cm-live' + (liveActive ? ' on' : health.configured ? ' off' : ' none')}
            disabled={!health.configured}
            onClick={toggleLive}
            title={health.configured ? 'Toggle live LLM calls on/off' : 'No model configured — set keys in the environment'}
          >
            <span className="cm-live-dot" />
            {!health.configured
              ? 'Mock · no model'
              : (health.provider === 'azure' ? 'Azure' : 'Anthropic') + (liveActive ? ' · LIVE' : ' · live off')}
          </button>
        )}
      </nav>

      {injectOpen && <div onClick={e => e.stopPropagation()}><U2.InjectMenu injects={injects} onPick={pickInject} /></div>}

      {loadError && (
        <div style={{ position:'absolute', top:'80px', left:'50%', transform:'translateX(-50%)', zIndex:50,
          background:'var(--panel-2)', border:'1px solid var(--line-2)', borderRadius:'10px', padding:'14px 20px',
          fontFamily:'var(--mono)', fontSize:'12px', color:'var(--ink-2)', textAlign:'center' }}>
          No run found. Generate one: <b>npm run generate-run -- --mock</b>, then reload.
        </div>
      )}

      {scoreboardOpen && <U2.Scoreboard scorecards={scorecards} onClose={() => setScoreboardOpen(false)} />}
      {raceOpen && raceModel && <U2.Race model={raceModel} onClose={() => setRaceOpen(false)} />}
      {bankOpen && bankMetrics && <U2.BankBoard model={bankMetrics} day={simDay} onClose={() => setBankOpen(false)} />}
      {askOpen && (
        <U2.AskBank
          runId={runIdRef.current} day={simDay} artifacts={artifacts} live={liveActive}
          onOpenDoc={k => { setAskOpen(false); setOpenAgent(null); setOpenArtifact(k); }}
          onClose={() => setAskOpen(false)}
        />
      )}
      {redTeamOpen && (
        <U2.RedTeam scenarios={redTeam} runId={runIdRef.current} day={simDay} live={liveActive} onClose={() => setRedTeamOpen(false)} />
      )}
      {conOpen && constitution && (
        <U2.Constitution model={constitution} agentCount={Object.keys(agentConfigs).length} live={liveActive} onClose={() => setConOpen(false)} />
      )}
      {forkOpen && (
        <U2.Fork shocks={shocks} runId={runIdRef.current} day={simDay} agentConfigs={agentConfigs} live={liveActive} onClose={() => setForkOpen(false)} />
      )}
      {guideOpen && <U2.Guide onClose={() => setGuideOpen(false)} />}

      <div className="body">
        <div className="main-col">
          <U.OrgMap
            org={org} scorecards={scorecards}
            statuses={statuses} pulses={pulses} selected={selected} nodeDocs={nodeDocs}
            escArmed={escArmed} litConn={litConn} tokens={tokens}
            tokenLayerRef={tokenLayerRef} onNode={handleNode}
          />
          <div className={'scrim' + (openArtifact || openAgent ? ' show' : '')} onClick={() => { setOpenArtifact(null); setOpenAgent(null); }} />
          <U2.ArtifactDrawer
            open={!!openArtifact || !!openAgent} current={openArtifact} produced={producedDocs} artifacts={artifacts}
            runId={runIdRef.current}
            agent={openAgent ? agentConfigs[openAgent] : null}
            agentScore={openAgent ? scorecards.find(c => c.agentId === openAgent && c.quarter === 'FY') : null}
            onOpen={k => { setOpenAgent(null); setOpenArtifact(k); }}
            onClose={() => { setOpenArtifact(null); setOpenAgent(null); }}
          />
          <div className="kbd-hint">
            <span><kbd>Space</kbd> play / pause</span>
            <span><kbd>←</kbd> <kbd>→</kbd> scrub</span>
            <span><kbd>Esc</kbd> close</span>
          </div>
        </div>
        <div className="feed-col">
          <U2.Feed rows={feed} onOpen={k => setOpenArtifact(k)} />
        </div>
      </div>
      <U2.Ledger producedDocs={producedDocs} artifacts={artifacts} config={ledgerConfig} />
    </div>
  );
}

/* ---------- scale-to-fit ---------- */
function fitStage() {
  const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
  const el = document.querySelector('.scaler');
  if (el) el.style.transform = `scale(${s})`;
}
window.addEventListener('resize', fitStage);

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
setTimeout(fitStage, 0);
