/**
 * push-dispatch.ts — Nexus push-mode dispatch engine.
 *
 * Lifecycle for a single push-dispatch run:
 *   1. Resolve invoker (by preferredRuntime or auto-fallback).
 *   2. Build ContextPack from memories + file candidates + cr-graph context.
 *   3. Spawn child agent via invoker.dispatch().
 *   4. Emit DispatchEvents to nexusEventBus (for SSE streaming to dashboard).
 *   5. On completion:  write auto-memory, update operative spend, emit complete.
 *   6. On cancellation / budget breach: kill child, emit failed.
 *
 * Pull mode (IDE → nexus) remains unchanged and coexists.
 */
import { randomUUID } from 'node:crypto';
import { invokerRegistry } from '../../invokers/registry.js';
import { buildContextPack } from './context-pack.js';
import { emitDispatchEvent, emitDispatchComplete } from './event-mapper.js';
import { nexusEventBus } from '../event-bus.js';
// In-process run registry (survives across calls in the same daemon process)
const _runs = new Map();
export async function pushDispatch(opts) {
    const runId = randomUUID();
    // 1. Resolve invoker
    const invoker = await invokerRegistry.resolve(opts.preferredRuntime);
    if (!invoker) {
        const err = 'No headless agent runtime available. Install claude-code, codex, or aider.';
        nexusEventBus.emit('dispatch.failed', { runId, operativeId: opts.operativeId, error: err });
        throw new Error(err);
    }
    // 2. Build context pack
    const contextPack = buildContextPack({
        goal: opts.goal,
        repoRoot: opts.workingDir,
        repoName: opts.repoName,
        memoryLimit: 10,
        fileLimit: 20,
        memories: opts.memories,
        files: opts.files,
        crGraphContext: opts.crGraphContext,
    });
    // 3. Dispatch
    const handle = await invoker.dispatch({
        goal: opts.goal,
        contextPack,
        specialistId: opts.specialistId,
        operativeId: opts.operativeId,
        budgetCapUsd: opts.budgetCapUsd,
        workingDir: opts.workingDir,
        timeoutMs: opts.timeoutMs ?? 600_000,
        runId,
    });
    _runs.set(runId, { ...handle, startedAt: Date.now(), options: opts });
    nexusEventBus.emit('dispatch.started', {
        runId, operativeId: opts.operativeId,
        invokerId: invoker.id, pid: handle.pid,
    });
    // 4. Drain events and relay to bus (non-blocking — runs in background)
    _drainEvents(handle, opts, runId).catch(err => {
        nexusEventBus.emit('dispatch.failed', {
            runId, operativeId: opts.operativeId,
            error: err instanceof Error ? err.message : String(err),
        });
    });
    return { runId, pid: handle.pid, invoker: invoker.id };
}
async function _drainEvents(handle, opts, runId) {
    let spentUsd = 0;
    for await (const event of handle.events) {
        emitDispatchEvent(runId, opts.operativeId, event);
        // Budget enforcement: cancel child if cost exceeds cap
        if (event.kind === 'token-usage') {
            // Rough blended cost at $4/M in + $16/M out
            spentUsd = ((event.prompt * 4 + event.completion * 16) / 1_000_000);
            if (spentUsd >= opts.budgetCapUsd) {
                nexusEventBus.emit('dispatch.budget-exceeded', {
                    runId, operativeId: opts.operativeId,
                    spentUsd, capUsd: opts.budgetCapUsd,
                });
                await handle.cancel();
                return;
            }
        }
    }
    const result = await handle.result;
    // 5a. Write auto-memory
    if (result.summary && opts.storeMemory) {
        await opts.storeMemory(result.summary, 0.4, ['#hire-run', opts.specialistId, opts.repoName]).catch(err => {
            nexusEventBus.emit('mcp.auto-memory.failed', {
                toolName: 'push-dispatch', error: err instanceof Error ? err.message : String(err),
            });
        });
    }
    // 5b. Update operative spend
    if (result.costUsd && opts.updateSpend) {
        await opts.updateSpend(opts.operativeId, result.costUsd).catch(() => { });
    }
    // 5c. Emit completion
    emitDispatchComplete(runId, opts.operativeId, result);
    _runs.delete(runId);
}
/** Cancels a running dispatch by runId. Returns true if found and cancelled. */
export async function cancelDispatch(runId) {
    const entry = _runs.get(runId);
    if (!entry)
        return false;
    await entry.cancel();
    _runs.delete(runId);
    nexusEventBus.emit('dispatch.cancelled', {
        runId, operativeId: entry.options.operativeId,
    });
    return true;
}
/** Returns a snapshot of all in-flight dispatches. */
export function listActiveDispatches() {
    return [..._runs.entries()].map(([runId, entry]) => ({
        runId, operativeId: entry.options.operativeId, startedAt: entry.startedAt,
    }));
}
