/**
 * workforce.js — Workforce view: org chart, missions, dispatch, hire sheet.
 * Two modes: empty state "Your next 10 hires" and populated org chart.
 */

import { S }              from '../state.js';
import { api, post, bustCache, notifyNotReady } from '../api.js';
import { openDrawer }     from '../widgets/drawer.js';
import { register as registerHeartbeat, clear as clearHeartbeats } from '../widgets/heartbeat.js';
import { V }              from '../vocabulary.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;');

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 chipHtml(text,mod='') { return `<span class="chip ${esc(mod)}">${esc(String(text))}</span>`; }
function statusChip(s) {
    const v=String(s||'').toLowerCase();
    if (['done','complete','completed','success','ok','active','running'].includes(v)) return chipHtml(s,'good');
    if (['failed','error','blocked','zombie','dead'].includes(v)) return chipHtml(s,'bad');
    return chipHtml(s);
}

/* ── Active-dispatch state (push-mode) ── */
// Keyed by runId.  Entries are created on dispatch.started and removed on complete/failed/cancelled.
const _dispatches = new Map();
// {runId → {operativeId, status, tokens, costUsd, messages[], filesChanged}}

/* ── SSE dispatch event handler (called from main.js) ── */
export function handleDispatchEvent(evt) {
    const p   = evt.payload ?? evt.data ?? {};
    const rid = p.runId  ?? '';
    const oid = p.operativeId ?? '';
    if (!rid) return;

    const run = _dispatches.get(rid) ?? { runId: rid, operativeId: oid, status: 'queued', tokens: 0, costUsd: 0, messages: [], filesChanged: [] };
    _dispatches.set(rid, run);

    const type = evt.type ?? '';

    if (type === 'dispatch.started') {
        if (oid) _dispatches.delete(`__warmup__:${oid}`);
        run.status    = 'spawning';
        run.invoker   = p.invokerId ?? '';
    } else if (type === 'dispatch.event') {
        const kind = p.kind ?? '';
        if (kind === 'progress') {
            run.status = 'running';
            if (p.message) { run.messages.push(String(p.message).slice(0, 120)); if (run.messages.length > 20) run.messages.shift(); }
        } else if (kind === 'token-usage') {
            run.tokens  = (p.prompt ?? 0) + (p.completion ?? 0);
            // rough blended cost $4/M in + $16/M out
            run.costUsd = Math.round(((p.prompt ?? 0) * 4 + (p.completion ?? 0) * 16) / 1_000_000 * 1000) / 1000;
        } else if (kind === 'file-change') {
            if (p.path && !run.filesChanged.includes(p.path)) run.filesChanged.push(p.path);
        } else if (kind === 'done') {
            run.status = run.filesChanged.length ? 'reviewing' : 'complete';
        } else if (kind === 'error') {
            run.status = 'failed';
            run.error  = p.error ?? 'Unknown error';
        }
    } else if (type === 'dispatch.complete') {
        run.status       = p.success ? 'complete' : 'failed';
        run.tokens       = p.tokensUsed ?? run.tokens;
        run.costUsd      = p.costUsd    ?? run.costUsd;
        run.filesChanged = run.filesChanged.length ? run.filesChanged : new Array(p.filesChanged ?? 0).fill('—');
        run.summary      = p.summary ?? '';
        // Clean up after a short delay so the UI can show the final state.
        setTimeout(() => _dispatches.delete(rid), 30_000);
    } else if (type === 'dispatch.failed') {
        run.status = 'failed';
        run.error  = p.error ?? 'Dispatch failed';
        setTimeout(() => _dispatches.delete(rid), 30_000);
    } else if (type === 'dispatch.cancelled') {
        run.status = 'cancelled';
        setTimeout(() => _dispatches.delete(rid), 5_000);
    }

    // Refresh the drawer if it is showing this operative
    _refreshDrawerForOp(oid);
}

function _refreshDrawerForOp(operativeId) {
    // Only refresh if the drawer is actually showing this operative
    const drawerTitle = document.getElementById('drawer-title');
    const drawerBody  = document.getElementById('drawer-body');
    if (!drawerTitle || !drawerBody) return;
    // Look for the strip placeholder we inserted
    const strip = drawerBody.querySelector(`[data-dispatch-strip]`);
    if (!strip) return;
    // Rebuild the strip for all active dispatches belonging to this operative
    const runs = [..._dispatches.values()].filter(r => r.operativeId === operativeId);
    strip.innerHTML = runs.map(_buildDispatchStrip).join('');
    // Re-attach stop buttons
    strip.querySelectorAll('[data-stop-run]').forEach(btn => {
        btn.addEventListener('click', async () => {
            const runId = btn.dataset.stopRun;
            btn.disabled = true; btn.textContent = 'Stopping…';
            try {
                await fetch(`/api/dispatch/${encodeURIComponent(runId)}/cancel`, { method: 'POST' });
            } catch { /* non-fatal */ }
        });
    });
}

