// Stage 05 + 06 — logits and sampling.
// Shows a bar chart of candidate next tokens with probabilities, then a
// "dice roll" selecting one. Returns the chosen token via onSample(token) when progress reaches 1.
function StageLogits({ context, progress, active, paused, temperature, onSample, onExplain }) {
  // Tier 1 (local, always available, instant): deterministic bias-table + categorical fallback.
  // Tier 2 (LIVE via proxy, best-effort): POST /api/next-tokens → Claude Haiku 4.5.
  //   Session-level cooldown state machine handles 400/429/503/529/timeout/network gracefully.
  //   If proxy fails or is in cooldown, we silently stay on local — UI never stalls.
  const localDist = React.useMemo(
    () => window.LogitsEngine.computeLocalDistribution(context, temperature),
    [context, temperature]
  );

  const [proxyDist, setProxyDist] = React.useState(null);
  const [proxyState, setProxyState] = React.useState(() => window.LogitsEngine.getProxyState());

  React.useEffect(() => {
    let cancelled = false;
    setProxyDist(null); // drop stale proxy result on context/temperature change
    window.LogitsEngine.computeProxyDistribution(context, temperature)
      .then(result => {
        if (cancelled) return;
        if (result) setProxyDist(result);
        setProxyState(window.LogitsEngine.getProxyState());
      })
      .catch(() => {
        if (!cancelled) setProxyState(window.LogitsEngine.getProxyState());
      });
    return () => { cancelled = true; };
  }, [context, temperature]);

  const distribution = (proxyDist && proxyDist.items) || localDist.items;
  // Badge source priority: if we have proxy data, show LIVE; else show the per-state badge
  // computed by the engine (LOCAL / BUDGET_EXHAUSTED / ERROR).
  const source = proxyDist ? proxyDist.source : (proxyState.badge === 'LIVE' ? 'local' : proxyState.badge.toLowerCase());

  // sampling: deterministic given distribution + context hash
  const chosen = React.useMemo(() => {
    const joined = context.map(t => t.text).join('');
    let h = 5381;
    for (let i = 0; i < joined.length; i++) { h = ((h << 5) + h + joined.charCodeAt(i)) | 0; }
    const r = Math.abs(Math.sin(h) * 10000) % 1;
    let cum = 0;
    for (const item of distribution) { cum += item.prob; if (r < cum) return item; }
    return distribution[0];
  }, [distribution, context]);

  // Fire onSample on completion
  const firedRef = React.useRef(false);
  React.useEffect(() => {
    if (active && progress >= 0.999 && !firedRef.current) {
      firedRef.current = true;
      onSample && onSample(chosen.tok);
    }
    if (!active) firedRef.current = false;
  }, [active, progress, chosen, onSample]);

  const p = active ? progress : 0;
  const phaseLogits = clamp(p / 0.5, 0, 1);
  const phaseSample = clamp((p - 0.7) / 0.3, 0, 1);

  // When the stage becomes active (including via jump-while-paused), run a short
  // local cascade so the bars light up regardless of play state. This doesn't
  // affect the real stage progression — it just guarantees visual feedback.
  const [localReveal, setLocalReveal] = React.useState(0);
  React.useEffect(() => {
    if (!active) { setLocalReveal(0); return; }
    // animate 0 → 1 over ~600ms
    let raf, start;
    const tick = (t) => {
      if (start == null) start = t;
      const dt = (t - start) / 600;
      setLocalReveal(Math.min(1, dt));
      if (dt < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [active]);

  // When paused, the reveal is driven by the local cascade (so jumping works).
  // When playing, it's driven by the real progress phase.
  const effectivePhaseLogits = paused ? localReveal : phaseLogits;

  return (
    <Panel stage={5} title="Logits → sample" subtitle={`linear head · softmax(·/T) · T=${temperature.toFixed(2)}`} active={active} progress={progress} onExplain={onExplain}>
      <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto', gap: 14, alignItems: 'stretch' }}>
        <div style={{ border: '1px solid var(--line)', borderRadius: 4, padding: '8px 10px', background: 'rgba(0,0,0,0.25)' }}>
          <div style={{ fontFamily: 'var(--mono)', fontSize: 9, color: 'var(--text-faint)', marginBottom: 6, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
              <span>top candidates · probability</span>
              <BadgeState source={source} proxyState={proxyState} />
            </span>
            <span>{distribution.length} shown of ~50000</span>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
            {distribution.slice(0, 10).map((item, i) => {
              const isChosen = active && phaseSample > 0 && item.tok === chosen.tok;
              const shown = effectivePhaseLogits > i / 10;
              return (
                <div key={item.tok} style={{
                  display: 'grid', gridTemplateColumns: '70px minmax(0, 1fr) 48px', alignItems: 'center', gap: 8,
                  opacity: shown ? 1 : 0.15, transition: 'opacity 150ms',
                }}>
                  <div style={{
                    fontFamily: 'var(--mono)', fontSize: 11,
                    color: isChosen ? 'var(--magenta)' : 'var(--text)',
                    textShadow: isChosen ? '0 0 8px var(--magenta)' : 'none',
                    whiteSpace: 'pre',
                    fontWeight: isChosen ? 500 : 400,
                  }}>"{item.tok.replace(/ /g, '␣')}"</div>
                  <div style={{ height: 10, background: 'rgba(140,255,160,0.06)', borderRadius: 1, overflow: 'hidden', position: 'relative' }}>
                    <div style={{
                      height: '100%', width: `${item.prob * 100}%`,
                      background: isChosen
                        ? 'linear-gradient(90deg, var(--magenta), color-mix(in oklch, var(--magenta), white 10%))'
                        : 'linear-gradient(90deg, color-mix(in oklch, var(--ink) 60%, transparent), var(--ink))',
                      boxShadow: isChosen ? '0 0 10px var(--magenta)' : '0 0 6px var(--ink)',
                      transition: 'width 300ms, background 200ms',
                    }} />
                  </div>
                  <div style={{
                    fontFamily: 'var(--mono)', fontSize: 10,
                    color: isChosen ? 'var(--magenta)' : 'var(--text-dim)',
                    textAlign: 'right',
                    fontVariantNumeric: 'tabular-nums',
                  }}>{(item.prob * 100).toFixed(1)}%</div>
                </div>
              );
            })}
          </div>
        </div>

        {/* sample indicator */}
        <div style={{
          border: `1px solid ${phaseSample > 0 ? 'var(--magenta)' : 'var(--line)'}`,
          borderRadius: 4, padding: '10px 14px', minWidth: 170,
          background: phaseSample > 0 ? 'color-mix(in oklch, var(--magenta) 8%, transparent)' : 'rgba(0,0,0,0.25)',
          display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', gap: 6,
          transition: 'border-color 300ms, background 300ms',
          boxShadow: phaseSample > 0 ? '0 0 20px color-mix(in oklch, var(--magenta) 30%, transparent)' : 'none',
        }}>
          <div style={{ fontFamily: 'var(--mono)', fontSize: 9, color: phaseSample > 0 ? 'var(--magenta)' : 'var(--text-faint)', letterSpacing: '0.1em' }}>SAMPLED</div>
          <div style={{
            fontFamily: 'var(--mono)', fontSize: 22, fontWeight: 500,
            color: phaseSample > 0 ? 'var(--magenta)' : 'var(--text-faint)',
            textShadow: phaseSample > 0 ? '0 0 14px var(--magenta)' : 'none',
            transition: 'color 300ms',
            whiteSpace: 'pre',
          }}>
            {phaseSample > 0 ? `"${chosen.tok.replace(/ /g, '␣')}"` : '···'}
          </div>
          <div style={{ fontFamily: 'var(--mono)', fontSize: 9, color: 'var(--text-faint)' }}>
            p = {phaseSample > 0 ? (chosen.prob * 100).toFixed(1) + '%' : '—'}
          </div>
        </div>
      </div>
    </Panel>
  );
}
// ── Badge ──────────────────────────────────────────────────────────────────
// Two visible states:
//   LIVE (magenta, glowing) — proxy returned a real distribution.
//   LOCAL (dim, quiet)      — anything else. The fallback is graceful by design,
//                             so unreachable/timeout/error all just read LOCAL.
// BUDGET exhaustion is actionable so it gets its own amber treatment; everything
// else (network down, cooldown, no proxy configured) reads LOCAL, with the
// reason surfaced only in the hover tooltip.
function BadgeState({ source, proxyState }) {
  const isLive = source === 'proxy' || source === 'proxy-cached';
  const isBudget = proxyState.badge === 'BUDGET_EXHAUSTED' && !isLive;

  let label, color, title;
  if (isLive) {
    label = 'LIVE';
    color = 'var(--magenta)';
    const dayRem = proxyState.dayRemainingUsd;
    const livenessNote = source === 'proxy-cached' ? ' (cached)' : '';
    title = `Distribution via Anthropic proxy${livenessNote}. The Messages API doesn't expose token logits — probabilities are Claude-estimated, not measured. See proxy/README.md. ${dayRem != null ? `Budget remaining today: $${dayRem.toFixed(3)}.` : ''}`;
  } else if (isBudget) {
    label = 'BUDGET';
    color = 'var(--amber)';
    title = `Daily/monthly proxy budget exhausted — using local bias table until the window resets. ${proxyState.lastError}`;
  } else {
    label = 'LOCAL';
    color = 'var(--text-faint)';
    // Compose a tooltip that quietly explains why we're on LOCAL.
    const reasonBits = [];
    if (proxyState.badge === 'ERROR' && proxyState.lastError) {
      reasonBits.push(`Proxy unavailable: ${proxyState.lastError}. Will retry automatically.`);
    } else {
      reasonBits.push('Run `node proxy/server.mjs` with ANTHROPIC_API_KEY to enable LIVE (or use ./dev if .env is set).');
    }
    title = `Using the local deterministic bias table. Ships with the HTML, no network required. ${reasonBits.join(' ')}`;
  }

  const dotGlow = isLive ? '0 0 4px var(--magenta)' : (isBudget ? '0 0 4px var(--amber)' : 'none');
  const borderColor = isLive ? 'var(--magenta)' : (isBudget ? 'var(--amber)' : 'var(--line-strong)');

  return (
    <span title={title} style={{
      display: 'inline-flex', alignItems: 'center', gap: 4,
      padding: '1px 5px', borderRadius: 2,
      border: `1px solid ${borderColor}`,
      color,
      fontSize: 8, letterSpacing: '0.1em', textTransform: 'uppercase',
    }}>
      <span style={{
        display: 'inline-block', width: 4, height: 4, borderRadius: '50%',
        background: color, boxShadow: dotGlow,
      }} />
      {label}
    </span>
  );
}

window.StageLogits = StageLogits;
