/* NIGHTSHIFT MOBILE — compact React HUD with tabbed bottom sheet.
   Game logic lives in game-loop.js. */

const { useState, useMemo, useCallback } = React;
const M_ROSTER = window.NSData.ROSTER;
const M_SKILL_ABBR = window.NSData.SKILL_ABBR;
const MG = window.NightshiftGame;
const { fmtClock, fmtMoney, bestSkills, useGameLoop } = MG;

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 }) {
  return (
    <div id="topbar">
      <div className="left">
        <div className="t-stat">
          <div className="lbl">SHIFT</div>
          <div className="val amber">{fmtClock(tick)}</div>
        </div>
      </div>
      <div className="brand">Night<span className="amp">·</span>Shift</div>
      <div className="right">
        <div className="t-stat">
          <div className="lbl">TAKE</div>
          <div className="val amber">{fmtMoney(score.cash)}</div>
        </div>
        <div className="t-stat" style={{ minWidth: 66 }}>
          <div className="lbl">HEAT</div>
          <div className="heat-meter"><div className="fill" style={{ width: `${Math.min(100, heat)}%` }} /></div>
        </div>
      </div>
    </div>
  );
}

// ─── Map labels (over the scene) ──────────────────────────────────
function MapLabels({ calls, tick }) {
  // Re-render every frame via tick so positions follow camera breathing.
  const labels = useMemo(() => {
    if (!window.NightshiftScene) return [];
    return calls
      .filter(c => c.status === 'open' || c.status === 'en-route' || c.status === 'on-scene')
      .map(c => {
        const pos = NightshiftScene.gridToScreen(c.cell.x, c.cell.z, 13);
        return { id: c.id, x: pos.x, y: pos.y, type: c.type, urgent: c.urgent, status: c.status };
      });
  }, [calls, tick]);

  return (
    <React.Fragment>
      {labels.map(l => (
        <div
          key={l.id}
          className={'map-tag' + (l.urgent ? ' urgent' : '')}
          style={{ left: l.x + 'px', top: l.y + 'px' }}
        >
          {l.type}
        </div>
      ))}
    </React.Fragment>
  );
}

// ─── Call card ────────────────────────────────────────────────────
function CallRow({ call, selected, onSelect, opAssigned, odds }) {
  const ratio = call.timer / call.timerMax;
  const classes = ['m-call'];
  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="row1">
        <span className="code">{call.code} · {call.id.slice(2)}</span>
        {showOdds
          ? <span className={'odds-chip ' + oddsClass(odds.p)}>{Math.round(odds.p * 100)}%</span>
          : <span className="phase">{phaseLabel}</span>}
      </div>
      <div className="type">{call.type}</div>
      <div className="loc">{call.location.toUpperCase()}</div>
      <div className="tags">
        {call.teamSize === 2 && <span className="tag two">2-OP</span>}
        {call.req.map(r => (
          <span key={r} className="tag req">{M_SKILL_ABBR[r]} REQ</span>
        ))}
        {call.bonus.map(b => (
          <span key={b} className="tag">{M_SKILL_ABBR[b]} +</span>
        ))}
        {opAssigned && <span className="tag met">UNIT {opAssigned}</span>}
      </div>
      <div className="tbar"><div className="tfill" style={{ width: `${ratio * 100}%` }} /></div>
    </div>
  );
}

// ─── Op card ──────────────────────────────────────────────────────
function OpRow({ op, opState, odds, picked, teamSize, busy, onAssign }) {
  const targetable = odds != null && !busy;
  const classes = ['m-op'];
  if (picked) classes.push('picked');
  else if (targetable) classes.push('compatible');
  if (busy) classes.push('busy');
  const statusLabel = {
    available: 'AVAILABLE · HQ',
    enroute: `EN ROUTE · ${opState.eta}s`,
    onscene: `ON SCENE · ${opState.workLeft}s`,
    returning: `RETURNING · ${opState.eta}s`,
  }[opState.status] || opState.status.toUpperCase();
  const verb = teamSize === 2 ? 'TAP TO PAIR ▸' : 'TAP TO DISPATCH ▸';
  return (
    <div className={classes.join(' ')} onClick={() => !busy && onAssign(op.id)}>
      <div className="av" style={{ borderColor: '#' + op.color.toString(16).padStart(6, '0') }}>
        {op.codename[0]}
      </div>
      <div className="meta">
        <div className="name">{op.codename}</div>
        <div className="role">{op.role}</div>
        <div className="status">
          {picked
            ? <span className="picked-tag">PICKED ✓ · TAP TO REMOVE</span>
            : targetable
              ? <span className={'m-odds ' + oddsClass(odds)}>{Math.round(odds * 100)}% · {verb}</span>
              : statusLabel}
        </div>
      </div>
      <div className="pips">
        {bestSkills(op).map(({ k, v }) => (
          <span key={k} className={'pip' + (v >= 2 ? ' hi' : '')}>{M_SKILL_ABBR[k]} · {v}</span>
        ))}
      </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)}>{M_SKILL_ABBR[o.skill]} · {Math.round(o.chance * 100)}%</span>}
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