function _buildDispatchStrip(run) {
    const stages = ['queued','spawning','running','reviewing','complete'];
    const cur    = stages.indexOf(run.status);
    const stageHtml = stages.map((s, i) => {
        const cls = i < cur ? 'ds-done' : i === cur ? 'ds-active' : 'ds-pending';
        return `<span class="ds-stage ${esc(cls)}">${esc(s)}</span>`;
    }).join('<span class="ds-sep">→</span>');

    const isFinal = ['complete','failed','cancelled'].includes(run.status);
    const isPendingAdapter = run.pendingAdapter === true;

    return `<div class="dispatch-strip" data-run-id="${esc(run.runId)}">
        <div class="ds-stages">${stageHtml}</div>
        ${run.messages.length ? `<div class="ds-stdout">${esc(run.messages[run.messages.length - 1])}</div>` : ''}
        <div class="ds-meta">
            ${run.invoker ? `<span>${esc(run.invoker)}</span>` : ''}
            ${run.tokens ? `<span>${run.tokens.toLocaleString()} tokens</span>` : ''}
            ${run.costUsd ? `<span>$${run.costUsd.toFixed(4)}</span>` : ''}
            ${run.filesChanged.length ? `<span>${run.filesChanged.length} file(s)</span>` : ''}
        </div>
        ${run.summary ? `<div class="ds-summary">${esc(String(run.summary).slice(0, 160))}</div>` : ''}
        ${run.error   ? `<div class="ds-error">${esc(run.error)}</div>` : ''}
        ${!isFinal && !isPendingAdapter ? `<button class="btn btn-sm ds-stop-btn" data-stop-run="${esc(run.runId)}">Stop</button>` : ''}
    </div>`;
}

/* ── Data loader ── */
export async function load() {
    const [teams, health, disp, appr, assets, healthData, kanban, sortiesRes] = await Promise.all([
        api('/api/synapse/teams', 5000),
        api('/api/synapse/health', 5000),
        api('/api/architects/dispatch', 5000),
        api('/api/synapse/approvals', 5000),
        api('/api/dashboard/surface/assets', 15000),
        api('/api/health', 15000),
        api('/api/workforce/kanban', 5000),
        api('/api/synapse/sorties?limit=40', 5000),
    ]);
    S.synapseTeams  = teams;
    S.synapseHealthRaw = health;
    S.synapseHealth = (Array.isArray(health) ? health : (health?.operatives||[])).map(_norm);
    S.archDispatch  = disp;
    S.approvals     = Array.isArray(appr) ? appr : (appr?.approvals||[]);
    S.assetsSurface = assets;
    S.healthData    = healthData;
    S.wfKanban      = kanban;
    S.sorties       = Array.isArray(sortiesRes?.sorties) ? sortiesRes.sorties : [];
    notifyNotReady([teams, health, disp]);
    render();
}

function _norm(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 };
}

export function render() {
    clearHeartbeats();
    renderOrgChart();
    renderMissions();
    renderDispatch();
    renderSpecialistGrid();
    renderKanban();
    renderSortieTimeline();
}

/* ── Unified workforce kanban ── */
const KANBAN_LANES = ['backlog','ready','claimed','wip','review','blocked','done','failed','cancelled'];
const LANE_LABEL   = { backlog:'Backlog', ready:'Ready', claimed:'Claimed', wip:'In Progress',
                       review:'Review', blocked:'Blocked', done:'Done', failed:'Failed', cancelled:'Cancelled' };
const LANE_CLASS   = { ready:'lane-ready', wip:'lane-wip', review:'lane-review',
                       blocked:'lane-blocked', done:'lane-done', failed:'lane-failed' };

