// cluster.jsx — Live simulation of GhostLabs's inference cluster
// Renders 4 Mac Studio node cards + streaming server log + aggregate stats.
// Animates in real time: token/sec drift, queue movement, model swaps,
// streaming log entries with realistic event types.

const CL_FONTS = {
  sans: "'Inter Tight', system-ui, sans-serif",
  mono: "'JetBrains Mono', ui-monospace, monospace",
};
const cv = (k) => `var(--sig-${k})`;

// Frontier-class models we'd run on a serious Mac Studio cluster.
// Sizes assume MLX quantization; the M3 Ultra reaches 512GB unified memory,
// so 405B-class models fit comfortably and 685B MoE models at low quant fit too.
const CL_MODELS = [
  ['llama-3.1-405b-instruct',         'MLX-Q4',   '232GB'],
  ['llama-3.3-70b-instruct',          'MLX-fp16', '140GB'],
  ['qwen-3-235b-a22b-instruct',       'MLX-Q4',   '128GB'],
  ['qwen-3-72b-instruct',             'MLX-fp16', '144GB'],
  ['qwen-3-coder-32b',                'MLX-fp16', '64GB'],
  ['deepseek-v3-685b-instruct',       'MLX-Q3',   '342GB'],
  ['deepseek-r1-685b',                'MLX-Q3',   '342GB'],
  ['deepseek-r1-distill-llama-70b',   'MLX-Q5',   '52GB'],
  ['mistral-large-2411-123b',         'MLX-fp16', '246GB'],
  ['mistral-large-3-200b',            'MLX-Q5',   '146GB'],
  ['mixtral-8x22b-instruct',          'MLX-Q5',   '108GB'],
  ['command-r-plus-104b-v2',          'MLX-fp16', '208GB'],
  ['nemotron-ultra-340b',             'MLX-Q4',   '196GB'],
  ['grok-2-314b',                     'MLX-Q4',   '182GB'],
  ['jamba-1.5-large-398b',            'MLX-Q4',   '224GB'],
  ['yi-2-72b-chat',                   'MLX-fp16', '144GB'],
  ['gemma-3-27b-it',                  'MLX-Q8',   '28GB'],
  ['phi-5-mini-30b-instruct',         'MLX-fp16', '60GB'],
];

const CL_PROJECTS = ['jtr','architect-scaler','internal','workshop-idx','nightly-bench','infra','embeddings','research'];

// ─── Real-time anchors ────────────────────────────────────────
// The cluster's identity is anchored to a fixed point in the past, so every
// page load reads a TRUE elapsed time, not a re-rolled random number.
// Per-node boot dates are also fixed so each node's uptime is real.
const CLUSTER_GENESIS = new Date('2025-08-12T03:14:00Z');

const CL_NODES_INIT = [
  { id: 'studio-01', hw: 'M3 Ultra · 512 GB', boot: new Date('2025-09-12T03:42:00Z') },
  { id: 'studio-02', hw: 'M3 Ultra · 384 GB', boot: new Date('2025-11-04T11:17:00Z') },
  { id: 'studio-03', hw: 'M3 Ultra · 256 GB', boot: new Date('2026-01-22T22:08:00Z') },
  { id: 'studio-04', hw: 'M2 Ultra · 192 GB', boot: new Date('2025-10-29T08:55:00Z') },
];

// Diurnal traffic curve — req/hour rises during work hours, dips overnight.
// Two-peak shape: morning ~11, evening ~21, trough ~04. Always real-time.
function diurnalReqPerHour() {
  const h = new Date().getHours() + new Date().getMinutes() / 60;
  const base = 1900;
  const wave = 850 * (Math.sin((h - 5) / 24 * 2 * Math.PI) + 1);
  return Math.round(base + wave);  // ranges ~1900–3600
}