// ─── Minigame: quick-hack (Phase C1) ────────────────────────────────
// Tap LOCK while the cursor is in 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. Board pauses while it's up.
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;
      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} · {M_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 ▸'}
          </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 {M_SKILL_ABBR[spec.skill]} roll.</div>
      </div>
    </div>
  );
}

// Lockpick (Stealth): hold to turn the pin up, release inside the "give".
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; }
      }
      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} · {M_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 {M_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} · {M_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 {M_SKILL_ABBR[spec.skill]} roll.</div>
      </div>
    </div>
  );
}

// Marquee hack (Phase C3a): route the avatar through the maze to the green
// node. On-screen d-pad (mobile) or arrow keys. Reach goal → win.
const HACK_STEP = 90, HACK_MARGIN = 34;
function hackKey(a, b) { return a < b ? a + '-' + b : b + '-' + a; }
function HackGame({ mg, onFinish }) {
  const { useState, useEffect, useRef } = React;
  const spec = mg.spec;
  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 curRef = useRef(spec.start), doneRef = useRef(false);
  const fragsRef = useRef(new Set()), keysRef = useRef(new Set());
  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 adjRef = useRef(null);
  if (!adjRef.current) {
    adjRef.current = {}; spec.nodes.forEach(n => adjRef.current[n.id] = []);
    spec.edges.forEach(e => { const [a, b] = e.split('-').map(Number); adjRef.current[a].push(b); adjRef.current[b].push(a); });
  }
  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);
  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 cur = to; while (prev[cur] !== from) cur = prev[cur];
    return cur;
  };
  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;
    if (!passable(t)) return;
    curRef.current = t; setCur(t);
    if (purRef.current === t) { setCaught(true); finish(false); return; }
    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);
  };

  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 m = { ArrowUp: [0, -1], ArrowDown: [0, 1], ArrowLeft: [-1, 0], ArrowRight: [1, 0] }[e.key];
      if (m) { e.preventDefault(); move(m[0], m[1]); }
    };
    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();
  };

  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} · {M_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);
            return <line key={e} className="hack-edge" 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.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>
        <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={() => move(0, -1)} disabled={!!result}>▲</button>
            <button className="dp left" onClick={() => move(-1, 0)} disabled={!!result}>◀</button>
            <button className="dp right" onClick={() => move(1, 0)} disabled={!!result}>▶</button>
            <button className="dp down" onClick={() => move(0, 1)} disabled={!!result}>▼</button>
          </div>
          <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'
            : 'Keys ' + fragIds.length + '/' + spec.fragCount + ' · trip passkeys for red locks · 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} />;
}