function renderKanban() {
    const el = document.getElementById('workforce-kanban');
    if (!el) return;
    const board = S.wfKanban;
    if (!board || typeof board !== 'object') {
        el.innerHTML = `<div class="kanban-empty kanban-empty-panel">
            <div class="kanban-empty-title">No workforce jobs yet</div>
            <div class="kanban-empty-sub">Synapse and Architects jobs appear here only after a mission creates real work items.</div>
        </div>`;
        return;
    }
    const lanes = board.lanes ?? {};
    const total = board.totalJobs ?? 0;
    if (total === 0) {
        el.innerHTML = `<div class="kanban-empty kanban-empty-panel">
            <div class="kanban-empty-title">No workforce jobs yet</div>
            <div class="kanban-empty-sub">Run a goal or hire a specialist; the board will populate when work is actually dispatched.</div>
        </div>`;
        return;
    }

    const lanesHtml = KANBAN_LANES.map(lane => {
        const cards = lanes[lane] ?? [];
        if (cards.length === 0 && ['cancelled','failed'].includes(lane)) return '';
        const cls = LANE_CLASS[lane] ?? '';
        const MAX_VISIBLE = 8;
        const visible = cards.slice(0, MAX_VISIBLE);
        const overflow = cards.length - MAX_VISIBLE;
        const cardsHtml = visible.map(c => _buildKanbanCard(c)).join('');
        const overflowHtml = overflow > 0
            ? `<div class="kanban-overflow">+${overflow} more</div>`
            : '';
        return `<div class="kanban-lane ${cls}">
            <div class="kanban-lane-hd">
                <span class="kanban-lane-title">${LANE_LABEL[lane] ?? lane}</span>
                <span class="kanban-lane-count">${cards.length}</span>
            </div>
            <div class="kanban-cards">${cardsHtml}${overflowHtml}</div>
        </div>`;
    }).filter(Boolean).join('');

    el.innerHTML = `<div class="kanban-meta">Total: ${total} jobs</div>
        <div class="kanban-scroll">${lanesHtml || '<div class="kanban-empty">No visible lanes for the selected filters.</div>'}</div>`;
}

function _buildKanbanCard(c) {
    const shortId = String(c.id ?? '').slice(0, 8);
    const title   = esc(String(c.title ?? shortId).slice(0, 60));
    const wid     = c.workerId ? `<span class="kc-worker">→ ${esc(String(c.workerId).slice(0, 8))}</span>` : '';
    const pri     = c.priority != null ? `<span class="kc-pri">${c.priority}</span>` : '';
    return `<div class="kanban-card" title="${title}">
        <div class="kc-top">${pri}<span class="kc-id">${shortId}</span></div>
        <div class="kc-title">${title}</div>
        ${wid}
    </div>`;
}

/* ── Sortie timeline (kanban diff + parallel execution view) ── */
function renderSortieTimeline() {
    const el = document.getElementById('sortie-timeline');
    if (!el) return;
    const sorties = S.sorties ?? [];
    if (!sorties.length) {
        el.innerHTML = '<div class="empty"><div class="empty-title">No sortie history yet.</div></div>';
        return;
    }

    // Group into parallel buckets: sorties whose time ranges overlap form one row
    const sorted = [...sorties].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0));
    const buckets = []; // each bucket = array of overlapping sorties
    for (const s of sorted) {
        const start = s.startedAt ?? 0;
        const end   = s.completedAt ?? (start + (s.duration ?? 0));
        let placed  = false;
        for (const bucket of buckets) {
            // overlaps if any existing sortie in bucket overlaps this one
            const overlaps = bucket.some(b => {
                const bs = b.startedAt ?? 0;
                const be = b.completedAt ?? (bs + (b.duration ?? 0));
                return start < be && end > bs;
            });
            if (overlaps) { bucket.push(s); placed = true; break; }
        }
        if (!placed) buckets.push([s]);
    }

    const rows = buckets.map(bucket => {
        const cards = bucket.map(s => {
            const dur   = s.duration ? `${(s.duration / 1000).toFixed(1)}s` : '';
            const files = Array.isArray(s.filesChanged) ? s.filesChanged : [];
            const tok   = s.tokensUsed ? fmtNum(s.tokensUsed) + 't' : '';
            const sc    = s.status === 'complete' ? 'good' : s.status === 'failed' ? 'bad' : '';
            const filesHtml = files.length
                ? `<div class="st-files">${files.slice(0, 5).map(f => `<span class="st-file">${esc(String(f).split('/').pop())}</span>`).join('')}${files.length > 5 ? `<span class="st-file-more">+${files.length - 5}</span>` : ''}</div>`
                : '';
            return `<div class="sortie-card sc-${sc}">
                <div class="sc-top">
                    <span class="sc-id">${esc(String(s.id ?? '').slice(0, 8))}</span>
                    <span class="chip chip-${sc}">${esc(s.status ?? '—')}</span>
                </div>
                <div class="sc-op">${esc(String(s.operativeId ?? '').slice(0, 12))}</div>
                ${filesHtml}
                <div class="sc-meta">${[dur, tok].filter(Boolean).join(' · ')}</div>
            </div>`;
        }).join('');
        const parallel = bucket.length > 1 ? `<span class="st-parallel-badge">⇉ ${bucket.length} parallel</span>` : '';
        return `<div class="sortie-row">${parallel}${cards}</div>`;
    }).join('');

    el.innerHTML = `<div class="sortie-scroll">${rows}</div>`;
}