// Cluster lifetime totals — integrate the diurnal curve over time since
// genesis (rough but deterministic enough to feel real).
function totalReqsSinceGenesis() {
  const hours = (Date.now() - CLUSTER_GENESIS.getTime()) / 3600000;
  return Math.round(hours * 2750);  // ≈ average of the diurnal curve
}
function totalTokensSinceGenesis() {
  // ~38 tok/s average × number of running nodes (avg 3.6) × seconds since genesis
  const secs = (Date.now() - CLUSTER_GENESIS.getTime()) / 1000;
  return Math.round(secs * 38 * 3.6);
}
function fmtBigNumber(n) {
  if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
  if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
  if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
  return String(n);
}

// ─── Utilities ─────────────────────────────────────────────────
const rand    = (a, b) => a + Math.random() * (b - a);
const randInt = (a, b) => Math.floor(rand(a, b + 1));
const pick    = (arr) => arr[Math.floor(Math.random() * arr.length)];

function fmtTime(d) {
  const h  = String(d.getHours()).padStart(2,'0');
  const m  = String(d.getMinutes()).padStart(2,'0');
  const s  = String(d.getSeconds()).padStart(2,'0');
  const ms = String(d.getMilliseconds()).padStart(3,'0');
  return `${h}:${m}:${s}.${ms}`;
}

function fmtUptime(min) {
  const d = Math.floor(min / 1440);
  const h = Math.floor((min % 1440) / 60);
  return `${d}d ${h}h`;
}

function makeNode(seed) {
  const [model, quant, size] = pick(CL_MODELS);
  // Uptime is real elapsed time since the node's hardcoded boot date.
  const uptimeMin = Math.max(0, Math.floor((Date.now() - seed.boot.getTime()) / 60000));
  return {
    ...seed,
    model, quant, size,
    ctx: pick([4096, 8192, 16384, 32768]),
    tokSec: rand(28, 60),
    history: Array.from({ length: 12 }, () => rand(0.35, 0.85)),
    queue: randInt(0, 4),
    memPct: randInt(58, 88),
    uptimeMin,
    status: 'RUNNING',
    swapTargetModel: null,
    swapStartedAt: null,
  };
}

// ─── Log event generation ──────────────────────────────────────

const LOG_EVENT_TYPES = [
  'inference.complete', 'inference.complete',  // weighted: common
  'inference.start',
  'request.received', 'request.received',
  'stream.delta', 'stream.delta',
  'cache.hit',
  'embed.batch.done',
  'kv.compact',
  'queue.drain',
  'thermal.normal',
  'websocket.connect',
  'heartbeat',
];

function buildLogEntry(nodes) {
  const node = pick(nodes);
  const project = pick(CL_PROJECTS);
  const evt = pick(LOG_EVENT_TYPES);
  let detail = '';
  let tone = 'dim';
  switch (evt) {
    case 'inference.complete':
      detail = `project=${project}  in=${randInt(40,1800)}  out=${randInt(120,3800)}  t=${rand(0.32,2.4).toFixed(2)}s`;
      tone = 'live'; break;
    case 'inference.start':
      detail = `project=${project}  temp=${rand(0.2,1.0).toFixed(2)}  max=${pick([512,1024,2048,4096])}`;
      tone = 'live'; break;
    case 'request.received':
      detail = `project=${project}  ctx=${randInt(380,8192)}  batch=${randInt(1,8)}`;
      tone = 'dim'; break;
    case 'stream.delta':
      detail = `project=${project}  tokens=${randInt(16,128)}  rate=${rand(28,58).toFixed(1)}t/s`;
      tone = 'dim'; break;
    case 'cache.hit':
      detail = `kv_reuse=${randInt(64,98)}%  saved=${randInt(180,2400)}ms`;
      tone = 'dim'; break;
    case 'embed.batch.done':
      detail = `vecs=${randInt(64,1024)}  dim=1536  t=${rand(0.05,0.42).toFixed(2)}s`;
      tone = 'dim'; break;
    case 'kv.compact':
      detail = `freed=${randInt(120,1800)}MB  ratio=${randInt(11,37)}%`;
      tone = 'dim'; break;
    case 'queue.drain':
      detail = `pending=0  drained_in=${rand(0.4,2.2).toFixed(2)}s`;
      tone = 'live'; break;
    case 'thermal.normal':
      detail = `temp=${randInt(56,71)}C  fan=${randInt(30,52)}%`;
      tone = 'mute'; break;
    case 'websocket.connect':
      detail = `client=workshop-cli  src=*.*.*.${randInt(2,254)}`;
      tone = 'dim'; break;
    case 'heartbeat':
      detail = `cluster=healthy  drift=${randInt(0,4)}ms  nodes=${nodes.length}`;
      tone = 'mute'; break;
  }
  return { ts: new Date(), node: node.id, evt, detail, tone, id: `${Date.now()}-${Math.random().toString(36).slice(2,8)}` };
}