// ─── App ──────────────────────────────────────────────────────────
function App() {
  const [tab, setTab] = useState('calls'); // calls | ops | radio

  const onSelect = useCallback((next) => { if (next) setTab('ops'); }, []);
  const onAssign = useCallback(() => setTab('calls'), []);
  const onCancel = useCallback(() => setTab('calls'), []);

  const game = useGameLoop({
    callInterval: [10, 17],   // slightly easier on mobile
    trackLabels: true,
    onSelect, onAssign, onCancel,
  });

  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>
      <TopBar tick={game.tick} score={game.score} heat={game.heat} />

      <MapLabels calls={game.calls} tick={game.labelTick} />

      {!game.sceneFailed && <BanterBubbles banter={game.banter} tick={game.labelTick} />}

      <EventOverlay event={game.activeMinigame ? null : game.activeEvent} tick={game.tick} onResolve={game.resolveEvent} />
      <MinigameOverlay minigame={game.activeMinigame} onFinish={game.finishMinigame} />

      <div id="sheet">
        <div className="tabs">
          <button className={'tab' + (tab === 'calls' ? ' active' : '')} onClick={() => setTab('calls')}>
            <span>CALLS</span>
            <span className={'count' + (game.callsOpen > 0 && !game.selectedCallId ? ' alert' : '')}>{game.callsOpen}</span>
          </button>
          <button className={'tab' + (tab === 'ops' ? ' active' : '')} onClick={() => setTab('ops')}>
            <span>UNITS</span>
            <span className="count">{game.opsAvail}/{M_ROSTER.length}</span>
          </button>
          <button className={'tab' + (tab === 'radio' ? ' active' : '')} onClick={() => setTab('radio')}>
            <span>RADIO</span>
            <span className="count">{game.log.length}</span>
          </button>
        </div>

        <div className="sheet-body">
          {game.selectedCall && tab === 'ops' && (
            <div className="dispatch-banner">
              <div>
                <div className="lhs">
                  ▸ DISPATCHING · {game.selectedCall.code}
                  {(game.selectedCall.teamSize || 1) === 2 && ` · PAIR ${game.pendingOps.length}/2`}
                </div>
                <div className="target">{game.selectedCall.type}</div>
              </div>
              <button className="cancel" onClick={game.cancelSelect}>CANCEL</button>
            </div>
          )}

          {tab === 'calls' && (
            <React.Fragment>
              {game.callsOpen === 0 && <div className="empty">— quiet on the wire —</div>}
              {game.calls
                .filter(c => c.status !== 'resolved' && c.status !== 'failed')
                .map(c => {
                  const assignedNames = (c.assignedOpIds || [])
                    .map(id => { const o = M_ROSTER.find(x => x.id === id); return o ? o.codename : id; })
                    .join(' & ');
                  return (
                    <CallRow
                      key={c.id}
                      call={c}
                      selected={game.selectedCallId === c.id}
                      onSelect={game.selectCall}
                      opAssigned={assignedNames || null}
                      odds={c.status === 'open' ? game.bestOddsFor(c) : null}
                    />
                  );
                })}
            </React.Fragment>
          )}

          {tab === 'ops' && (
            <React.Fragment>
              {M_ROSTER.map(op => {
                const opState = game.opsDisplay[op.id];
                const busy = opState.status !== 'available';
                const teamSize = game.selectedCall ? (game.selectedCall.teamSize || 1) : 1;
                const picked = game.pendingOps.includes(op.id);
                let odds = null;
                if (game.selectedCall && !busy && !picked) {
                  odds = teamSize === 1
                    ? game.oddsFor(game.selectedCall, op)
                    : game.teamOddsFor(game.selectedCall, [...game.pendingOps, op.id]);
                }
                return (
                  <OpRow
                    key={op.id}
                    op={op}
                    opState={opState}
                    odds={odds}
                    picked={picked}
                    teamSize={teamSize}
                    busy={busy}
                    onAssign={game.assignOp}
                  />
                );
              })}
            </React.Fragment>
          )}

          {tab === 'radio' && (
            <div className="m-log">
              {game.log.length === 0 && <div className="empty">— no chatter yet —</div>}
              {game.log.slice(-50).reverse().map(l => (
                <div key={l.k} className="line">
                  <span className="t">{l.time}</span>
                  <span className={'tag ' + l.tag}>{l.tag.toUpperCase()}</span>
                  <span className="body">{l.body}</span>
                </div>
              ))}
            </div>
          )}
        </div>
      </div>

      {game.gameOver && (
        <div id="endshift" className="show">
          <div className="endshift-card">
            <div className="sub">03:00 · SHIFT COMPLETE</div>
            <h2>Sun's up.</h2>
            <div className="endshift-stats">
              <div>
                <div className="lbl">CLOSED</div>
                <div className="val" style={{ color: 'var(--green)' }}>{game.score.closed}</div>
              </div>
              <div>
                <div className="lbl">FAILED</div>
                <div className="val" style={{ color: 'var(--red)' }}>{game.score.failed}</div>
              </div>
              <div>
                <div className="lbl">MISSED</div>
                <div className="val" style={{ color: 'var(--ink-dim)' }}>{game.score.missed || 0}</div>
              </div>
              <div>
                <div className="lbl">TAKE</div>
                <div className="val" style={{ color: 'var(--amber)' }}>{fmtMoney(game.score.cash)}</div>
              </div>
            </div>
            <button className="endshift-btn" onClick={game.restart}>WORK ANOTHER NIGHT ▸</button>
          </div>
        </div>
      )}

      <div id="intro">
        <a className="intro-back" href="index.html">◂ DOCS</a>
        <div className="sub">CHAPTER ONE</div>
        <h1>Night<span className="accent">·</span>Shift</h1>
        <div className="sub">DISPATCH · MOBILE</div>
        <div className="sub deck">
          <div><b>SHIFT</b>19:00 → 03:00</div>
          <div><b>RUN</b>≈ 6 MIN</div>
          <div><b>OPS</b>6 ON CALL</div>
        </div>
        <div className="intro-help">
          Tap a call → tap a unit that matches the skill tags.<br/>
          Keep heat down. Keep the night quiet.
        </div>
        {game.hasSave ? (
          <React.Fragment>
            <button className="start-btn" onClick={handleContinue}>CONTINUE SHIFT ▸</button>
            <button className="start-btn ghost" onClick={handleStart}>NEW SHIFT</button>
          </React.Fragment>
        ) : (
          <button className="start-btn" onClick={handleStart}>BEGIN SHIFT</button>
        )}
        {game.bestScore > 0 && (
          <div className="intro-best">BEST TAKE · <span>{fmtMoney(game.bestScore)}</span></div>
        )}
        {game.sceneFailed && (
          <div className="intro-warn">3D CITY UNAVAILABLE (WEBGL) · DISPATCH STILL RUNS</div>
        )}
      </div>

      <div id="orient">
        <div>
          <div className="icon">↻</div>
          <div className="msg">Turn your phone<br/>upright to dispatch.</div>
        </div>
      </div>
    </React.Fragment>
  );
}

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