/* ── Org chart ── */
function _renderOpNode(op) {
    const sc=op.status==='ACTIVE'||op.status==='active'?'active':op.status==='ZOMBIE'||op.status==='dead'?'dead':'';
    const interval=op.sortieIntervalMs||60000, lastAt=op.lastSortieAt||op.heartbeatAt||Date.now();
    const pillId=`op-pill-${esc(op.id||op.operativeId||'')}`;
    return `<div class="org-box" id="${pillId}" data-opid="${esc(op.id||op.operativeId)}" data-opname="${esc(op.role||op.name||'agent')}" data-interval="${interval}" data-lastat="${lastAt}">
        <span class="op-status-dot ${esc(sc)}"></span>${esc(op.role||op.name||'agent')}
    </div>`;
}

function _renderOrgBranch(op, childrenOf, depth) {
    const kids = childrenOf.get(op.id||op.operativeId) || [];
    const kidsHtml = (depth < 3 && kids.length)
        ? `<div class="org-vline"></div><div class="org-row">${kids.map(k => _renderOrgBranch(k, childrenOf, depth+1)).join('')}</div>`
        : '';
    const teamTint = op.team ? `data-team="${esc(op.team)}"` : '';
    return `<div class="org-branch" ${teamTint}>
        <div style="display:flex;flex-direction:column;align-items:center">
            ${_renderOpNode(op)}${kidsHtml}
        </div>
    </div>`;
}

function renderOrgChart() {
    const el=$('org-container'); if (!el) return;
    const ops=Array.isArray(S.synapseHealth) ? S.synapseHealth : [];
    if (!ops.length) {
        const readiness = _getHireReadiness();
        const notes = readiness.notes.map(n => n.text).filter(Boolean).join(' ');
        const title = readiness.blocked ? 'Workforce not attached' : V.empty.workforce;
        const subtitle = readiness.blocked
            ? (notes || 'Synapse is not ready for hires in this dashboard session.')
            : 'Hire a specialist from the panel below; the org chart will appear after a real operative is created.';
        el.innerHTML=`<div class="empty"><div class="empty-title">${esc(title)}</div><div class="empty-sub">${esc(subtitle)}</div></div>`;
        return;
    }

    // Build parent→children map keyed by operative id (null = root).
    const childrenOf = new Map();
    for (const op of ops) {
        const parent = op.reportsToOperativeId || null;
        if (!childrenOf.has(parent)) childrenOf.set(parent, []);
        childrenOf.get(parent).push(op);
    }
    // Operatives with no parent whose parent id doesn't match any known op are also roots.
    const knownIds = new Set(ops.map(o => o.id||o.operativeId));
    for (const op of ops) {
        if (op.reportsToOperativeId && !knownIds.has(op.reportsToOperativeId)) {
            if (!childrenOf.has(null)) childrenOf.set(null, []);
            childrenOf.get(null).push(op);
        }
    }
    const roots = childrenOf.get(null) || ops; // fallback: all ops as roots if no hierarchy

    const tHtml = roots.map(op => _renderOrgBranch(op, childrenOf, 0)).join('');
    el.innerHTML=`<div style="display:flex;flex-direction:column;align-items:center">
        <div class="org-box root-box">Orchestrator</div>
        <div class="org-vline"></div>
        <div class="org-row">${tHtml}</div>
    </div>`;

    // Attach click + heartbeat
    el.querySelectorAll('[data-opid]').forEach(pill => {
        pill.addEventListener('click', () => _openOpDrawer(pill.dataset.opid, pill.dataset.opname));
        registerHeartbeat(pill, Number(pill.dataset.lastat)||Date.now(), Number(pill.dataset.interval)||60000, 'active');
    });
}