function buildSwapInitEntry(node, newModel) {
  return {
    ts: new Date(), node: node.id, evt: 'model.swap.init',
    detail: `from=${node.model}  target=${newModel[0]}  size=${newModel[2]}`,
    tone: 'amber', id: `${Date.now()}-${Math.random().toString(36).slice(2,8)}`,
  };
}
function buildLoadedEntry(nodeId, newModel) {
  return {
    ts: new Date(), node: nodeId, evt: 'model.loaded',
    detail: `model=${newModel[0]}  rss=${newModel[2]}  warmup=${rand(2.4,8.1).toFixed(1)}s`,
    tone: 'live', id: `${Date.now()}-${Math.random().toString(36).slice(2,8)}`,
  };
}

// ─── Visuals ───────────────────────────────────────────────────
// One-time style injection. The cluster reuses the page's palette CSS vars
// so it themes automatically with the rest of the site.

if (typeof document !== 'undefined' && !document.getElementById('cl-styles')) {
  const s = document.createElement('style'); s.id = 'cl-styles';
  s.textContent = `
    @keyframes cl-cursor { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
    @keyframes cl-fade-in { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: translateY(0); } }
    .cl-log-line { animation: cl-fade-in .22s ease-out; }
    .cl-node { transition: border-color .25s; }
    .cl-node.swapping { border-color: var(--sig-accent-2) !important; }
  `;
  document.head.appendChild(s);
}

function ClusterNode({ node }) {
  const isSwapping = node.status === 'SWAPPING';
  const max = Math.max(...node.history);
  const displayModel = isSwapping ? node.swapTargetModel[0] : node.model;
  const displayMeta  = isSwapping
    ? `loading · ${node.swapTargetModel[1]} · ${node.swapTargetModel[2]}`
    : `${node.quant} · ctx ${node.ctx.toLocaleString()}`;
  return (
    <div className={`cl-node${isSwapping ? ' swapping' : ''}`} style={{
      background: cv('surface'), border: `1px solid ${cv('line')}`,
      padding: '18px 18px 16px', display:'flex', flexDirection:'column', gap:11,
      fontFamily: CL_FONTS.mono, position:'relative', overflow:'hidden', minHeight: 232,
    }}>
      {/* header */}
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center' }}>
        <div style={{ display:'flex', alignItems:'center', gap:9 }}>
          <span className={`sig-dot-pulse ${isSwapping ? 'amber' : ''}`} style={{ width:9, height:9 }} />
          <span style={{ fontSize:12, color: cv('text'), letterSpacing:'0.08em', fontWeight:500, textTransform:'uppercase' }}>{node.id}</span>
        </div>
        <span style={{ fontSize:9, color: cv('text-mute'), letterSpacing:'0.06em' }}>{node.hw}</span>
      </div>

      {/* model name + meta */}
      <div style={{ marginTop:2 }}>
        <div style={{
          fontSize: 14, color: isSwapping ? cv('accent-2') : cv('text'),
          letterSpacing:'-0.005em', fontWeight:500, lineHeight: 1.25,
          opacity: isSwapping ? 0.7 : 1, transition: 'opacity .2s',
          wordBreak: 'break-all',
        }}>{displayModel}</div>
        <div style={{ fontSize:10, color: cv('text-dim'), marginTop:5, letterSpacing:'0.02em' }}>{displayMeta}</div>
      </div>

      {/* mem bar */}
      <div>
        <div style={{ display:'flex', justifyContent:'space-between', marginBottom:5 }}>
          <span style={{ fontSize:9, color: cv('text-mute'), letterSpacing:'0.1em' }}>MEM</span>
          <span style={{ fontSize:10, color: cv('text-dim') }}>{isSwapping ? '— —' : `${node.memPct}%`}</span>
        </div>
        <div style={{ height:4, background: cv('line'), position:'relative' }}>
          <div style={{
            position:'absolute', left:0, top:0, height:'100%',
            width: `${isSwapping ? 18 : node.memPct}%`,
            background: isSwapping ? cv('accent-2') : cv('accent'),
            transition: 'width .8s ease',
          }} />
        </div>
      </div>

      {/* sparkline + tok/s */}
      <div>
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:4 }}>
          <span style={{ fontSize:9, color: cv('text-mute'), letterSpacing:'0.1em' }}>TOK/S</span>
          <span style={{ fontSize:16, color: cv('highlight'), fontWeight:500, letterSpacing:'-0.01em' }}>
            {isSwapping ? '—' : node.tokSec.toFixed(1)}
          </span>
        </div>
        <svg viewBox="0 0 120 24" width="100%" height="22" preserveAspectRatio="none" style={{ display:'block' }}>
          {node.history.map((h, i) => {
            const barH = Math.max(1, (h / max) * 22);
            const baseOpacity = isSwapping ? 0.25 : 0.35 + (i / node.history.length) * 0.55;
            return (
              <rect key={i}
                x={i * 10} y={24 - barH} width={8} height={barH}
                fill={isSwapping ? cv('text-mute') : cv('accent')}
                opacity={baseOpacity}
              />
            );
          })}
        </svg>
      </div>

      {/* footer */}
      <div style={{
        display:'flex', justifyContent:'space-between', alignItems:'center',
        paddingTop:10, marginTop:'auto', borderTop:`1px solid ${cv('line')}`,
      }}>
        <span style={{ fontSize: 10, color: isSwapping ? cv('accent-2') : cv('accent'), letterSpacing:'0.12em', fontWeight:500 }}>
          ● {node.status}
        </span>
        <span style={{ fontSize:10, color: cv('text-mute'), letterSpacing:'0.02em' }}>
          {isSwapping ? 'cold start' : `queue ${node.queue} · up ${fmtUptime(node.uptimeMin)}`}
        </span>
      </div>
    </div>
  );
}

