/* NIGHTSHIFT — desktop React HUD. Game logic lives in game-loop.js. */

const NS_ROSTER = window.NSData.ROSTER;
const NS_SKILL_ABBR = window.NSData.SKILL_ABBR;
const NSG = window.NightshiftGame;
const { fmtClock, fmtMoney, bestSkills, useGameLoop } = NSG;

// success-% → severity class for color-grading the odds readouts
function oddsClass(p) {
  if (p >= 0.85) return 'odds-hi';
  if (p >= 0.60) return 'odds-mid';
  if (p >= 0.35) return 'odds-lo';
  return 'odds-crit';
}

// ─── Top bar ─────────────────────────────────────────────────────────
function TopBar({ tick, score, heat, callsOpen }) {
  return (
    <div id="topbar" className="panel">
      <div className="topbar-left">
        <div className="stat">
          <div className="label">Shift Clock</div>
          <div className="value amber">{fmtClock(tick)}</div>
        </div>
        <div className="stat">
          <div className="label">Open Cases</div>
          <div className="value">{callsOpen}</div>
        </div>
      </div>
      <div className="topbar-center">
        <div className="brand">Night<span className="amp">·</span>Shift</div>
        <div className="brand-sub">PRIVATE INVESTIGATIONS · CH. 7</div>
      </div>
      <div className="topbar-right">
        <div className="stat">
          <div className="label">Closed</div>
          <div className="value cyan">{score.closed}</div>
        </div>
        <div className="stat">
          <div className="label">Earnings</div>
          <div className="value amber">{fmtMoney(score.cash)}</div>
        </div>
        <div className="stat" style={{ minWidth: 130 }}>
          <div className="label">City Heat</div>
          <div className="meter">
            <div className="meter-fill" style={{ width: `${Math.min(100, heat)}%` }} />
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Calls panel ─────────────────────────────────────────────────────
function CallCard({ call, selected, onSelect, opAssigned, odds }) {
  const ratio = call.timer / call.timerMax;
  const classes = ['call-card'];
  if (selected) classes.push('selected');
  if (call.urgent) classes.push('urgent');
  if (call.status !== 'open') classes.push('assigned');

  const phaseLabel = {
    open: 'INCOMING',
    'en-route': 'EN ROUTE',
    'on-scene': 'ON SCENE',
  }[call.status] || call.status.toUpperCase();

  const showOdds = call.status === 'open' && odds && odds.p > 0;

  return (
    <div className={classes.join(' ')} onClick={() => call.status === 'open' && onSelect(call.id)}>
      <div className="call-row1">
        <span className="call-id">{call.code} · {call.id.slice(2)}</span>
        {showOdds ? (
          <span className={'odds-chip ' + oddsClass(odds.p)}>{Math.round(odds.p * 100)}%</span>
        ) : (
          <span className="call-id" style={{ color: call.urgent ? 'var(--red)' : 'var(--ink-mute)' }}>
            {phaseLabel}
          </span>
        )}
      </div>
      <div className="call-type">{call.type}</div>
      <div className="call-loc">{call.location.toUpperCase()}</div>
      <div className="call-tags">
        {call.teamSize === 2 && <span className="tag two">2-OP</span>}
        {call.req.map(r => (
          <span key={r} className="tag req">{NS_SKILL_ABBR[r]} REQ</span>
        ))}
        {call.bonus.map(b => (
          <span key={b} className="tag">{NS_SKILL_ABBR[b]} +</span>
        ))}
        {opAssigned && <span className="tag met">UNIT {opAssigned}</span>}
      </div>
      <div className="timer-bar">
        <div className="timer-fill" style={{ width: `${ratio * 100}%` }} />
      </div>
    </div>
  );
}

function CallsPanel({ calls, selectedCallId, onSelect, bestOddsFor }) {
  const open = calls.filter(c => c.status !== 'resolved' && c.status !== 'failed');
  return (
    <div id="calls" className="panel">
      <div className="panel-head">
        <span><span className="dot" /> Incoming · Ch. 7</span>
        <span>{open.length} ACTIVE</span>
      </div>
      <div className="panel-body">
        {open.length === 0 && (
          <div className="empty-state">— quiet on the wire —</div>
        )}
        {open.map(c => {
          const assignedNames = (c.assignedOpIds || [])
            .map(id => { const o = NS_ROSTER.find(x => x.id === id); return o ? o.codename : id; })
            .join(' & ');
          return (
            <CallCard
              key={c.id}
              call={c}
              selected={selectedCallId === c.id}
              onSelect={onSelect}
              opAssigned={assignedNames || null}
              odds={c.status === 'open' ? bestOddsFor(c) : null}
            />
          );
        })}
      </div>
    </div>
  );
}

// ─── Operatives panel ───────────────────────────────────────────────
function OpCard({ op, opState, odds, picked, busy, onAssign }) {
  const targetable = odds != null && !busy;
  const classes = ['op-card'];
  if (picked) classes.push('picked');
  else if (targetable) classes.push('compatible');
  if (busy) classes.push('busy');
  if (opState.status === 'resting') classes.push('resting');
  if (opState.status === 'down') classes.push('down');

  const statusLabel = {
    available: 'AVAILABLE · HQ',
    enroute: `EN ROUTE · ETA ${opState.eta}s`,
    onscene: `ON SCENE · ${opState.workLeft}s`,
    returning: `RETURNING · ${opState.eta}s`,
    resting: 'RESTING',
  }[opState.status];

  return (
    <div className={classes.join(' ')} onClick={() => !busy && onAssign(op.id)}>
      <div className="op-avatar" style={{ borderColor: '#' + op.color.toString(16).padStart(6, '0') }}>
        {op.codename[0]}
      </div>
      <div className="op-meta">
        <div className="op-name">{op.codename}</div>
        <div className="op-role">{op.role}</div>
      </div>
      <div className="op-skills">
        {bestSkills(op).map(({ k, v }) => (
          <span key={k} className={'skill-pip' + (v >= 2 ? ' hi' : '')}>{NS_SKILL_ABBR[k]} · {v}</span>
        ))}
      </div>
      <div className="op-status">
        <span>{statusLabel}</span>
        {picked ? (
          <span className="picked-tag">PICKED ✓</span>
        ) : targetable ? (
          <span className={'odds-chip ' + oddsClass(odds)}>{Math.round(odds * 100)}% ▸</span>
        ) : null}
      </div>
    </div>
  );
}

function OpsPanel({ opsDisplay, selectedCall, onAssign, oddsFor, teamOddsFor, pendingOps }) {
  const teamSize = selectedCall ? (selectedCall.teamSize || 1) : 1;
  return (
    <div id="operatives" className="panel">
      <div className="panel-head">
        <span><span className="dot" /> Roster · 6 ON CALL</span>
        <span>NIGHTSHIFT</span>
      </div>
      <div className="panel-body">
        {selectedCall && teamSize === 2 && (
          <div className="ops-hint">CO-DISPATCH · SELECT {teamSize} UNITS · {pendingOps.length}/{teamSize}</div>
        )}
        {NS_ROSTER.map(op => {
          const opState = opsDisplay[op.id];
          const busy = opState.status !== 'available';
          const picked = pendingOps.includes(op.id);
          let odds = null;
          if (selectedCall && !busy && !picked) {
            odds = teamSize === 1
              ? oddsFor(selectedCall, op)
              : teamOddsFor(selectedCall, [...pendingOps, op.id]);
          }
          return (
            <OpCard
              key={op.id}
              op={op}
              opState={opState}
              odds={odds}
              picked={picked}
              busy={busy}
              onAssign={onAssign}
            />
          );
        })}
      </div>
    </div>
  );
}

// ─── Radio log ───────────────────────────────────────────────────────
function RadioPanel({ log }) {
  return (
    <div id="radio" className="panel">
      <div className="radio-log">
        {log.slice(-30).reverse().map((l) => (
          <div key={l.k} className="log-line">
            <span className="log-time">{l.time}</span>
            <span className={'log-tag ' + l.tag}>{l.tag.toUpperCase()}</span>
            <span className="log-body">{l.body}</span>
          </div>
        ))}
      </div>
      <div className="radio-mini">
        <div className="radio-eq" aria-hidden>
          {[...Array(14)].map((_, i) => (
            <span key={i} style={{ animationDelay: `${i * 0.08}s` }} />
          ))}
        </div>
        <div className="radio-meta">
          <span>RADIO · 154.235 MHz</span>
          <span>SIGNAL 98%</span>
        </div>
        <div className="radio-meta">
          <span>WEATHER</span>
          <span>RAIN · 51°F</span>
        </div>
      </div>
    </div>
  );
}

// ─── Intro overlay ──────────────────────────────────────────────────
function Intro({ onStart, onContinue, hasSave, bestScore, sceneFailed }) {
  return (
    <div id="intro">
      <a className="intro-back" href="index.html">◂ ALL DESIGN DOCS</a>
      <div className="sub">CHAPTER ONE</div>
      <h1>Night<span className="accent">·</span>Shift</h1>
      <div className="sub">DISPATCH PROTOTYPE · 06:00 OF DAYLIGHT TO GO</div>
      <div className="intro-deck">
        <div><b>SHIFT</b> 19:00 → 03:00</div>
        <div><b>WIRE</b> CH. 7 PRIVATE</div>
        <div><b>RUN</b> ≈ 6 MIN</div>
        <div><b>OPS</b> 6 ON CALL</div>
      </div>
      {hasSave ? (
        <React.Fragment>
          <button className="start-btn" onClick={onContinue}>CONTINUE SHIFT ▸</button>
          <button className="start-btn ghost" onClick={onStart}>NEW SHIFT</button>
        </React.Fragment>
      ) : (
        <button className="start-btn" onClick={onStart}>BEGIN SHIFT</button>
      )}
      {bestScore > 0 && (
        <div className="intro-best">BEST TAKE · <span>{fmtMoney(bestScore)}</span></div>
      )}
      {sceneFailed && (
        <div className="intro-warn">3D CITY UNAVAILABLE (WEBGL) · DISPATCH STILL RUNS</div>
      )}
      <div style={{ marginTop: 32, fontSize: 10, letterSpacing: '0.18em', color: 'var(--ink-mute)', maxWidth: 540, textAlign: 'center', lineHeight: 1.7 }}>
        CLICK A CALL ON THE LEFT, THEN AN OPERATIVE ON THE RIGHT TO DISPATCH.<br />
        MATCH THE SKILL TAGS. KEEP CITY HEAT BELOW THE LINE.
      </div>
    </div>
  );
}

// ─── End of shift ───────────────────────────────────────────────────
function EndShift({ score, onRestart, show }) {
  if (!show) return null;
  return (
    <div id="endshift" className={show ? 'show' : ''}>
      <div className="endshift-card">
        <div className="sub">03:00 · SHIFT COMPLETE</div>
        <h2>Sun's up.</h2>
        <div className="endshift-stats">
          <div className="stat">
            <div className="label">Closed</div>
            <div className="value" style={{ color: 'var(--green)' }}>{score.closed}</div>
          </div>
          <div className="stat">
            <div className="label">Failed</div>
            <div className="value" style={{ color: 'var(--red)' }}>{score.failed}</div>
          </div>
          <div className="stat">
            <div className="label">Missed</div>
            <div className="value" style={{ color: 'var(--ink-dim)' }}>{score.missed || 0}</div>
          </div>
          <div className="stat">
            <div className="label">Take</div>
            <div className="value" style={{ color: 'var(--amber)' }}>{fmtMoney(score.cash)}</div>
          </div>
        </div>
        <button className="endshift-btn" onClick={onRestart}>WORK ANOTHER NIGHT ▸</button>
      </div>
    </div>
  );
}

// ─── Map banter bubbles ─────────────────────────────────────────────
function BanterBubbles({ banter, tick }) {
  if (!window.NightshiftScene || !banter || !banter.length) return null;
  return (
    <React.Fragment>
      {banter.map(b => {
        const pos = NightshiftScene.gridToScreen(b.cell.x, b.cell.z, 9);
        return (
          <div key={b.id} className="banter-bubble" style={{ left: `${pos.x}px`, top: `${pos.y}px` }}>
            {b.text}
          </div>
        );
      })}
    </React.Fragment>
  );
}

// ─── Mid-job event overlay ──────────────────────────────────────────
function EventOverlay({ event, tick, onResolve }) {
  if (!event) return null;
  const timeLeft = Math.max(0, event.deadline - tick);
  const frac = Math.max(0, Math.min(1, timeLeft / event.timer));
  return (
    <div id="event-overlay">
      <div className="event-card">
        <div className="event-head">
          <span className="event-code">{event.code} · {event.type}</span>
          <span className="event-loc">{event.location.toUpperCase()}</span>
        </div>
        <div className="event-prompt">{event.prompt}</div>
        <div className="event-timer"><div className="event-timer-fill" style={{ width: `${frac * 100}%` }} /></div>
        <div className="event-options">
          {event.options.map((o, i) => (
            <button key={i} className="event-opt" onClick={() => onResolve(i)}>
              <span className="event-opt-label">
                {o.label}
                {o.minigame && <span className="mg-badge">▸ HACK</span>}
              </span>
              {o.safe
                ? <span className="event-opt-tag">SAFE</span>
                : <span className={'odds-chip ' + oddsClass(o.chance)}>{NS_SKILL_ABBR[o.skill]} · {Math.round(o.chance * 100)}%</span>}
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

// ─── Minigame: quick-hack (Phase C1) ────────────────────────────────
// A cursor sweeps a track; lock it inside the live band to beat the hack.
// Beat → the event option's win effect is guaranteed; skip or miss → the
// loop falls back to a plain skill roll. Difficulty (band width + sweep
// speed) comes from the on-scene op's Tech level via NSData.minigameSpec.
function QuickHack({ mg, onFinish }) {
  const { useState, useEffect, useRef } = React;
  const spec = mg.spec;
  const band = spec.band, speed = spec.speed, total = spec.timer;
  const [cursor, setCursor] = useState(0);
  const [timeLeft, setTimeLeft] = useState(total);
  const [result, setResult] = useState(null); // null | 'win' | 'miss'
  const center = useRef(band / 2 + Math.random() * (1 - band));
  const lo = center.current - band / 2, hi = center.current + band / 2;
  const cursorRef = useRef(0); cursorRef.current = cursor;
  const doneRef = useRef(false);

  const finish = (didWin) => {
    if (doneRef.current) return;
    doneRef.current = true;
    setResult(didWin ? 'win' : 'miss');
    setTimeout(() => onFinish(didWin), 700);
  };
  const lock = () => finish(cursorRef.current >= lo && cursorRef.current <= hi);

  useEffect(() => {
    const start = performance.now();
    let raf;
    function frame(now) {
      const t = (now - start) / 1000;
      const left = Math.max(0, total - t);
      setTimeLeft(left);
      const phase = (t * speed) % 1;             // triangle wave 0→1→0
      setCursor(phase < 0.5 ? phase * 2 : 2 - phase * 2);
      if (left <= 0) { finish(false); return; }
      if (!doneRef.current) raf = requestAnimationFrame(frame);
    }
    raf = requestAnimationFrame(frame);
    const onKey = (e) => { if (e.code === 'Space' || e.key === ' ') { e.preventDefault(); lock(); } };
    window.addEventListener('keydown', onKey);
    return () => { cancelAnimationFrame(raf); window.removeEventListener('keydown', onKey); };
  }, []);

  return (
    <div id="minigame-overlay">
      <div className="mg-card">
        <div className="mg-head">
          <span className="mg-code">{mg.label}</span>
          <span className="mg-kind">{spec.title} · {NS_SKILL_ABBR[spec.skill]} {mg.level}</span>
        </div>
        <div className="mg-hint">{spec.hint}</div>
        <div className="mg-track">
          <div className="mg-band" style={{ left: `${lo * 100}%`, width: `${band * 100}%` }} />
          <div className={'mg-cursor' + (result ? ' ' + result : '')} style={{ left: `${cursor * 100}%` }} />
        </div>
        <div className="mg-timer"><div className="mg-timer-fill" style={{ width: `${(timeLeft / total) * 100}%` }} /></div>
        <div className="mg-actions">
          <button className={'mg-lock' + (result ? ' ' + result : '')} onClick={lock} disabled={!!result}>
            {result === 'win' ? 'INTERCEPTED ✓' : result === 'miss' ? 'MISSED — FELL BACK' : 'LOCK ▸ SPACE'}
          </button>
          <button className="mg-skip" onClick={() => finish(false)} disabled={!!result}>SKIP</button>
        </div>
        <div className="mg-foot">Beat it to lock the play · skip or miss falls back to a {NS_SKILL_ABBR[spec.skill]} roll.</div>
      </div>
    </div>
  );
}

// Lockpick (Stealth): hold to turn the pin up, release inside the "give".
// Overshoot past the give and the cylinder slips (fail).
function LockPick({ mg, onFinish }) {
  const { useState, useEffect, useRef } = React;
  const spec = mg.spec;
  const band = spec.band, rate = spec.speed, total = spec.timer;
  const center = useRef(0.30 + Math.random() * Math.max(0.05, 0.62 - band));
  const lo = center.current, hi = center.current + band;
  const [pos, setPos] = useState(0);
  const [timeLeft, setTimeLeft] = useState(total);
  const [result, setResult] = useState(null);
  const posRef = useRef(0), heldRef = useRef(false), doneRef = useRef(false);

  const finish = (didWin) => {
    if (doneRef.current) return; doneRef.current = true;
    setResult(didWin ? 'win' : 'miss');
    setTimeout(() => onFinish(didWin), 700);
  };
  const press = () => { if (!doneRef.current) heldRef.current = true; };
  const release = () => {
    if (doneRef.current || !heldRef.current) return;
    heldRef.current = false;
    finish(posRef.current >= lo && posRef.current <= hi);
  };

  useEffect(() => {
    const start = performance.now(); let last = start, raf;
    function frame(now) {
      const dt = (now - last) / 1000; last = now;
      setTimeLeft(Math.max(0, total - (now - start) / 1000));
      if (heldRef.current) {
        posRef.current = Math.min(1, posRef.current + rate * dt);
        setPos(posRef.current);
        if (posRef.current >= 1) { finish(false); return; }   // slipped past the give
      }
      if (total - (now - start) / 1000 <= 0) { finish(false); return; }
      if (!doneRef.current) raf = requestAnimationFrame(frame);
    }
    raf = requestAnimationFrame(frame);
    const kd = (e) => { if ((e.code === 'Space' || e.key === ' ') && !e.repeat) { e.preventDefault(); press(); } };
    const ku = (e) => { if (e.code === 'Space' || e.key === ' ') { e.preventDefault(); release(); } };
    window.addEventListener('keydown', kd); window.addEventListener('keyup', ku);
    window.addEventListener('mouseup', release); window.addEventListener('touchend', release);
    return () => { cancelAnimationFrame(raf); window.removeEventListener('keydown', kd); window.removeEventListener('keyup', ku); window.removeEventListener('mouseup', release); window.removeEventListener('touchend', release); };
  }, []);

  return (
    <div id="minigame-overlay">
      <div className="mg-card">
        <div className="mg-head"><span className="mg-code">{mg.label}</span><span className="mg-kind">{spec.title} · {NS_SKILL_ABBR[spec.skill]} {mg.level}</span></div>
        <div className="mg-hint">{spec.hint}</div>
        <div className="mg-track">
          <div className="mg-band give" style={{ left: `${lo * 100}%`, width: `${band * 100}%` }} />
          <div className={'mg-cursor' + (result ? ' ' + result : '')} style={{ left: `${pos * 100}%` }} />
        </div>
        <div className="mg-timer"><div className="mg-timer-fill" style={{ width: `${(timeLeft / total) * 100}%` }} /></div>
        <div className="mg-actions">
          <button className={'mg-lock' + (result ? ' ' + result : '')} onMouseDown={press} onTouchStart={(e) => { e.preventDefault(); press(); }} disabled={!!result}>
            {result === 'win' ? 'PICKED ✓' : result === 'miss' ? 'SLIPPED — FELL BACK' : 'HOLD TO TURN'}
          </button>
          <button className="mg-skip" onClick={() => finish(false)} disabled={!!result}>SKIP</button>
        </div>
        <div className="mg-foot">Release in the give to set the pin · skip or slip falls back to a {NS_SKILL_ABBR[spec.skill]} roll.</div>
      </div>
    </div>
  );
}

// Stabilize (Medical): vitals drift down; hold to push them up. Keep them in
// the green for the cumulative count to win.
function Stabilize({ mg, onFinish }) {
  const { useState, useEffect, useRef } = React;
  const spec = mg.spec;
  const band = spec.band, drift = spec.speed, need = spec.hold, total = spec.timer, lift = 0.72;
  const lo = Math.max(0, 0.55 - band / 2), hi = Math.min(1, 0.55 + band / 2);
  const [val, setVal] = useState(0.5);
  const [held, setHeld] = useState(0);
  const [timeLeft, setTimeLeft] = useState(total);
  const [result, setResult] = useState(null);
  const valRef = useRef(0.5), accRef = useRef(0), heldRef = useRef(false), doneRef = useRef(false);

  const finish = (didWin) => {
    if (doneRef.current) return; doneRef.current = true;
    setResult(didWin ? 'win' : 'miss');
    setTimeout(() => onFinish(didWin), 700);
  };
  const press = () => { if (!doneRef.current) heldRef.current = true; };
  const release = () => { heldRef.current = false; };

  useEffect(() => {
    const start = performance.now(); let last = start, raf;
    function frame(now) {
      const dt = (now - last) / 1000; last = now;
      const left = Math.max(0, total - (now - start) / 1000); setTimeLeft(left);
      let v = valRef.current + (heldRef.current ? lift : -drift) * dt;
      v = Math.max(0, Math.min(1, v)); valRef.current = v; setVal(v);
      if (v >= lo && v <= hi) { accRef.current += dt; setHeld(accRef.current); }
      if (accRef.current >= need) { finish(true); return; }
      if (left <= 0) { finish(false); return; }
      if (!doneRef.current) raf = requestAnimationFrame(frame);
    }
    raf = requestAnimationFrame(frame);
    const kd = (e) => { if ((e.code === 'Space' || e.key === ' ') && !e.repeat) { e.preventDefault(); press(); } };
    const ku = (e) => { if (e.code === 'Space' || e.key === ' ') { e.preventDefault(); release(); } };
    window.addEventListener('keydown', kd); window.addEventListener('keyup', ku);
    window.addEventListener('mouseup', release); window.addEventListener('touchend', release);
    return () => { cancelAnimationFrame(raf); window.removeEventListener('keydown', kd); window.removeEventListener('keyup', ku); window.removeEventListener('mouseup', release); window.removeEventListener('touchend', release); };
  }, []);

  const frac = Math.min(1, held / need);
  const inBand = val >= lo && val <= hi;
  return (
    <div id="minigame-overlay">
      <div className="mg-card">
        <div className="mg-head"><span className="mg-code">{mg.label}</span><span className="mg-kind">{spec.title} · {NS_SKILL_ABBR[spec.skill]} {mg.level}</span></div>
        <div className="mg-hint">{spec.hint}</div>
        <div className="mg-track">
          <div className="mg-band" style={{ left: `${lo * 100}%`, width: `${(hi - lo) * 100}%` }} />
          <div className={'mg-cursor' + (result ? ' ' + result : (inBand ? ' win' : ''))} style={{ left: `${val * 100}%` }} />
        </div>
        <div className="mg-progress"><div className="mg-progress-fill" style={{ width: `${frac * 100}%` }} /></div>
        <div className="mg-timer"><div className="mg-timer-fill" style={{ width: `${(timeLeft / total) * 100}%` }} /></div>
        <div className="mg-actions">
          <button className={'mg-lock' + (result ? ' ' + result : '')} onMouseDown={press} onTouchStart={(e) => { e.preventDefault(); press(); }} disabled={!!result}>
            {result === 'win' ? 'STABILIZED ✓' : result === 'miss' ? 'LOST THEM — FELL BACK' : 'HOLD TO BOOST'}
          </button>
          <button className="mg-skip" onClick={() => finish(false)} disabled={!!result}>SKIP</button>
        </div>
        <div className="mg-foot">Hold vitals in the green for {need.toFixed(1)}s · skip or fail falls back to a {NS_SKILL_ABBR[spec.skill]} roll.</div>
      </div>
    </div>
  );
}

// Marquee hack (Phase C3a): route the avatar token through the node-grid maze
// to the green goal node. Arrow keys (desktop) or the on-screen d-pad (mobile).
// Reach the goal → guaranteed win; skip or time out → falls back to a Tech roll.
const HACK_STEP = 90, HACK_MARGIN = 34;
function hackKey(a, b) { return a < b ? a + '-' + b : b + '-' + a; }
const HACK_DELTA = { U: [0, -1], D: [0, 1], L: [-1, 0], R: [1, 0] };
const HACK_ARROW = { U: '▲', D: '▼', L: '◀', R: '▶' };
function HackGame({ mg, onFinish }) {
  const { useState, useEffect, useRef } = React;
  const spec = mg.spec;
  const closed = spec.closedEdges || {};
  const gates = spec.gates || {};
  const edgeSet = useRef(new Set(spec.edges)).current;
  const [cur, setCur] = useState(spec.start);
  const [timeLeft, setTimeLeft] = useState(spec.timer);
  const [result, setResult] = useState(null);
  const [fragIds, setFragIds] = useState([]);
  const [keyIds, setKeyIds] = useState([]);
  const [building, setBuilding] = useState(0);   // gate id being built, 0 = none
  const [seqIdx, setSeqIdx] = useState(0);
  const [bad, setBad] = useState(0);              // bumps to flash a wrong key
  const [builtVer, setBuiltVer] = useState(0);    // bumps when a gate is built (forces re-render)
  const curRef = useRef(spec.start), doneRef = useRef(false);
  const fragsRef = useRef(new Set()), keysRef = useRef(new Set()), builtRef = useRef(new Set());
  const buildingRef = useRef(0), seqIdxRef = useRef(0);
  const nodeMap = useRef(null);
  if (!nodeMap.current) { nodeMap.current = {}; spec.nodes.forEach(n => nodeMap.current[n.id] = n); }
  const passable = (id) => { const n = nodeMap.current[id]; return !n.lock || keysRef.current.has(n.lock); };
  const edgeOpen = (a, b) => { const g = closed[hackKey(a, b)]; return !g || builtRef.current.has(g); };
  const adjRef = useRef(null);
  const rebuildAdj = () => {
    const a = {}; spec.nodes.forEach(n => a[n.id] = []);
    spec.edges.forEach(e => { const g = closed[e]; if (g && !builtRef.current.has(g)) return; const [x, y] = e.split('-').map(Number); a[x].push(y); a[y].push(x); });
    adjRef.current = a;
  };
  if (!adjRef.current) rebuildAdj();
  const [pur, setPur] = useState(spec.pursuer ? spec.pursuer.start : -1);
  const [caught, setCaught] = useState(false);
  const purRef = useRef(spec.pursuer ? spec.pursuer.start : -1), pacc = useRef(0);
  // Shortest-path next hop from the daemon toward the player, over OPEN wires.
  const nextHop = (from, to) => {
    if (from === to) return from;
    const prev = {}; const seen = new Set([from]); const q = [from];
    while (q.length) { const u = q.shift(); if (u === to) break; for (const v of adjRef.current[u]) if (!seen.has(v)) { seen.add(v); prev[v] = u; q.push(v); } }
    if (!(to in prev)) return from;
    let c = to; while (prev[c] !== from) c = prev[c];
    return c;
  };
  const px = (c) => HACK_MARGIN + c * HACK_STEP;
  const py = (r) => HACK_MARGIN + r * HACK_STEP;
  const W = (spec.cols - 1) * HACK_STEP + 2 * HACK_MARGIN;
  const H = (spec.rows - 1) * HACK_STEP + 2 * HACK_MARGIN;

  const finish = (didWin) => {
    if (doneRef.current) return; doneRef.current = true;
    setResult(didWin ? 'win' : 'miss');
    setTimeout(() => onFinish(didWin), 750);
  };
  const move = (dc, dr) => {
    if (doneRef.current) return;
    const c = curRef.current % spec.cols, r = Math.floor(curRef.current / spec.cols);
    const nc = c + dc, nr = r + dr;
    if (nc < 0 || nc >= spec.cols || nr < 0 || nr >= spec.rows) return;
    const t = nr * spec.cols + nc;
    if (!edgeSet.has(hackKey(curRef.current, t))) return;   // no wire between the nodes
    if (!edgeOpen(curRef.current, t)) return;                // unbuilt path — build it at the gate node
    if (!passable(t)) return;                                // locked until its passkey is tripped
    curRef.current = t; setCur(t);
    if (purRef.current === t) { setCaught(true); finish(false); return; }   // stepped onto the daemon
    const n = nodeMap.current[t];
    if (n.frag && !fragsRef.current.has(t)) { fragsRef.current.add(t); setFragIds([...fragsRef.current]); }
    if (n.key && !keysRef.current.has(n.key)) { keysRef.current.add(n.key); setKeyIds([...keysRef.current]); }
    if (t === spec.goal && fragsRef.current.size >= spec.fragCount) finish(true);
  };
  const startBuild = () => {
    const n = nodeMap.current[curRef.current];
    if (!n || !n.gate || builtRef.current.has(n.gate)) return;
    buildingRef.current = n.gate; setBuilding(n.gate);
    seqIdxRef.current = 0; setSeqIdx(0);
  };
  const cancelBuild = () => { buildingRef.current = 0; setBuilding(0); seqIdxRef.current = 0; setSeqIdx(0); };
  const feedSeq = (d) => {
    const gid = buildingRef.current; if (!gid) return;
    const seq = gates[gid].seq;
    if (d === seq[seqIdxRef.current]) {
      seqIdxRef.current++; setSeqIdx(seqIdxRef.current);
      if (seqIdxRef.current >= seq.length) {            // built!
        builtRef.current.add(gid); rebuildAdj();
        buildingRef.current = 0; setBuilding(0);
        seqIdxRef.current = 0; setSeqIdx(0);
        setBuiltVer(v => v + 1);
      }
    } else { seqIdxRef.current = 0; setSeqIdx(0); setBad(b => b + 1); }
  };
  const inputDir = (d) => {
    if (doneRef.current) return;
    if (buildingRef.current) feedSeq(d);
    else { const m = HACK_DELTA[d]; move(m[0], m[1]); }
  };

  useEffect(() => {
    const t0 = performance.now(); let last = t0, raf;
    function frame(now) {
      const dt = Math.min(0.1, (now - last) / 1000); last = now;   // cap so a tab-switch can't teleport the daemon
      const left = Math.max(0, spec.timer - (now - t0) / 1000);
      setTimeLeft(left);
      if (spec.pursuer) {
        pacc.current += dt;
        while (pacc.current >= spec.pursuer.step && !doneRef.current) {
          pacc.current -= spec.pursuer.step;
          const nh = nextHop(purRef.current, curRef.current);
          purRef.current = nh; setPur(nh);
          if (nh === curRef.current) { setCaught(true); finish(false); return; }
        }
      }
      if (left <= 0) { finish(false); return; }
      if (!doneRef.current) raf = requestAnimationFrame(frame);
    }
    raf = requestAnimationFrame(frame);
    const kd = (e) => {
      const dir = { ArrowUp: 'U', ArrowDown: 'D', ArrowLeft: 'L', ArrowRight: 'R' }[e.key];
      if (dir) { e.preventDefault(); inputDir(dir); return; }
      if ((e.key === 'Enter' || e.key === 'b' || e.key === 'B') && !buildingRef.current) { e.preventDefault(); startBuild(); }
    };
    window.addEventListener('keydown', kd);
    return () => { cancelAnimationFrame(raf); window.removeEventListener('keydown', kd); };
  }, []);

  const cc = cur % spec.cols, cr = Math.floor(cur / spec.cols);
  const gc = spec.goal % spec.cols, gr = Math.floor(spec.goal / spec.cols);
  const hex = (x, y, rad) => {
    let pts = '';
    for (let i = 0; i < 6; i++) { const a = Math.PI / 180 * (60 * i - 90); pts += (x + rad * Math.cos(a)).toFixed(1) + ',' + (y + rad * Math.sin(a)).toFixed(1) + ' '; }
    return pts.trim();
  };
  const onGate = (() => { const n = nodeMap.current[cur]; return n && n.gate && !builtRef.current.has(n.gate) ? n.gate : 0; })();
  const buildSeq = building ? gates[building].seq : null;

  return (
    <div id="minigame-overlay">
      <div className={'mg-card hack' + (caught ? ' caught' : '')}>
        <div className="mg-head"><span className="mg-code">{mg.label}</span><span className="mg-kind">{spec.title} · {NS_SKILL_ABBR[spec.skill]} {mg.level}</span></div>
        <div className="mg-hint">{spec.hint}</div>
        <svg className="hack-grid" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid meet">
          {spec.edges.map(e => {
            const [a, b] = e.split('-').map(Number);
            const g = closed[e]; const isClosed = g && !builtRef.current.has(g);
            return <line key={e} className={'hack-edge' + (isClosed ? ' closed' : '')} x1={px(a % spec.cols)} y1={py(Math.floor(a / spec.cols))} x2={px(b % spec.cols)} y2={py(Math.floor(b / spec.cols))} />;
          })}
          {spec.nodes.map(n => {
            if (n.id === spec.goal) return null;
            const x = px(n.c), y = py(n.r);
            if (n.gate) return <rect key={n.id} className={'hack-gate' + (builtRef.current.has(n.gate) ? ' built' : '')} x={x - 9} y={y - 9} width="18" height="18" />;
            if (n.frag) return <polygon key={n.id} className={'hack-frag' + (fragIds.includes(n.id) ? ' got' : '')} points={`${x},${y - 10} ${x + 10},${y} ${x},${y + 10} ${x - 10},${y}`} />;
            if (n.lock) return <circle key={n.id} className={'hack-lock' + (keyIds.includes(n.lock) ? ' open' : '')} cx={x} cy={y} r="9" />;
            if (n.key) return <circle key={n.id} className={'hack-key' + (keyIds.includes(n.key) ? ' used' : '')} cx={x} cy={y} r="9" />;
            return <circle key={n.id} className={'hack-node' + (n.id === spec.start ? ' start' : '')} cx={x} cy={y} r="9" />;
          })}
          <polygon className={'hack-goal' + (fragIds.length >= spec.fragCount ? ' ready' : '')} points={hex(px(gc), py(gr), 16)} />
          {spec.pursuer && pur >= 0 && <circle className="hack-pursuer" cx={px(pur % spec.cols)} cy={py(Math.floor(pur / spec.cols))} r="10" />}
          <circle className={'hack-avatar' + (result ? ' ' + result : '')} cx={px(cc)} cy={py(cr)} r="11" />
        </svg>
        {building ? (
          <div className={'hack-build' + (bad ? ' bad' : '')} key={'b' + bad}>
            <span className="hack-build-lbl">BUILD PATH</span>
            <span className="hack-build-seq">
              {buildSeq.map((d, i) => <span key={i} className={'hb-arrow' + (i < seqIdx ? ' done' : i === seqIdx ? ' cur' : '')}>{HACK_ARROW[d]}</span>)}
            </span>
            <button className="hack-build-cancel" onClick={cancelBuild}>BACK</button>
          </div>
        ) : null}
        <div className="mg-progress"><div className="mg-progress-fill" style={{ width: `${(fragIds.length / spec.fragCount) * 100}%` }} /></div>
        <div className="mg-timer"><div className="mg-timer-fill" style={{ width: `${(timeLeft / spec.timer) * 100}%` }} /></div>
        <div className="hack-controls">
          <div className="dpad">
            <button className="dp up" onClick={() => inputDir('U')} disabled={!!result}>▲</button>
            <button className="dp left" onClick={() => inputDir('L')} disabled={!!result}>◀</button>
            <button className="dp right" onClick={() => inputDir('R')} disabled={!!result}>▶</button>
            <button className="dp down" onClick={() => inputDir('D')} disabled={!!result}>▼</button>
          </div>
          {onGate && !building
            ? <button className="hack-buildbtn" onClick={startBuild} disabled={!!result}>BUILD PATH ▸</button>
            : <button className="mg-skip" onClick={() => finish(false)} disabled={!!result}>SKIP</button>}
        </div>
        <div className="mg-foot">
          {caught ? 'INTRUDER DETECTED — LOCKED OUT'
            : result === 'win' ? 'TERMINAL BREACHED ✓'
            : result === 'miss' ? 'LOCKED OUT — FELL BACK'
            : building ? 'Enter the arrow sequence to build the path.'
            : onGate ? 'Build node — open the closed (dashed) wire from here.'
            : 'Keys ' + fragIds.length + '/' + spec.fragCount + ' · build dashed wires · dodge the daemon · reach the green node.'}
        </div>
      </div>
    </div>
  );
}

function MinigameOverlay({ minigame, onFinish }) {
  if (!minigame) return null;
  const key = minigame.callId + ':' + minigame.eventIdx;
  const kind = minigame.spec.kind;
  if (kind === 'hack') return <HackGame key={key} mg={minigame} onFinish={onFinish} />;
  if (kind === 'lockpick') return <LockPick key={key} mg={minigame} onFinish={onFinish} />;
  if (kind === 'stabilize') return <Stabilize key={key} mg={minigame} onFinish={onFinish} />;
  return <QuickHack key={key} mg={minigame} onFinish={onFinish} />;
}

// ─── Main app ────────────────────────────────────────────────────────
function App() {
  const game = useGameLoop({ callInterval: [9, 16] });

  const dismissIntro = () => {
    const el = document.getElementById('intro');
    if (el) {
      el.classList.add('hidden');
      setTimeout(() => { el.style.display = 'none'; }, 800);
    }
  };
  const handleStart = () => { dismissIntro(); game.start(); };
  const handleContinue = () => { dismissIntro(); game.continueShift(); };

  return (
    <React.Fragment>
      <div id="hud">
        <TopBar tick={game.tick} score={game.score} heat={game.heat} callsOpen={game.callsOpen} />
        <CallsPanel calls={game.calls} selectedCallId={game.selectedCallId} onSelect={game.selectCall} bestOddsFor={game.bestOddsFor} />
        <OpsPanel opsDisplay={game.opsDisplay} selectedCall={game.selectedCall} onAssign={game.assignOp} oddsFor={game.oddsFor} teamOddsFor={game.teamOddsFor} pendingOps={game.pendingOps} />
        <RadioPanel log={game.log} />
      </div>
      {!game.sceneFailed && <BanterBubbles banter={game.banter} tick={game.tick} />}
      <EventOverlay event={game.activeMinigame ? null : game.activeEvent} tick={game.tick} onResolve={game.resolveEvent} />
      <MinigameOverlay minigame={game.activeMinigame} onFinish={game.finishMinigame} />
      <EndShift score={game.score} show={game.gameOver} onRestart={game.restart} />
      <Intro onStart={handleStart} onContinue={handleContinue} hasSave={game.hasSave} bestScore={game.bestScore} sceneFailed={game.sceneFailed} />
    </React.Fragment>
  );
}

const root = ReactDOM.createRoot(document.getElementById('react-root'));
root.render(<App />);