/* ── Missions ── */
function renderMissions() {
    const el=$('mission-list'); if (!el) return;
    if (!S.missions.length) { el.innerHTML=`<div class="empty"><div class="empty-title">No active missions</div><div class="empty-sub">Missions appear after a goal dispatches through Synapse.</div></div>`; return; }
    el.innerHTML=S.missions.slice(0,20).map(m=>{
        const icon=m.status==='complete'||m.status==='done'?'✓':m.status==='active'||m.status==='running'?'●':'○';
        const bud=m.budgetUsed!=null&&m.budget!=null?`${fmtNum(m.budgetUsed)}/${fmtNum(m.budget)}t`:'';
        return `<div class="mission-row" data-mid="${esc(m.id||m.missionId)}">
            <span style="color:var(--accent)">${icon}</span>
            <div class="mission-body">
                <div class="mission-title">${esc(m.goal||m.title||'Mission')}</div>
                <div class="mission-meta">${statusChip(m.status)} ${esc(m.operativeId||'')}</div>
            </div>
            ${bud?`<span class="mission-budget">${esc(bud)}</span>`:''}
        </div>`;
    }).join('');
    el.querySelectorAll('[data-mid]').forEach(row => {
        row.addEventListener('click', () => _openMissionDrawer(row.dataset.mid));
    });
}

/* ── Dispatch ── */
function renderDispatch() {
    const el=$('dispatch-panel'); if (!el) return;
    const d=S.archDispatch;
    if (!d||!d.activeWorklists?.length) { el.innerHTML=`<div class="empty"><div class="empty-title">No active dispatch</div><div class="empty-sub">Architects worklists appear when a mission attaches real work items.</div></div>`; return; }
    el.innerHTML=d.activeWorklists.slice(0,10).map(wl=>{
        return `<div class="mission-row">
            <div class="mission-body">
                <div class="mission-title">${esc(wl.name||wl.worklistId||'Worklist')}</div>
                <div class="mission-meta">${statusChip(wl.status)} ${esc(wl.assignedCount||0)} assigned</div>
            </div>
        </div>`;
    }).join('');
}

/* ── Specialist grid ── */
function renderSpecialistGrid() {
    const el=$('specialist-grid'); if (!el) return;
    const surface=S.assetsSurface;
    const specs=(surface?.specialists||[]).slice(0,20);
    if (!specs.length) { el.innerHTML=`<div class="empty"><div class="empty-title">No specialists available</div></div>`; return; }
    el.innerHTML=specs.map(s=>{
        return `<div class="spec-card" data-specid="${esc(s.id)}" data-specname="${esc(s.name||s.id)}">
            <div class="spec-name">${esc(s.name||s.id)}</div>
            <div class="spec-domain">${esc((s.domains||[]).slice(0,3).join(', ')||'—')}</div>
            <button class="btn btn-sm" data-hire="${esc(s.id)}" data-hirename="${esc(s.name||s.id)}">Hire</button>
        </div>`;
    }).join('');
    el.querySelectorAll('[data-hire]').forEach(btn => {
        btn.addEventListener('click', e => {
            e.stopPropagation();
            _showHireSheet(btn.dataset.hire, btn.dataset.hirename);
        });
    });
}

/* ── Hire sheet ── */
function _buildHireSelectors() {
    const ops = (S.synapseHealth || []);
    const teams = [...new Set(ops.map(o => o.team).filter(Boolean))];
    const opOptions = [`<option value="">— none (team lead) —</option>`,
        ...ops.map(o => `<option value="${esc(o.id||o.operativeId)}">${esc(o.role||o.name||o.id)}</option>`)
    ].join('');
    const teamOptions = [`<option value="">— solo —</option>`,
        ...teams.map(t => `<option value="${esc(t)}">${esc(t)}</option>`)
    ].join('');
    const sel = (id, label, opts) =>
        `<label style="display:block;margin-bottom:4px;font-size:var(--text-sm);color:var(--text-muted)">${label}</label>
         <select id="${id}" style="width:100%;background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);padding:6px 10px;font-size:var(--text-sm);color:var(--text-main);margin-bottom:var(--space-4)">${opts}</select>`;
    return { opOptions: sel('hire-reports-to', 'Reports to (optional)', opOptions),
             teamOptions: sel('hire-team', 'Strike team (optional)', teamOptions) };
}