// Renders a single token of a log detail, with light syntax highlighting.
function LogToken({ tok }) {
  if (tok.startsWith('project=')) {
    return (
      <span>
        <span style={{ color: cv('text-mute') }}>project=</span>
        <span style={{ color: cv('highlight') }}>{tok.slice(8)}</span>
      </span>
    );
  }
  if (/^[a-z_]+=/.test(tok)) {
    const eq = tok.indexOf('=');
    return (
      <span>
        <span style={{ color: cv('text-mute') }}>{tok.slice(0, eq+1)}</span>
        <span style={{ color: cv('text-dim') }}>{tok.slice(eq+1)}</span>
      </span>
    );
  }
  return <span>{tok}</span>;
}

function ClusterLog({ entries }) {
  return (
    <div style={{
      background: cv('panel-bg'), border:`1px solid ${cv('line')}`,
      padding:'16px 22px 12px', display:'flex', flexDirection:'column',
      height: 380, overflow:'hidden', fontFamily: CL_FONTS.mono, position:'relative',
    }}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', paddingBottom:12, borderBottom:`1px solid ${cv('line')}`, marginBottom:10 }}>
        <span style={{ fontSize:11, color: cv('text-dim'), letterSpacing:'0.1em' }}>// live · tail -f /var/log/ghostlabs/cluster.log</span>
        <span style={{ fontSize:10, color: cv('accent'), letterSpacing:'0.08em' }}>● STREAMING</span>
      </div>
      <div style={{ flex:1, overflow:'hidden', display:'flex', flexDirection:'column', justifyContent:'flex-end', gap:7 }}>
        {entries.map((e, i) => {
          const ageFromTop = entries.length - 1 - i;
          // newest at bottom (ageFromTop=0), full opacity; older lines fade
          const opacity = ageFromTop > 11 ? 0 : ageFromTop > 8 ? 0.42 : ageFromTop > 5 ? 0.7 : 1;
          const toneColor =
              e.tone === 'live'  ? cv('accent')
            : e.tone === 'amber' ? cv('accent-2')
            : e.tone === 'mute'  ? cv('text-mute')
            : cv('text-dim');
          return (
            <div key={e.id} className="cl-log-line" style={{
              fontSize:11.5, lineHeight:1.5, opacity, transition:'opacity .35s',
              display:'flex', gap:14, color: cv('text-dim'), whiteSpace:'nowrap', overflow:'hidden',
            }}>
              <span style={{ color: cv('text-mute'), flexShrink:0 }}>{fmtTime(e.ts)}</span>
              <span style={{ color: cv('text'), flexShrink:0, width:74, letterSpacing:'0.04em' }}>{e.node}</span>
              <span style={{ color: toneColor, flexShrink:0, width:172, fontWeight:500 }}>{e.evt}</span>
              <span style={{ flex:1, overflow:'hidden', textOverflow:'ellipsis' }}>
                {e.detail.split(/(\s+)/).map((tok, j) => <LogToken key={j} tok={tok} />)}
              </span>
            </div>
          );
        })}
        <div style={{ display:'flex', gap:14, alignItems:'center', marginTop:2 }}>
          <span style={{ fontSize:11.5, color: cv('accent') }}>{'>'}</span>
          <span style={{
            width: 8, height: 13, background: cv('accent'),
            animation: 'cl-cursor 1.1s steps(2) infinite', display:'inline-block',
          }} />
        </div>
      </div>
    </div>
  );
}

