/**
 * board.js — Board view: memory pyramid, hero KPIs, kanban, events feed, live strip.
 * PM tweak: pyramid is rendered ABOVE the kanban. The pyramid is the memory aha moment.
 */

import { S, bus }          from '../state.js';
import { api, post, bustCache, notifyNotReady } from '../api.js';
import { openDrawer }      from '../widgets/drawer.js';
import { drawSparkline }   from '../widgets/sparkline.js';
import { renderPyramid }   from '../widgets/pyramid.js';

const $ = id => document.getElementById(id);
const esc = s => s == null ? '' : String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');

/* ── Formatting helpers ── */
function fmtNum(n) {
    if (n == null || isNaN(n)) return '—';
    const v = Number(n);
    if (v >= 1e6)  return (v/1e6).toFixed(1)+'M';
    if (v >= 1000) return (v/1000).toFixed(1)+'K';
    return String(Math.round(v));
}
function fmtTs(ts) {
    if (!ts) return '';
    return new Date(ts).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'});
}
function timeAgo(ts) {
    if (!ts) return '';
    const d = Date.now() - ts;
    if (d < 60000)    return Math.round(d/1000)+'s ago';
    if (d < 3600000)  return Math.round(d/60000)+'m ago';
    if (d < 86400000) return Math.round(d/3600000)+'h ago';
    return Math.round(d/86400000)+'d ago';
}
function animCounter(id, target) {
    const el = $(id); if (!el) return;
    const from = parseFloat(el.dataset.raw||'0')||0;
    const to   = Number(target)||0;
    el.dataset.raw = to;
    if (from === to) return;
    const dur = 800, t0 = performance.now();
    const tick = now => {
        const t = Math.min((now-t0)/dur, 1);
        el.textContent = fmtNum(Math.round(from + (to-from)*(1-Math.pow(1-t,3))));
        if (t < 1) requestAnimationFrame(tick);
    };
    requestAnimationFrame(tick);
}
function chipHtml(text, mod='') {
    return `<span class="chip ${esc(mod)}">${esc(String(text))}</span>`;
}
function miniListHtml(items, limit = 5) {
    const arr = Array.isArray(items) ? items.filter(Boolean) : [];
    if (!arr.length) return '<span class="muted">—</span>';
    const shown = arr.slice(0, limit).map(item => chipHtml(String(item).replace(/^.*\//, ''))).join('');
    const more = arr.length > limit ? chipHtml('+' + (arr.length - limit), 'muted') : '';
    return `<div class="decision-spine-list">${shown}${more}</div>`;
}
function decisionSpineUrl(runId) {
    return '/api/runs/' + encodeURIComponent(runId) + '/decision-spine';
}
function catColor(cat) {
    const m = {memory:'#00d4ff',tokens:'#00ff88',runtime:'#ffd14d',pod:'#b05cff',
               shield:'#ff5f57',mcp:'#52525b',clients:'#a1a1aa',system:'#a1a1aa',
               workflows:'#00d4ff',automations:'#00d4ff',hooks:'#a1a1aa'};
    return m[cat]||'#52525b';
}
function normalizeOperative(op) {
    if (!op || typeof op !== 'object') return op;
    return { ...op, role: op.roleTitle||op.role||op.name||null,
             status: op.state||op.healthState||op.status||'IDLE',
             budget: op.budgetCapUsd??op.budget??null,
             budgetUsed: op.spentUsd??op.budgetUsed??0,
             team: op.strikeTeamId||op.team||op.strikeName||null };
}

/* ── Data loader ── */
export async function load() {
    const [tok, life, op, sh, health, memHealth, runs] = await Promise.all([
        api('/api/tokens/summary'),
        api('/api/tokens/lifetime', 15000),
        api('/api/dashboard/surface/operate', 5000),
        api('/api/synapse/health', 5000),
        api('/api/health', 15000),
        api('/api/memory/health', 15000),
        api('/api/runs?limit=12', 3000),
    ]);
    // Non-blocking: tool health data from ring buffer
    loadToolHealth();
    S.tokensSummary  = tok;
    S.tokensLifetime = life;
    S.operateSurface = op;
    S.synapseHealthRaw = sh;
    S.synapseHealth  = (Array.isArray(sh) ? sh : (sh?.operatives||[])).map(normalizeOperative);
    S.healthData     = health;
    S.memHealth      = memHealth;
    S.runs           = Array.isArray(runs) ? runs : [];
    notifyNotReady([sh]);
    // Prefetch curated specialists for first-run hero (non-blocking)
    if (!S.synapseHealth.length && !S.curatedSpecialists) {
        api('/api/specialists/curated', 60000).then(d => {
            S.curatedSpecialists = Array.isArray(d) ? d : [];
            renderFirstRunHero();
        });
    }
    render();
}

export function render() {
    renderConnectedEcosystem();
    renderPyramidWidget();
    renderHero();
    renderFirstRunHero();
    renderAgentsLiveStrip();
    renderKanban();
    renderEvents();
    renderPulse();
    renderToolHealth();
    renderNeuralStream();
}

/* ── Connected Ecosystem (per-client MCP status, restored from v3.8.0) ── */
const _CLIENT_LABEL = {
    'claude-code': 'Claude Code',
    claude: 'Claude',
    'claude-desktop': 'Claude Desktop',
    codex: 'Codex',
    cursor: 'Cursor',
    opencode: 'Opencode',
    windsurf: 'Windsurf',
    antigravity: 'Antigravity',
    aider: 'Aider',
    continue: 'Continue',
    cline: 'Cline',
    openclaw: 'OpenClaw',
    hermes: 'Hermes',
    nanoclaw: 'NanoClaw',
    picoclaw: 'PicoClaw',
    mcp: 'MCP',
};
function _clientStateColor(state) {
    const s = String(state || '').toLowerCase();
    if (s === 'primaryactive' || s === 'active') return 'var(--accent-good, #4ade80)';
    if (s === 'idle' || s === 'detected') return 'var(--accent-warn, #fbbf24)';
    if (s === 'installed') return 'var(--accent, #60a5fa)';
    return 'var(--muted, #6b7280)';
}
function renderConnectedEcosystem() {
    const host = document.getElementById('connected-ecosystem');
    if (!host) return;
    const list = document.getElementById('ecosystem-list');
    const stamp = document.getElementById('ecosystem-stamp');
    const op = S.operateSurface;
    const clients = (op?.clients?.detected || op?.clients || []);
    const arr = Array.isArray(clients) ? clients : [];
    if (arr.length === 0) {
        host.style.display = 'none';
        return;
    }
    host.style.display = 'block';
    const primaryId = op?.clients?.primary?.clientId || op?.primaryClient?.clientId || null;
    const ordered = [...arr].sort((a, b) => {
        const ap = a.clientId === primaryId ? 0 : 1;
        const bp = b.clientId === primaryId ? 0 : 1;
        if (ap !== bp) return ap - bp;
        return String(a.clientId || '').localeCompare(String(b.clientId || ''));
    });
    const html = ordered.slice(0, 12).map((c) => {
        const id = String(c.clientId || c.id || 'unknown');
        const label = _CLIENT_LABEL[id] || id;
        const isPrimary = id === primaryId;
        const stateRaw = isPrimary ? 'primaryActive' : (c.state || c.status || 'idle');
        const color = _clientStateColor(stateRaw);
        const stateLabel = isPrimary ? 'PRIMARY·ACTIVE' : String(stateRaw).toUpperCase();
        const lastSeen = c.lastHeartbeatAt || c.lastSeenAt;
        const ago = lastSeen ? timeAgo(lastSeen) : '';
        return `<div style="display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:6px;background:var(--surface2);font-size:12px;border-left:3px solid ${color}">
            <span style="width:6px;height:6px;border-radius:50%;background:${color};flex-shrink:0"></span>
            <span style="font-family:var(--font-mono);font-weight:600;color:var(--fg1)">${esc(label)}</span>
            <span style="font-size:10px;color:${color};font-family:var(--font-mono);text-transform:uppercase;letter-spacing:0.5px">${esc(stateLabel)}</span>
            ${ago ? `<span style="margin-left:auto;font-size:10px;color:var(--muted)">${esc(ago)}</span>` : ''}
        </div>`;
    }).join('');
    // Workforce status indicator — surface the v7.6.1 mute as a visible
    // chip with the env-var hint, so users who want autonomous agent hiring
    // know how to re-enable it without reading CLAUDE.md.
    const workforceMuted = (op?.workforce?.disabled === true)
        || ((op?.runtimeEnvelope?.engines?.synapse?.deferred === true)
            && (op?.runtimeEnvelope?.engines?.architects?.deferred === true));
    const workforceChip = workforceMuted
        ? `<div style="display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:6px;background:var(--surface2);font-size:11px;border-left:3px solid var(--muted);opacity:0.85" title="Restart with NEXUS_DISABLE_WORKFORCE=0 to enable Synapse + Architects">
                <span style="width:6px;height:6px;border-radius:50%;background:var(--muted);flex-shrink:0"></span>
                <span style="font-family:var(--font-mono);font-weight:600;color:var(--muted)">Workforce</span>
                <span style="font-size:10px;color:var(--muted);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:0.5px">muted · NEXUS_DISABLE_WORKFORCE=0 to enable</span>
            </div>`
        : '';
    list.innerHTML = html + workforceChip;
    if (stamp) stamp.textContent = `${ordered.length} client${ordered.length === 1 ? '' : 's'}${workforceMuted ? ' · workforce muted' : ''}`;
}

/* ── Neural Stream HUD (live SSE event feed strip, restored from v3.8.0) ── */
const _NEURAL_MAX = 12;
function _neuralColor(category) {
    switch (String(category || '').toLowerCase()) {
        case 'memory': return 'oklch(68% 0.22 295)';
        case 'token':
        case 'tokens': return 'oklch(87% 0.19 152)';
        case 'orchestration':
        case 'planner': return 'oklch(83% 0.14 220)';
        case 'synapse':
        case 'operative':
        case 'mission': return 'oklch(83% 0.17 55)';
        case 'architects':
        case 'dispatch':
        case 'sentinel': return 'oklch(75% 0.20 30)';
        case 'darwin':
        case 'governance': return 'oklch(78% 0.18 340)';
        case 'graph': return 'oklch(80% 0.15 180)';
        default: return 'var(--muted, #6b7280)';
    }
}
export function pushNeuralEvent(evt) {
    if (!evt || typeof evt !== 'object') return;
    if (!Array.isArray(S.neuralStream)) S.neuralStream = [];
    S.neuralStream.unshift({
        type: String(evt.type || ''),
        category: String(evt.category || ''),
        time: Number(evt.time || Date.now()),
        summary: typeof evt.summary === 'string' ? evt.summary : '',
    });
    if (S.neuralStream.length > _NEURAL_MAX) S.neuralStream.length = _NEURAL_MAX;
    renderNeuralStream();
}
function renderNeuralStream() {
    const host = document.getElementById('neural-stream-card');
    if (!host) return;
    const list = document.getElementById('neural-stream-list');
    const count = document.getElementById('neural-stream-count');
    const items = Array.isArray(S.neuralStream) ? S.neuralStream : [];
    if (items.length === 0) {
        host.style.display = 'none';
        return;
    }
    host.style.display = 'block';
    if (count) count.textContent = `${items.length} signal${items.length === 1 ? '' : 's'}`;
    list.innerHTML = items.map((e) => {
        const color = _neuralColor(e.category);
        const ago = timeAgo(e.time);
        return `<div style="display:flex;align-items:baseline;gap:8px;padding:3px 6px;border-left:2px solid ${color};font-size:11px;font-family:var(--font-mono);line-height:1.4">
            <span style="color:${color};font-weight:600;min-width:90px">${esc(e.type)}</span>
            <span style="color:var(--fg1);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(e.summary || e.category || '')}</span>
            <span style="color:var(--muted);font-size:10px;flex-shrink:0">${esc(ago)}</span>
        </div>`;
    }).join('');
}

/* ── Memory pyramid (above kanban) ── */
let _activeTier = null;
function renderPyramidWidget() {
    const container = $('board-pyramid');
    if (!container) return;
    const h = S.memHealth;
    if (!h) { container.innerHTML = ''; return; }
    const tierCounts = h.tierCounts || {};
    const count = (...values) => {
        const positive = values.find(v => Number(v) > 0);
        return positive != null ? Number(positive) : 0;
    };
    const counts = {
        prefrontal:  count(h.working, h.prefrontal, tierCounts.prefrontal),
        hippocampus: count(h.episodic, h.hippocampus, tierCounts.hippocampus),
        cortex:      count(h.semantic, h.cortex, tierCounts.cortex),
    };
    renderPyramid(container, counts, _activeTier, tier => {
        _activeTier = tier;
        renderPyramidWidget();
    });
}

/* ── Hero KPI row ── */
function renderHero() {
    const t=S.tokensSummary, lt=S.tokensLifetime, op=S.operateSurface;
    const saved = Number(
        lt?.savedTokens
        ?? lt?.lifetime?.savedTokens
        ?? lt?.totalSaved
        ?? lt?.lifetime?.saved
        ?? op?.tokenOptimization?.savedTokens
        ?? op?.tokenOptimization?.estimatedSavings
        ?? t?.savedTokens
        ?? t?.saved
        ?? 0,
    );
    animCounter('m-tokens', saved);
    const gross = Number(
        lt?.grossInputTokens
        ?? lt?.lifetime?.grossInputTokens
        ?? op?.tokenOptimization?.grossInputTokens
        ?? t?.gross
        ?? 0,
    );
    const net = Number(t?.net ?? 0);
    const pct = Number(
        lt?.compressionPct
        ?? lt?.lifetime?.compressionPct
        ?? op?.tokenOptimization?.compressionPct
        ?? (gross > 0 && net > 0 ? Math.round((1-net/gross)*100) : 0),
    );
    const pEl = $('m-pct');
    if (pEl) { pEl.innerHTML=`${pct}<sup>%</sup>`; pEl.dataset.raw=pct; }
    const memCount = op?.memory?.total ?? S.memHealth?.total ?? S.memories.length;
    const opsCount = S.synapseHealth.filter(o=>o.status==='ACTIVE'||o.status==='active').length;
    animCounter('m-memories', memCount);
    animCounter('m-ops', opsCount);
    if (!S.spark) S.spark = { tokens:[], pct:[], memories:[], ops:[] };
    const push = (arr, v) => { arr.push(v); if (arr.length>20) arr.shift(); };
    push(S.spark.tokens, saved); push(S.spark.pct, pct);
    push(S.spark.memories, memCount); push(S.spark.ops, opsCount);
    drawSparkline('spark-tokens',   S.spark.tokens,   'oklch(87% 0.19 152)');
    drawSparkline('spark-pct',      S.spark.pct,      'oklch(83% 0.14 220)');
    drawSparkline('spark-memories', S.spark.memories, 'oklch(68% 0.22 295)');
    drawSparkline('spark-ops',      S.spark.ops,      'oklch(83% 0.17 55)');
}

/* ── First-run hero (shown when no operatives are hired yet) ── */
const FIRST_RUN_KEY = 'nexus_first_run_seen';

function getHireReadiness() {
    const notes = [];
    const synapseState = S.healthData?.runtimeEnvelope?.engines?.synapse ?? {};
    const memoryStorage = S.healthData?.memory?.storage ?? {};
    const rawReason = S.synapseHealthRaw?.reason;
    const deferred = synapseState.deferred === true || /deferred\s*\(lazy mode\)/i.test(`${rawReason || ''} ${synapseState.reason || ''}`);
    const unavailable = Boolean(
        (S.synapseHealthRaw?.notReady && !deferred)
        || (synapseState.ready === false && !deferred)
        || (synapseState.available === false && !deferred),
    );
    if (unavailable) {
        notes.push({ tone: 'bad', text: rawReason || synapseState.reason || 'Synapse is not ready for hires in this dashboard session.' });
    }
    if (deferred && !unavailable) {
        notes.push({ tone: 'warn', text: 'Synapse is in lazy mode and will warm up on the first hire.' });
    }
    if (synapseState.fallbackApplied) {
        notes.push({ tone: 'warn', text: synapseState.reason || 'Synapse is running in fallback storage mode.' });
    }
    if (memoryStorage.fallbackApplied) {
        notes.push({ tone: 'warn', text: memoryStorage.fallbackReason || 'Memory storage fallback is active.' });
    }
    return { unavailable, notes };
}

function setFirstRunStatus(message, tone='info') {
    const el = $('frh-status');
    if (!el) return;
    el.textContent = message || '';
    el.style.display = message ? 'block' : 'none';
    el.style.color = tone === 'bad' ? 'var(--bad)' : tone === 'warn' ? '#ffd14d' : 'var(--accent)';
}

function renderFirstRunHero() {
    const el = $('first-run-hero');
    // No placeholder in current HTML — inject it before the live strip if needed
    const parent = $('agents-live-strip')?.parentElement;
    if (!parent) return;

    const hasOps = S.synapseHealth.length > 0;
    const hasRuns = (S.runs || []).length > 0;
    const alreadySeen = (() => { try { return !!localStorage.getItem(FIRST_RUN_KEY); } catch { return false; } })();

    // Remove any existing hero card
    const existing = $('first-run-hero');
    if (existing) existing.remove();

    if (hasOps || (alreadySeen && hasRuns)) return;

    const specs = (S.curatedSpecialists || []).slice(0, 3);
    if (!specs.length) return; // Still loading — will re-render when prefetch resolves
    const readiness = getHireReadiness();
    const noticesHtml = readiness.notes.length
        ? `<div class="frh-notices" style="display:flex;flex-direction:column;gap:8px;margin-bottom:12px">
            ${readiness.notes.map(note => `<div class="chip ${note.tone === 'bad' ? 'bad' : ''}" style="${note.tone === 'bad' ? 'color:var(--bad);border-color:#ff5f5733;background:#ff5f5712' : 'color:#ffd14d;border-color:#ffd14d33;background:#ffd14d12'}">${esc(note.text)}</div>`).join('')}
        </div>`
        : '';

    const card = document.createElement('div');
    card.id = 'first-run-hero';
    card.className = 'first-run-hero card';
    card.innerHTML = `
        <div class="frh-header">
            <div class="frh-title">${hasRuns ? 'Your next hire' : 'Start the first real run'}</div>
            <div class="frh-sub">${hasRuns ? 'Hire a specialist to start running tasks autonomously.' : 'Run a goal from the dashboard or hire a specialist. The run will appear in Board and Context Log.'}</div>
        </div>
        ${noticesHtml}
        <div class="frh-command">
            <input id="frh-goal-input" class="form-input" type="text" placeholder="Inspect this repo and suggest the next fix" autocomplete="off">
            <button class="btn btn-primary btn-sm" id="frh-run-btn">Run goal</button>
            <button class="btn btn-sm" id="frh-context-btn">Open context</button>
        </div>
        <div class="frh-picks">
            ${specs.map(s => `
            <div class="frh-pick" data-specid="${esc(s.specialistId)}" data-specname="${esc(s.name)}">
                <div class="frh-pick-name">${esc(s.name)}</div>
                <div class="frh-pick-desc">${esc((s.description||'').slice(0, 72))}${(s.description||'').length > 72 ? '…' : ''}</div>
                <div class="frh-pick-cost">~$${esc(String(s.pricing?.typical ?? '?'))}/sortie</div>
                <button class="btn btn-primary btn-sm frh-hire-btn" data-specid="${esc(s.specialistId)}" data-specname="${esc(s.name)}" ${readiness.unavailable ? 'disabled title="Synapse is not ready"' : ''}>Hire</button>
            </div>`).join('')}
        </div>
        <div id="frh-status" style="display:none;margin-top:12px;font-size:var(--text-sm)"></div>
        <button class="btn btn-ghost btn-sm frh-dismiss" style="margin-top:var(--space-3)">Dismiss</button>`;

    // Wire buttons before inserting
    card.querySelector('#frh-run-btn')?.addEventListener('click', async () => {
        const input = card.querySelector('#frh-goal-input');
        const button = card.querySelector('#frh-run-btn');
        const goal = (input?.value || `Inspect ${S.workspace?.repoName || 'this repo'} and report the next best action`).trim();
        if (!goal) return;
        if (button) {
            button.disabled = true;
            button.textContent = 'Queueing…';
        }
        setFirstRunStatus('Queueing dashboard run…');
        const result = await post('/api/orchestrate', { goal, source: 'dashboard-onboarding' });
        if (result.ok) {
            setFirstRunStatus('Run queued. Board and Context Log will update as Nexus writes artifacts.');
            bustCache('/api/runs?limit=12');
            bustCache('/api/events');
            setTimeout(load, 900);
        } else {
            setFirstRunStatus(result.error || 'Run failed to queue.', 'bad');
            if (button) {
                button.disabled = false;
                button.textContent = 'Run goal';
            }
        }
    });
    card.querySelector('#frh-context-btn')?.addEventListener('click', () => {
        window.location.hash = '#context-log';
    });
    card.querySelectorAll('.frh-hire-btn').forEach(btn => {
        btn.addEventListener('click', async e => {
            e.stopPropagation();
            if (readiness.unavailable) {
                setFirstRunStatus(readiness.notes[0]?.text || 'Synapse is not ready for hires.', 'bad');
                return;
            }
            btn.disabled = true; btn.textContent = 'Hiring…';
            setFirstRunStatus('Submitting hire request…');
            const result = await post('/api/synapse/hire', {
                specialistId: btn.dataset.specid,
                name: btn.dataset.specname,
                budgetCapUsd: 2,
                fireFirstSortie: true,
            });
            if (result.ok) {
                setFirstRunStatus(readiness.notes.some(note => note.tone === 'warn')
                    ? 'Hired successfully. Fallback storage warning is still active.'
                    : 'Hired successfully.');
                try { localStorage.setItem(FIRST_RUN_KEY, '1'); } catch { /* ignore */ }
                bustCache('/api/synapse/health');
                setTimeout(load, 800);
            } else {
                btn.disabled = false; btn.textContent = 'Hire';
                const failure = result?.data?.hint
                    ? `${result.error}. ${result.data.hint}`
                    : (result.error || 'Hire failed.');
                setFirstRunStatus(failure, 'bad');
                bus.emit('toast', { msg: failure, kind: 'bad' });
            }
        });
    });
    card.querySelector('.frh-dismiss')?.addEventListener('click', () => {
        try { localStorage.setItem(FIRST_RUN_KEY, '1'); } catch { /* ignore */ }
        card.remove();
    });

    // Insert before the live strip
    const strip = $('agents-live-strip');
    strip ? parent.insertBefore(card, strip) : parent.appendChild(card);
}

/* ── Agents live strip ── */
function renderAgentsLiveStrip() {
    const strip = $('agents-live-strip'); if (!strip) return;
    const ops = S.synapseHealth;
    if (!ops.length) { strip.innerHTML=''; return; }
    strip.innerHTML = ops.slice(0,12).map(op => {
        const st = (op.status||'idle').toLowerCase();
        const cls = (st==='active'||st==='running') ? 'active' : (st==='blocked'||st==='zombie'||st==='dead') ? 'blocked' : '';
        const label = esc((op.role||op.name||op.id||'agent').slice(0,20));
        return `<div class="agent-live-pill ${cls}" data-opid="${esc(op.id)}" title="${label}">
            <span class="dot"></span><span>${label}</span></div>`;
    }).join('');
}

/* ── Kanban ── */
function buildKanbanCols() {
    const cols={planning:[],hiring:[],running:[],ghostpass:[],done:[]};
    const op=S.operateSurface;
    if (op) {
        const pc=op.orchestration?.planningContext||op.planningContext;
        if (pc?.goal) cols.planning.push({id:'ctx',goal:pc.goal,status:'planning',tokens:null,time:pc.startedAt,role:null});
        const ws=op.orchestration?.workerPlan?.workers||op.workerPlan?.workers||[];
        for (const w of ws) {
            const st=String(w.status||'').toLowerCase();
            const sg=st==='active'||st==='running'?'running':st==='hiring'?'hiring':st==='complete'||st==='done'?'done':st==='reviewing'||st.includes('ghost')?'ghostpass':null;
            if (sg) cols[sg].push({id:w.id||w.workerId||w.goal,goal:w.goal||w.task||w.approach||'(worker)',status:st,tokens:w.tokensUsed||w.budget,time:w.startedAt||w.createdAt,role:w.role});
        }
    }
    for (const r of (S.runs||[]).slice(0,8)) {
        const runId = r.runId || r.id;
        if (!runId) continue;
        const status = String(r.status || r.state || '').toLowerCase();
        const stage = String(r.stage || '').toLowerCase();
        const lane = status.includes('complete') || status === 'done' || status === 'failed'
            ? 'done'
            : stage.includes('hire')
                ? 'hiring'
                : stage.includes('ghost') || stage.includes('review')
                    ? 'ghostpass'
                    : status === 'running' || stage.includes('orchestrat')
                        ? 'running'
                        : 'planning';
        if (!cols[lane].some(c=>c.id===runId)) {
            cols[lane].push({
                id: runId,
                goal: r.goal||r.mandate||'(run)',
                status: status || stage || 'queued',
                tokens:r.tokensUsed||r.tokenCount,
                time:r.completedAt||r.updatedAt||r.createdAt||r.startedAt,
                role:null,
            });
        }
    }
    return cols;
}

function kcardHtml(c, stage) {
    const tok  = c.tokens ? `<span class="token-badge">${fmtNum(c.tokens)}t</span>` : '';
    const role = c.role   ? chipHtml(c.role) : '';
    const tm   = c.time   ? `<div class="kcard-time">${timeAgo(c.time)}</div>` : '';
    const op   = S.synapseHealth.find(o =>
        (c.role && (o.role===c.role||o.name===c.role)) ||
        (c.id   && (o.id===c.id||o.operativeId===c.id)));
    const opBadge = op
        ? `<span class="agent-inline-badge st-${esc((op.status||'idle').toLowerCase())}" title="${esc(op.role||op.name||'')}">◎ ${esc((op.role||op.name||'').slice(0,18)||'agent')}</span>`
        : '';
    return `<div class="kcard s-${esc(stage)}" data-runid="${esc(String(c.id))}">
        <div class="kcard-goal">${esc(c.goal)}</div>
        <div class="kcard-meta">${role}${opBadge}${tok}</div>${tm}</div>`;
}

function kanbanEmptyHtml(stage) {
    const copy = {
        planning: ['No work in flight', 'Run a goal to see decomposition here.'],
        hiring: ['No hires pending', 'Worker selection appears when a run needs operatives.'],
        running: ['No active workers', 'Dispatched work appears here while it executes.'],
        ghostpass: ['No review pending', 'Ghost-pass waits here only when a patch exists.'],
        done: ['No completed runs yet', 'Finished runs land here with their final state.'],
    }[stage] || ['No work here', 'This lane will populate when the run reaches it.'];
    return `<div class="kb-empty">
        <span class="kb-empty-title">${esc(copy[0])}</span>
        <span class="kb-empty-sub">${esc(copy[1])}</span>
    </div>`;
}

function renderKanban() {
    const cols = buildKanbanCols();
    for (const s of ['planning','hiring','running','ghostpass','done']) {
        const body=$('kb-'+s), cnt=$('kc-'+s); if (!body) continue;
        const cards=cols[s]||[];
        if (cnt) cnt.textContent=String(cards.length);
        body.innerHTML = cards.length ? cards.map(c=>kcardHtml(c,s)).join('') : kanbanEmptyHtml(s);
    }
    // Click delegation
    document.querySelectorAll('.kcard[data-runid]').forEach(el => {
        el.addEventListener('click', () => _openRunDrawer(el.dataset.runid));
    });
}

/* ── Events feed ── */
function renderEvents() {
    const el=$('events-feed'); if (!el||!S.events.length) return;
    el.innerHTML = S.events.slice(0,40).map(e => {
        const dot=`<div class="ev-dot" style="background:${catColor(e.category)}"></div>`;
        const body=`<div class="ev-body"><div class="ev-type">${esc(e.type)}</div>${e.summary?`<div class="ev-summary">${esc(e.summary)}</div>`:''}</div>`;
        return `<div class="ev-item cat-${esc(e.category)}">${dot}${body}<div class="ev-time">${fmtTs(e.time)}</div></div>`;
    }).join('');
}

/* ── System pulse ── */
function renderPulse() {
    const el=$('system-pulse'); if (!el) return;
    const op=S.operateSurface;
    const ops=S.synapseHealth.filter(o=>o.status==='ACTIVE'||o.status==='active').length;
    const mem=op?.memory?.total??S.memories.length;
    const gt=S.tokensSummary?.gross||0, nt=S.tokensSummary?.net||0;
    const eff=gt>0?Math.round((1-nt/gt)*100)+'%':'—';
    const gh=S.worktreeHealth?.pending??0;
    const up=timeAgo(S.startTime).replace(' ago','');
    const rows=[['Synapse',ops?`${ops} active`:'idle'],['Memory',mem?`${fmtNum(mem)} entries`:'—'],['Efficiency',eff],['Ghost-pass',gh?`${gh} pending`:'clear'],['Uptime',up||'—']];
    el.innerHTML=rows.map(([k,v])=>`<div class="row"><span class="row-k">${esc(k)}</span><span class="row-v">${esc(v)}</span></div>`).join('');
}

/* ── Tool health strip ── */
let _toolHealth = [];

export async function loadToolHealth() {
    const data = await api('/api/runtime/tool-health', 10_000);
    if (Array.isArray(data)) {
        _toolHealth = data;
        renderToolHealth();
    }
}

function renderToolHealth() {
    const card = $('tool-health-card');
    const list = $('tool-health-list');
    const stamp = $('tool-health-stamp');
    if (!card || !list) return;
    if (!_toolHealth.length) { card.style.display = 'none'; return; }
    card.style.display = '';
    if (stamp) stamp.textContent = 'updated ' + new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'});

    const colorMap = { green: 'var(--accent)', amber: '#ffd14d', red: '#ff5f57' };
    list.innerHTML = _toolHealth.map(t => {
        const col = colorMap[t.status] || 'var(--muted)';
        const errPct = Math.round((t.errorRate || 0) * 100);
        const p95 = t.p95Ms >= 1000 ? (t.p95Ms / 1000).toFixed(1) + 's' : t.p95Ms + 'ms';
        const pill = errPct > 0 ? `<span style="background:#ff5f5722;color:#ff5f57;border-radius:4px;padding:1px 5px;font-size:10px">${errPct}% err</span>` : '';
        return `<div style="display:flex;align-items:center;gap:6px;padding:4px 8px;border-radius:6px;background:var(--surface2);font-size:12px">
            <span style="width:8px;height:8px;border-radius:50%;background:${col};flex-shrink:0"></span>
            <span style="font-family:var(--font-mono);color:var(--fg1)">${esc(t.toolName.replace('nexus_',''))}</span>
            <span style="color:var(--muted)">${p95}</span>${pill}
        </div>`;
    }).join('');
}

/* ── Run drawer helper ── */
function loadDecisionSpine(runId) {
    if (!runId) return Promise.resolve(null);
    return api(decisionSpineUrl(runId), 0).then(spine => {
        if (spine?.artifacts) {
            S.lastDecisionSpine = spine;
            renderOrchestrationPipeline();
            return spine;
        }
        return null;
    });
}

function _openRunDrawer(runId) {
    const run = S.runs.find(r=>r.id===runId||r.runId===runId);
    if (!run) {
        openDrawer({ title: 'Run '+runId, body: '<div class="empty"><div class="empty-title">Loading…</div></div>' });
        api('/api/runs/'+encodeURIComponent(runId),0).then(d=>{
            const b=document.getElementById('drawer-body');
            if(d && b) b.innerHTML=_buildRunHtml(d);
            loadDecisionSpine(runId).then(spine => { if (d && spine && b) b.innerHTML=_buildRunHtml({...d, decisionSpine: spine}); });
        });
        return;
    }
    openDrawer({ title: 'Run '+String(runId).slice(-6), body: _buildRunHtml(run) });
    loadDecisionSpine(runId).then(spine => {
        const b=document.getElementById('drawer-body');
        if (spine && b) b.innerHTML=_buildRunHtml({...run, decisionSpine: spine});
    });
}

function _buildRunHtml(r) {
    const drows = rows => rows.map(([k,v])=>`<div class="drow"><span class="drow-k">${esc(k)}</span><span class="drow-v">${esc(String(v??'—'))}</span></div>`).join('');
    const op = S.synapseHealth.find(o => o.currentRunId===(r.id||r.runId)||(r.goal&&(o.goal===r.goal||(o.role&&r.goal?.toLowerCase().includes(o.role?.toLowerCase())))));
    const opSection = op ? `<div class="dsec"><div class="dsec-title">Operative</div>${drows([['Name',op.name||op.role||op.id],['Status',op.status||'—'],['Budget used',op.budgetUsed!=null?fmtNum(op.budgetUsed)+'t':'—'],['Team',op.team||op.strikeName||'—']])}</div>` : '';
    const tokDisplay = r.tokensUsed ? `<span style="color:var(--accent);font-family:var(--font-mono);font-size:var(--text-lg);font-weight:var(--weight-semibold)">${fmtNum(r.tokensUsed)}t</span>` : '—';
    const spineSection = r.decisionSpine ? _buildDecisionSpineHtml(r.decisionSpine) : '';
    return `<div class="dsec"><div class="dsec-title">Run details</div>${drows([['ID',(r.id||r.runId||'').slice(-12)],['Status',r.status],['Goal',r.goal||r.mandate],['Created',r.createdAt?new Date(r.createdAt).toLocaleString():'—'],['Completed',r.completedAt?new Date(r.completedAt).toLocaleString():'—']])}</div>
    <div class="dsec"><div class="dsec-title">Token cost</div><div style="padding:var(--space-2) 0">${tokDisplay}</div></div>${spineSection}${opSection}`;
}

function _buildDecisionSpineHtml(spine) {
    const artifacts = spine?.artifacts || {};
    const brief = artifacts.requestBrief || {};
    const plan = artifacts.selectionPlan || {};
    const contextLog = Array.isArray(artifacts.contextLog) ? artifacts.contextLog : [];
    const decisionLog = Array.isArray(artifacts.decisionLog) ? artifacts.decisionLog : [];
    const selected = spine?.summary?.selected || plan.selected || {};
    const model = spine?.summary?.modelRoute || plan.modelRoute || brief.modelPolicy || {};
    const budgets = spine?.summary?.budgets || plan.budgets || {};
    const executionPolicy = spine?.summary?.executionPolicy || plan.executionPolicy || {};
    const agentFlow = spine?.summary?.agentFlow || executionPolicy.agentFlow || {};
    const latestDecision = spine?.summary?.latestDecision || decisionLog[decisionLog.length - 1] || null;
    const prompt = brief.rewrittenPrompt || brief.rawPrompt || '';
    return `<div class="dsec decision-spine">
        <div class="dsec-title">Decision spine <a class="decision-spine-full-link" href="#context-log">Open full log</a></div>
        ${prompt ? `<div class="decision-spine-prompt">${esc(prompt)}</div>` : ''}
        <div class="decision-spine-grid">
            <div class="decision-spine-card">
                <div class="decision-spine-k">Intent</div>
                <div class="decision-spine-v">${esc(brief.intent || plan.intent || spine?.summary?.intent || '—')}</div>
                <div class="decision-spine-sub">${esc(brief.risk || spine?.summary?.risk || 'risk: —')}</div>
            </div>
            <div class="decision-spine-card">
                <div class="decision-spine-k">Model route</div>
                <div class="decision-spine-v">${esc(model.workerTier || '—')}</div>
                <div class="decision-spine-sub">${esc(model.reviewerTier ? 'review ' + model.reviewerTier : model.reason || '—')}</div>
            </div>
            <div class="decision-spine-card">
                <div class="decision-spine-k">Budget</div>
                <div class="decision-spine-v">${fmtNum(budgets.totalTokens || 0)} t</div>
                <div class="decision-spine-sub">${fmtNum(budgets.codeBlocks || budgets.codeBlockPolicy?.reservedTokens || 0)} t code blocks</div>
            </div>
            <div class="decision-spine-card">
                <div class="decision-spine-k">Context</div>
                <div class="decision-spine-v">${fmtNum(contextLog.length)} events</div>
                <div class="decision-spine-sub">${fmtNum(decisionLog.length)} decisions</div>
            </div>
        </div>
        <div class="decision-spine-block"><span>Files</span>${miniListHtml(selected.files || plan.candidates?.files || [])}</div>
        <div class="decision-spine-block"><span>Skills</span>${miniListHtml(selected.skills || plan.candidates?.skills || [])}</div>
        <div class="decision-spine-block"><span>Workflows</span>${miniListHtml(selected.workflows || plan.candidates?.workflows || [])}</div>
        <div class="decision-spine-block"><span>Crew</span>${miniListHtml([...(selected.crews || []), ...(selected.specialists || [])], 6)}</div>
        <div class="decision-spine-block"><span>Workers</span>${miniListHtml(selected.workers || executionPolicy.subagents?.map(agent => `${agent.role}:${agent.modelTier}`) || [], 8)}</div>
        ${_buildDecisionSpineBudgetHtml(budgets)}
        ${_buildAgentFlowHtml(agentFlow)}
        ${latestDecision ? `<div class="decision-spine-latest">${esc(latestDecision.verb || 'decision')} · ${esc(latestDecision.decision || '')}</div>` : ''}
        ${_buildDecisionSpineLogBrowser(contextLog, decisionLog)}
    </div>`;
}

function _buildDecisionSpineBudgetHtml(budgets) {
    if (!budgets || typeof budgets !== 'object' || !Object.keys(budgets).length) return '';
    const rows = [
        ['Prompt', budgets.prompt],
        ['Memory', budgets.memory],
        ['Repo', budgets.repo],
        ['Skills', budgets.skills],
        ['Reasoning', budgets.reasoning],
        ['Code blocks', budgets.codeBlocks || budgets.codeBlockPolicy?.reservedTokens],
        ['Verification', budgets.verification],
        ['Reserve', budgets.reserve],
    ].filter(([, value]) => Number(value) > 0);
    const templates = budgets.codeBlockPolicy?.templates || [];
    return `<div class="decision-spine-budget-grid" aria-label="Token budget breakdown">
        ${rows.map(([label, value]) => `<div><span>${esc(label)}</span><strong>${fmtNum(value)}t</strong></div>`).join('')}
        ${templates.length ? `<div class="decision-spine-budget-wide"><span>Code block templates</span>${miniListHtml(templates, 6)}</div>` : ''}
    </div>`;
}

function _buildAgentFlowHtml(agentFlow) {
    const stages = Array.isArray(agentFlow?.stages) ? agentFlow.stages : [];
    if (!stages.length) return '';
    const gates = Array.isArray(agentFlow.deterministicGates) ? agentFlow.deterministicGates : [];
    return `<div class="decision-spine-agentflow" aria-label="AgentFlow pipeline">
        <div class="decision-spine-browser-title">AgentFlow pipeline</div>
        <div class="decision-spine-agentflow-rail">
            ${stages.map(stage => `<div class="decision-spine-agentflow-stage">
                <span>${esc(stage.stage)}</span>
                <strong>${esc(stage.ownerRole || 'worker')}</strong>
                <small>${esc(stage.modelTier || '')}</small>
            </div>`).join('')}
        </div>
        <div class="decision-spine-log-ref">${esc(agentFlow.dispatchPolicy || '')}</div>
        ${gates.length ? `<div class="decision-spine-block"><span>Gates</span>${miniListHtml(gates, 8)}</div>` : ''}
    </div>`;
}

function _buildDecisionSpineLogBrowser(contextLog, decisionLog) {
    const contextRows = contextLog.slice(0, 6).map(entry => `
        <div class="decision-spine-log-row">
            <div class="decision-spine-log-head">
                <span>${esc(entry.phase || 'context')}</span>
                <span>${esc(entry.actor || 'system')}</span>
            </div>
            <div class="decision-spine-log-reason">${esc(entry.reason || '—')}</div>
            ${miniListHtml([...(entry.contextRefs || []), ...(entry.fileRefs || []), ...(entry.evidenceRefs || [])], 7)}
        </div>`).join('');
    const decisionRows = decisionLog.slice(0, 6).map(entry => `
        <div class="decision-spine-log-row">
            <div class="decision-spine-log-head">
                <span>${esc(entry.verb || 'decision')}</span>
                <span>${esc(entry.surface || entry.actor || 'runtime')}</span>
            </div>
            <div class="decision-spine-log-reason">${esc(entry.decision || '—')}</div>
            <div class="decision-spine-log-ref">${esc(entry.reason || '')}</div>
        </div>`).join('');
    if (!contextRows && !decisionRows) return '';
    return `<div class="decision-spine-browser">
        <div class="decision-spine-browser-col">
            <div class="decision-spine-browser-title">Context log</div>
            ${contextRows || '<div class="decision-spine-empty">—</div>'}
        </div>
        <div class="decision-spine-browser-col">
            <div class="decision-spine-browser-title">Decision log</div>
            ${decisionRows || '<div class="decision-spine-empty">—</div>'}
        </div>
    </div>`;
}

function _buildDecisionSpineMiniHtml(spine) {
    const artifacts = spine?.artifacts || {};
    const brief = artifacts.requestBrief || {};
    const plan = artifacts.selectionPlan || {};
    const selected = spine?.summary?.selected || plan.selected || {};
    const model = spine?.summary?.modelRoute || plan.modelRoute || brief.modelPolicy || {};
    const budgets = spine?.summary?.budgets || plan.budgets || {};
    const agentFlow = spine?.summary?.agentFlow || plan.executionPolicy?.agentFlow || {};
    return `<div class="decision-spine-mini">
        <div class="decision-spine-mini-head">Decision Spine · run ${esc((spine.runId || '').slice(-8))}</div>
        <div class="decision-spine-mini-row">
            <span>intent ${esc(brief.intent || plan.intent || spine?.summary?.intent || '—')}</span>
            <span>model ${esc(model.workerTier || '—')}</span>
            <span>budget ${fmtNum(budgets.totalTokens || 0)}t</span>
            <span>flow ${fmtNum(agentFlow.stages?.length || 0)}</span>
            <span>ctx ${fmtNum(spine?.summary?.contextEvents || artifacts.contextLog?.length || 0)}</span>
            <span>decisions ${fmtNum(spine?.summary?.decisions || artifacts.decisionLog?.length || 0)}</span>
        </div>
        <div class="decision-spine-mini-lists">
            ${miniListHtml(selected.skills || plan.candidates?.skills || [], 4)}
            ${miniListHtml(selected.files || plan.candidates?.files || [], 4)}
        </div>
    </div>`;
}

/* ─── Orchestration pipeline (decomposition + completion live signal) ─── */

/**
 * Stash the latest orchestration.decomposed / orchestration.completed events
 * into S so renderOrchestrationPipeline can paint a real-time view of the
 * crew/specialists/skills/files Nexus picked for the current run.
 */
export function handleOrchestrationEvent(evt) {
    if (!evt || typeof evt.type !== 'string') return;
    const payload = evt.payload || evt;
    if (evt.type === 'orchestration.decomposed') {
        S.lastDecomposition = { ...payload, ts: evt.time || Date.now() };
        loadDecisionSpine(payload.runId || payload.id);
    } else if (evt.type === 'orchestration.completed') {
        S.lastCompletion = { ...payload, ts: evt.time || Date.now() };
        loadDecisionSpine(payload.runId || payload.id);
    }
    renderOrchestrationPipeline();
}

function renderOrchestrationPipeline() {
    const host = document.getElementById('orchestration-pipeline');
    if (!host) return;
    const dec = S.lastDecomposition;
    const cmp = S.lastCompletion;
    const spine = S.lastDecisionSpine;
    if (!dec && !cmp && !spine) {
        host.style.display = 'none';
        return;
    }
    host.style.display = 'block';
    const same = dec && cmp && dec.runId === cmp.runId;
    const completionState = (completion) => {
        const state = completion?.state || '';
        const result = String(completion?.result || '');
        if (state === 'failed' && /without a repository patch|no applicable diff|no selected mutation bindings|advisory/i.test(result)) {
            return 'inspected';
        }
        return state;
    };
    const stateColor = (state) => state === 'merged' || state === 'inspected' ? 'var(--accent-good, #4ade80)'
        : state === 'rolled_back' ? 'var(--accent-warn, #fbbf24)'
        : state === 'failed' ? 'var(--accent-bad, #ff5f57)'
        : 'var(--muted)';
    const chip = (label, value) => `<span style="display:inline-block;padding:2px 8px;margin:2px 4px 2px 0;border-radius:10px;background:var(--surface2);font-size:11px;color:var(--fg1)"><span style="color:var(--muted)">${esc(label)}</span> ${esc(String(value ?? '—'))}</span>`;
    const chipList = (label, items) => {
        const arr = Array.isArray(items) ? items : [];
        if (!arr.length) return '';
        const head = arr.slice(0, 6).map(esc).join(', ');
        const more = arr.length > 6 ? ` <span style="color:var(--muted)">+${arr.length - 6}</span>` : '';
        return `<div style="margin:4px 0"><span style="color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:0.5px">${esc(label)}</span> <span style="font-family:var(--font-mono);font-size:12px">${head}${more}</span></div>`;
    };
    const decBlock = dec ? `
        <div style="border-left:3px solid var(--accent);padding:8px 12px;margin-bottom:8px">
            <div style="font-size:13px;font-weight:600;margin-bottom:4px">Decomposition · run ${esc((dec.runId || '').slice(-8))}</div>
            <div style="font-size:12px;color:var(--muted);margin-bottom:6px">${esc(dec.goal || '')}</div>
            <div>${chip('intent', dec.intent || 'auto')}${chip('crew', dec.crew || 'baseline')}${chip('workers', dec.workers ?? 0)}${chip('phases', dec.phases ?? 0)}</div>
            ${chipList('specialists', dec.specialists)}
            ${chipList('skills', dec.skills)}
            ${chipList('files', dec.files)}
        </div>` : '';
    const cmpState = completionState(cmp);
    const cmpBlock = cmp ? `
        <div style="border-left:3px solid ${stateColor(cmpState)};padding:8px 12px">
            <div style="font-size:13px;font-weight:600;margin-bottom:4px">Completion · run ${esc((cmp.runId || '').slice(-8))} · <span style="color:${stateColor(cmpState)}">${esc(cmpState || '')}</span></div>
            <div style="font-size:12px;color:var(--muted);margin-bottom:6px">${esc(cmp.result || '')}</div>
            <div>${chip('verified', `${cmp.verifiedWorkers ?? 0}/${cmp.totalWorkers ?? 0}`)}${chip('saved', `${fmtNum(cmp.savedTokens ?? 0)} t`)}${chip('compression', `${cmp.compressionPct ?? 0}%`)}${chip('duration', `${Math.round((cmp.durationMs ?? 0) / 100) / 10}s`)}</div>
        </div>` : '';
    const spineBlock = spine ? _buildDecisionSpineMiniHtml(spine) : '';
    const headerNote = same ? '' : (dec && cmp ? '<div style="font-size:11px;color:var(--muted);margin-bottom:6px">Showing latest decomposition + most recent completion (different runs)</div>' : '');
    host.innerHTML = `
        <div class="shd" style="margin-bottom:8px">Orchestration Pipeline</div>
        ${headerNote}
        ${decBlock}
        ${cmpBlock}
        ${spineBlock}
    `;
}