function _getHireReadiness() {
    const synapseState = S.healthData?.runtimeEnvelope?.engines?.synapse ?? {};
    const memoryStorage = S.healthData?.memory?.storage ?? {};
    const notes = [];
    const blocked = Boolean(
        S.synapseHealthRaw?.notReady
        || synapseState.ready === false
        || synapseState.available === false,
    );
    if (blocked) {
        notes.push({ tone: 'bad', text: S.synapseHealthRaw?.reason || synapseState.reason || 'Synapse is not ready for hires in this dashboard session.' });
    }
    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 { blocked, notes };
}

function _formatHireFailure(result) {
    const hint = result?.data?.hint;
    const base = result?.error || 'Hire failed.';
    return hint ? `${base} ${hint}` : base;
}

function _showHireSheet(specialistId, name) {
    const { opOptions, teamOptions } = _buildHireSelectors();
    const readiness = _getHireReadiness();
    const noticesHtml = readiness.notes.length
        ? `<div class="dsec" style="margin-bottom:var(--space-4)">
            ${readiness.notes.map(note => `<div style="padding:8px 10px;border:1px solid ${note.tone === 'bad' ? '#ff5f5733' : '#ffd14d33'};background:${note.tone === 'bad' ? '#ff5f5712' : '#ffd14d12'};color:${note.tone === 'bad' ? 'var(--bad)' : '#ffd14d'};border-radius:8px;font-size:var(--text-sm);margin-bottom:8px">${esc(note.text)}</div>`).join('')}
        </div>`
        : '';
    openDrawer({ title: `Hire ${name}`,
        body: `<div class="dsec">
            <div class="dsec-title">Specialist</div>
            <div style="font-size:var(--text-sm);color:var(--text-muted);margin-bottom:var(--space-4)">${esc(name)}<br><span style="opacity:.6">${esc(specialistId)}</span></div>
            ${noticesHtml}
            ${opOptions}
            ${teamOptions}
            <label style="display:block;margin-bottom:var(--space-2);font-size:var(--text-sm);color:var(--text-muted)">Budget cap (USD)</label>
            <input type="number" id="hire-budget" value="2.00" step="0.50" min="0.50" max="50"
                style="width:100%;background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);padding:6px 10px;font-size:var(--text-sm);color:var(--text-main);margin-bottom:var(--space-4)">
            <button class="btn btn-primary" id="hire-confirm-btn" data-specid="${esc(specialistId)}" data-specname="${esc(name)}" ${readiness.blocked ? 'disabled title="Synapse is not ready"' : ''}>Confirm Hire</button>
            <div id="hire-inline-status" style="margin-top:var(--space-3);font-size:var(--text-sm);color:${readiness.blocked ? 'var(--bad)' : 'var(--text-muted)'}">${readiness.blocked ? esc(readiness.notes[0]?.text || 'Synapse is not ready for hires.') : ''}</div>
        </div>` });

    document.getElementById('hire-confirm-btn')?.addEventListener('click', async btn => {
        if (readiness.blocked) {
            const status = document.getElementById('hire-inline-status');
            if (status) status.textContent = readiness.notes[0]?.text || 'Synapse is not ready for hires.';
            return;
        }
        const specId = btn.target.dataset.specid;
        const specName = btn.target.dataset.specname;
        const budget = parseFloat(document.getElementById('hire-budget')?.value||'2');
        const reportsToVal = (document.getElementById('hire-reports-to')?.value||'').trim()||null;
        const teamVal = (document.getElementById('hire-team')?.value||'').trim()||null;
        const inlineStatus = document.getElementById('hire-inline-status');
        if (inlineStatus) {
            inlineStatus.textContent = readiness.notes.some(note => note.tone === 'warn')
                ? 'Submitting hire request. Fallback storage warning is active.'
                : 'Submitting hire request…';
            inlineStatus.style.color = readiness.notes.some(note => note.tone === 'warn') ? '#ffd14d' : 'var(--text-muted)';
        }
        btn.target.disabled = true;
        btn.target.textContent = 'Hiring…';

        // Optimistic hire: open the success drawer immediately (<5ms), then reconcile.
        const optimisticRecord = { operative: { specialistId: specId, _pending: true }, pricing: null };
        const result = await post('/api/synapse/hire', {
            specialistId: specId,
            budgetCapUsd: budget,
            reportsToOperativeId: reportsToVal,
            strikeTeamId: teamVal,
            fireFirstSortie: true,
        }, { optimistic: optimisticRecord });

        // Immediately open drawer with pending state.
        openDrawer({ title: `${specName} hired`,
            body: `<div class="dsec"><div class="dsec-title">Operative</div>
                <div style="color:var(--accent);font-size:var(--text-sm);margin-bottom:var(--space-4)" id="hire-status-msg">Confirming with server…</div>
                <div class="drow" id="hire-id-row" style="display:none"><span class="drow-k">ID</span><span class="drow-v" id="hire-id-val">—</span></div>
                <div class="drow" id="hire-cost-row" style="display:none"><span class="drow-k">Est. per sortie</span><span class="drow-v" id="hire-cost-val">—</span></div>
                <div class="drow"><span class="drow-k">Reports to</span><span class="drow-v">${esc(reportsToVal || 'team lead')}</span></div>
                <div class="drow"><span class="drow-k">Strike team</span><span class="drow-v">${esc(teamVal || 'solo')}</span></div>
                <div class="drow"><span class="drow-k">Budget cap</span><span class="drow-v">$${budget.toFixed(2)}</span></div>
            </div>` });

        // Reconcile when real response arrives.
        result.realResponse?.then(real => {
            const msg = document.getElementById('hire-status-msg');
            if (real.ok) {
                if (msg) {
                    msg.textContent = readiness.notes.some(note => note.tone === 'warn')
                        ? 'Hired successfully. Fallback storage warning is still active.'
                        : 'Hired successfully.';
                }
                const idRow = document.getElementById('hire-id-row');
                const idVal = document.getElementById('hire-id-val');
                if (idRow && idVal) { idVal.textContent = (real.data?.operative?.id||'').slice(-12)||'—'; idRow.style.display=''; }
                const costRow = document.getElementById('hire-cost-row');
                const costVal = document.getElementById('hire-cost-val');
                if (costRow && costVal && real.data?.pricing?.typical != null) {
                    costVal.textContent = `~$${Number(real.data.pricing.typical).toFixed(3)}`;
                    costRow.style.display = '';
                }
                bustCache('/api/synapse/health');
                setTimeout(load, 800);
                // The backend materializes this hire as a Workforce agent and
                // starts the first sortie when fireFirstSortie is true.
                const operativeId = real.data?.operative?.id || real.data?.operative?.operativeId;
                if (operativeId) {
                    const drawerBody = document.getElementById('drawer-body');
                    if (drawerBody) {
                        let stripDiv = drawerBody.querySelector('[data-dispatch-strip]');
                        if (!stripDiv) {
                            stripDiv = document.createElement('div');
                            stripDiv.setAttribute('data-dispatch-strip', '');
                            drawerBody.appendChild(stripDiv);
                        }
                        const warmupKey = `__warmup__:${operativeId}`;
                        const warmupRun = { runId: warmupKey, operativeId, status: 'queued', tokens: 0, costUsd: 0, messages: ['Adapter pending; waiting for dispatch.started…'], filesChanged: [], pendingAdapter: true };
                        _dispatches.set(warmupKey, warmupRun);
                        stripDiv.innerHTML = _buildDispatchStrip(warmupRun);
                    }
                    const fd = real.data?.firstDispatch;
                    if (fd?.runId) {
                        _dispatches.delete(`__warmup__:${operativeId}`);
                        _dispatches.set(fd.runId, { runId: fd.runId, operativeId, status: 'queued', tokens: 0, costUsd: 0, messages: [], filesChanged: [] });
                    } else if (fd?.queued) {
                        const pending = _dispatches.get(`__warmup__:${operativeId}`);
                        if (pending) pending.messages = ['First sortie queued; adapter start will stream here.'];
                    }
                    _refreshDrawerForOp(operativeId);
                }
            } else {
                if (msg) {
                    msg.textContent = `Hire failed: ${_formatHireFailure(real)}`;
                    msg.style.color = 'var(--bad)';
                }
            }
        }).catch(() => {
            const msg = document.getElementById('hire-status-msg');
            if (msg) { msg.textContent = 'Network error — check daemon status.'; msg.style.color = 'var(--bad)'; }
        });
    });
}