function ClusterAggregate({ stats }) {
  return (
    <div className="cl-stats" style={{
      display:'grid', gridTemplateColumns:'repeat(5,1fr)',
      border:`1px solid ${cv('line')}`, background: cv('surface'),
    }}>
      {stats.map(([val, lbl], i) => (
        <div key={i} style={{ padding:'22px 24px', borderRight: i < stats.length-1 ? `1px solid ${cv('line')}` : 'none' }}>
          <div style={{ fontFamily: CL_FONTS.mono, fontSize: 28, color: cv('text'), letterSpacing:'-0.015em', fontWeight:500, lineHeight: 1 }}>{val}</div>
          <div style={{ fontFamily: CL_FONTS.mono, fontSize: 10, color: cv('text-mute'), letterSpacing:'0.16em', textTransform:'uppercase', marginTop:8 }}>{lbl}</div>
        </div>
      ))}
    </div>
  );
}

function SignalCluster() {
  const [nodes, setNodes] = React.useState(() => CL_NODES_INIT.map(makeNode));
  const [log, setLog]     = React.useState([]);

  // Refs so timer callbacks read the latest state without re-binding intervals.
  const nodesRef = React.useRef(nodes);
  React.useEffect(() => { nodesRef.current = nodes; }, [nodes]);

  const addLog = React.useCallback((entry) => {
    setLog(prev => {
      const next = [...prev, entry];
      return next.length > 14 ? next.slice(-14) : next;
    });
  }, []);

  // ── Streaming log entries ────────────────────────────────────
  React.useEffect(() => {
    let alive = true;
    const tick = () => {
      if (!alive) return;
      const runningNodes = nodesRef.current.filter(n => n.status === 'RUNNING');
      if (runningNodes.length > 0) {
        addLog(buildLogEntry(runningNodes));
      }
      setTimeout(tick, randInt(380, 760));
    };
    // Seed the log with a handful of entries up-front
    const seedNodes = nodesRef.current;
    setLog(Array.from({ length: 8 }, () => buildLogEntry(seedNodes)));
    setTimeout(tick, 600);
    return () => { alive = false; };
  }, [addLog]);

  // ── Node stat drift every 1.4s ───────────────────────────────
  React.useEffect(() => {
    const id = setInterval(() => {
      setNodes(prev => prev.map(n => {
        if (n.status === 'SWAPPING') return n;  // swap effect handled separately
        const newTok = Math.max(11, Math.min(74, n.tokSec + rand(-3.6, 3.6)));
        const dq = Math.random();
        const newQueue = Math.max(0, Math.min(8, n.queue + (dq < 0.55 ? 0 : dq < 0.78 ? -1 : 1)));
        const newMem = Math.max(48, Math.min(94, n.memPct + randInt(-2, 2)));
        const newHist = [...n.history.slice(1), newTok / 70];
        return { ...n, tokSec: newTok, queue: newQueue, memPct: newMem, history: newHist, uptimeMin: Math.floor((Date.now() - n.boot.getTime()) / 60000) };
      }));
    }, 1400);
    return () => clearInterval(id);
  }, []);

  // ── Occasional model swap on a random running node ───────────
  React.useEffect(() => {
    const trySwap = () => {
      const candidates = nodesRef.current.filter(n => n.status === 'RUNNING');
      if (candidates.length === 0) return;
      const target = pick(candidates);
      const newModel = pick(CL_MODELS.filter(m => m[0] !== target.model));

      addLog(buildSwapInitEntry(target, newModel));
      setNodes(prev => prev.map(n => n.id === target.id
        ? { ...n, status: 'SWAPPING', swapTargetModel: newModel, swapStartedAt: Date.now() }
        : n));

      const completeIn = randInt(4500, 7500);
      setTimeout(() => {
        setNodes(prev => prev.map(n => {
          if (n.id !== target.id || !n.swapTargetModel) return n;
          const [m, q, sz] = n.swapTargetModel;
          return {
            ...n, status: 'RUNNING',
            model: m, quant: q, size: sz,
            swapTargetModel: null, swapStartedAt: null,
            tokSec: rand(30, 58),
            memPct: randInt(56, 88),
            ctx: pick([4096, 8192, 16384, 32768]),
          };
        }));
        addLog(buildLoadedEntry(target.id, newModel));
      }, completeIn);
    };

    const firstSwap = setTimeout(trySwap, 8500);   // first swap visible early
    const id = setInterval(trySwap, 17000);
    return () => { clearTimeout(firstSwap); clearInterval(id); };
  }, [addLog]);

  // ── Aggregate stats (derived) ────────────────────────────────
  const runningNodes = nodes.filter(n => n.status === 'RUNNING');
  const avgTokSec = runningNodes.length
    ? (runningNodes.reduce((a, n) => a + n.tokSec, 0) / runningNodes.length).toFixed(1)
    : '—';
  const activeJobs = nodes.reduce((a, n) => a + n.queue, 0);
  const utilPct = Math.round((runningNodes.length / nodes.length) * 100 * 0.92);

  // Aggregate stats anchored to real time — stable across reloads.
  const reqPerHourNum = diurnalReqPerHour();
  const reqPerHour = (reqPerHourNum / 1000).toFixed(1) + 'k';
  const uptimeMs = Date.now() - CLUSTER_GENESIS.getTime();
  const uptimeDays = Math.floor(uptimeMs / 86400000);
  const uptimeHours = Math.floor((uptimeMs % 86400000) / 3600000);
  const uptime = `${uptimeDays}d ${uptimeHours}h`;
  const totalReqs   = fmtBigNumber(totalReqsSinceGenesis());
  const totalTokens = fmtBigNumber(totalTokensSinceGenesis());

  // Force a re-render every second so the elapsed-time stats tick visibly.
  const [, forceTick] = React.useState(0);
  React.useEffect(() => {
    const id = setInterval(() => forceTick((x) => x + 1), 1000);
    return () => clearInterval(id);
  }, []);

  return (
    <div>
      <div className="cl-node-grid" style={{ display:'grid', gridTemplateColumns:'repeat(4, 1fr)', gap:14 }}>
        {nodes.map((n) => <ClusterNode key={n.id} node={n} />)}
      </div>
      <div style={{ marginTop:14 }}>
        <ClusterLog entries={log} />
      </div>
      <div style={{ marginTop:14 }}>
        <ClusterAggregate stats={[
          [reqPerHour,    'req / hour · now'],
          [avgTokSec,     'tok/s avg'],
          [activeJobs,    'queued jobs'],
          [`${utilPct}%`, 'cluster util'],
          [uptime,        'cluster uptime'],
        ]} />
      </div>
    </div>
  );
}

window.SignalCluster = SignalCluster;