/* ── Drawer helpers ── */
function _openOpDrawer(id, name) {
    const op=S.synapseHealth.find(o=>o.id===id||o.operativeId===id);
    if (!op) { openDrawer({ title: name||'Operative', body:'<div class="empty"><div class="empty-title">No data</div></div>' }); return; }
    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 opId = op.id||op.operativeId||id;
    // Render any active push-mode dispatches for this operative
    const activeRuns = [..._dispatches.values()].filter(r => r.operativeId === opId);
    const stripInner = activeRuns.map(_buildDispatchStrip).join('');
    openDrawer({ title: op.role||op.name||'Operative',
        body: `<div class="dsec"><div class="dsec-title">Operative</div>${drows([
            ['ID',op.id||op.operativeId],['Role',op.role],['Status',op.status],
            ['Budget used',op.budgetUsed!=null?fmtNum(op.budgetUsed):'—'],
            ['Budget alloc',op.budget!=null?fmtNum(op.budget):'—'],
            ['Team',op.team||op.strikeName||'—']
        ])}</div>
        <div class="dsec">
            <div class="dsec-title">Agent control</div>
            <textarea id="agent-control-input" rows="4" placeholder="Ask this agent to inspect, plan, or run a focused task"
                style="width:100%;resize:vertical;background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);padding:8px 10px;color:var(--text-main);font:var(--text-sm) var(--font-sans);margin-bottom:var(--space-3)"></textarea>
            <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
                <button class="btn btn-primary btn-sm" id="agent-control-send">Send to agent</button>
                <span id="agent-control-status" style="font-size:var(--text-sm);color:var(--text-muted)"></span>
            </div>
        </div>
        <div data-dispatch-strip>${stripInner}</div>` });
    // Attach stop button handlers for any already-rendered strips
    const drawerBody = document.getElementById('drawer-body');
    if (drawerBody) {
        drawerBody.querySelectorAll('[data-stop-run]').forEach(btn => {
            btn.addEventListener('click', async () => {
                const runId = btn.dataset.stopRun;
                btn.disabled = true; btn.textContent = 'Stopping…';
                try { await fetch(`/api/dispatch/${encodeURIComponent(runId)}/cancel`, { method: 'POST' }); } catch { /* non-fatal */ }
            });
        });
    }
    const sendBtn = document.getElementById('agent-control-send');
    const input = document.getElementById('agent-control-input');
    const status = document.getElementById('agent-control-status');
    sendBtn?.addEventListener('click', async () => {
        const goal = input?.value?.trim();
        if (!goal) {
            if (status) status.textContent = 'Enter a task first.';
            return;
        }
        sendBtn.disabled = true;
        sendBtn.textContent = 'Sending…';
        if (status) status.textContent = 'Dispatching to agent…';
        const response = await fetch(`/api/workforce/agents/${encodeURIComponent(opId)}/chat`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                goal,
                message: goal,
                specialistId: op.specialistId || op.role || undefined,
                budgetCapUsd: op.budget || 2,
            }),
        }).then(r => r.json().then(data => ({ ok: r.ok, data }))).catch(error => ({ ok: false, data: { error: error?.message || String(error) } }));
        if (response.ok) {
            if (input) input.value = '';
            const runId = response.data?.runId;
            if (runId) _dispatches.set(runId, { runId, operativeId: opId, status: 'queued', tokens: 0, costUsd: 0, messages: [response.data?.message || 'Agent command queued.'], filesChanged: [] });
            if (status) status.textContent = response.data?.mode === 'orchestrator-fallback'
                ? 'Queued through Nexus orchestrator.'
                : 'Queued to agent runtime.';
            _refreshDrawerForOp(opId);
            setTimeout(load, 800);
        } else if (status) {
            status.textContent = response.data?.hint || response.data?.error || 'Agent command failed.';
            status.style.color = 'var(--bad)';
        }
        sendBtn.disabled = false;
        sendBtn.textContent = 'Send to agent';
    });
}

function _openMissionDrawer(id) {
    const m=S.missions.find(x=>x.id===id||x.missionId===id);
    if (!m) { openDrawer({ title:'Mission', body:'<div class="empty"><div class="empty-title">No data</div></div>' }); return; }
    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('');
    openDrawer({ title:'Mission', body:`<div class="dsec"><div class="dsec-title">Mission</div>${drows([
        ['ID',m.id||m.missionId],['Status',m.status],['Goal',m.goal||m.title],
        ['Operative',m.operativeId||'—'],['Budget used',m.budgetUsed!=null?fmtNum(m.budgetUsed):'—']
    ])}</div>` });
}

function _toast(msg, kind='info') {
    const stack=document.getElementById('toast-stack'); if (!stack) return;
    const t=document.createElement('div');
    t.className='toast toast-'+kind; t.textContent=msg;
    stack.appendChild(t);
    setTimeout(()=>{ t.style.transition='opacity 0.2s'; t.style.opacity='0'; setTimeout(()=>t.remove(),220); }, 4000);
}
