/**
 * MCP tool dispatch router (Phase 3 split).
 *
 * Extracted from mcp.ts to keep each file under 1500 LOC.
 * Receives a fully-populated McpHandlerCtx and routes to the
 * correct handler group based on tool name.
 *
 * No business logic lives here — this is a pure routing table.
 */
import { handleOrchestrationGroup } from './handlers/orchestration.js';
import { handleMemoryGroup } from './handlers/memory.js';
import { handleGovernanceGroup } from './handlers/governance.js';
import { handleRuntimeGroup } from './handlers/runtime.js';
import { handleDiscoveryGroup } from './handlers/discovery.js';
import { handleMiscGroup } from './handlers/misc.js';
import { handleWorkforceGroup } from './handlers/workforce.js';
import { nexusEventBus } from '../../../engines/event-bus.js';
import { recordToolInvocation } from './tool-health.js';
import { getAsyncGate, withAsyncGate } from './async-gate.js';
import { getCircuitManager, checkBackpressure } from './circuit.js';
import { completeRun, createRun, failRun, updateStage } from '../../../engines/orchestrator/store.js';
import { buildSelectionPlan } from '../../../engines/orchestrator/decision-spine.js';
function summarizeAsyncMcpResult(toolName, result) {
    const text = result?.content
        ?.map((part) => part?.type === 'text' ? String(part.text ?? '') : '')
        .filter(Boolean)
        .join('\n')
        .trim();
    const firstLine = text?.split('\n').find((line) => line.trim())?.trim();
    return (firstLine ? `${toolName}: ${firstLine}` : `${toolName} completed asynchronously`).slice(0, 1024);
}
/** Tools that may exceed the MCP client timeout (~60s). Wrap in async gate. */
const SLOW_TOOLS = new Set([
    'nexus_orchestrate',
    'nexus_plan_execution',
    'nexus_ghost_pass',
    'nexus_spawn_workers',
    'nexus_optimize_tokens',
    'nexus_search',
    'nexus_assemble_context',
    'nexus_graph_query',
    'nexus_session_search',
    'nexus_memory_export',
    'nexus_memory_backup',
    'nexus_memory_import',
    'nexus_memory_hygiene',
    'nexus_memory_audit',
    'nexus_pattern_search',
    'nexus_knowledge_provenance',
    'nexus_rag_ingest_collection',
    'nexus_workflow_run',
    'nexus_automation_run',
    'nexus_doctor',
    'nexus_autofix',
    'nexus_hypertune_max',
]);
/** Estimated wall-clock duration for each slow tool (ms). */
const TOOL_ETA_MS = {
    nexus_orchestrate: 60_000,
    nexus_plan_execution: 30_000,
    nexus_ghost_pass: 45_000,
    nexus_spawn_workers: 30_000,
    nexus_optimize_tokens: 10_000,
    nexus_search: 20_000,
    nexus_assemble_context: 20_000,
    nexus_graph_query: 30_000,
    nexus_session_search: 20_000,
    nexus_memory_export: 20_000,
    nexus_memory_backup: 30_000,
    nexus_memory_import: 30_000,
    nexus_memory_hygiene: 30_000,
    nexus_memory_audit: 30_000,
    nexus_pattern_search: 20_000,
    nexus_knowledge_provenance: 20_000,
    nexus_rag_ingest_collection: 45_000,
    nexus_workflow_run: 45_000,
    nexus_automation_run: 45_000,
    nexus_doctor: 20_000,
    nexus_autofix: 45_000,
    nexus_hypertune_max: 45_000,
};
const DEFAULT_MAX_SYNC_MS = 2000;
const ORCHESTRATE_DEFAULT_MAX_SYNC_MS = 15_000;
const MAX_CLIENT_SYNC_WAIT_MS = 45_000;
function isTruthyFlag(value) {
    if (value === true)
        return true;
    if (typeof value === 'number')
        return value > 0;
    if (typeof value === 'string')
        return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
    return false;
}
function coerceBoundedWaitMs(value) {
    const numeric = Number(value);
    if (!Number.isFinite(numeric) || numeric < 0)
        return undefined;
    return Math.min(MAX_CLIENT_SYNC_WAIT_MS, Math.floor(numeric));
}
function shouldReturnQueuedReceipt(toolName, args) {
    if (!SLOW_TOOLS.has(toolName))
        return false;
    if (toolName === 'nexus_orchestrate') {
        return !isTruthyFlag(args.inline)
            && !isTruthyFlag(args.sync)
            && !isTruthyFlag(args.blocking)
            && !isTruthyFlag(args.wait);
    }
    return isTruthyFlag(args.background)
        || isTruthyFlag(args.async)
        || isTruthyFlag(args.queue)
        || isTruthyFlag(args.detach);
}
function resolveMaxSyncMs(toolName, args) {
    const explicitWait = isTruthyFlag(args.wait)
        || isTruthyFlag(args.inline)
        || isTruthyFlag(args.sync)
        || isTruthyFlag(args.blocking);
    return coerceBoundedWaitMs(args.waitMs)
        ?? coerceBoundedWaitMs(args.maxSyncMs)
        ?? (toolName === 'nexus_orchestrate' && explicitWait ? MAX_CLIENT_SYNC_WAIT_MS : undefined)
        ?? (toolName === 'nexus_orchestrate' ? ORCHESTRATE_DEFAULT_MAX_SYNC_MS : DEFAULT_MAX_SYNC_MS);
}
function asStringList(value) {
    if (Array.isArray(value)) {
        return value
            .map((item) => typeof item === 'string' ? item : (item && typeof item === 'object' ? String(item.name ?? item.id ?? '') : String(item ?? '')))
            .map((item) => item.trim())
            .filter(Boolean);
    }
    if (typeof value === 'string') {
        return value.split(',')
            .map((item) => item.trim())
            .filter(Boolean);
    }
    return [];
}
function uniqueList(items, max = 8) {
    const seen = new Set();
    const out = [];
    for (const item of items) {
        const value = String(item ?? '').trim();
        if (!value || seen.has(value))
            continue;
        seen.add(value);
        out.push(value);
        if (out.length >= max)
            break;
    }
    return out;
}
function extractLinkedSelectors(prompt, prefix) {
    const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const pattern = new RegExp(`\\[${escaped}([^\\]\\s]+)\\]`, 'g');
    return Array.from(prompt.matchAll(pattern))
        .map((match) => match[1]?.trim())
        .filter(Boolean);
}
function inferQueuedOrchestratePreview(args, runId) {
    const prompt = String(args.prompt ?? args.goal ?? '');
    const lower = prompt.toLowerCase();
    const files = uniqueList([
        ...asStringList(args.files),
        ...asStringList(args.filePaths),
    ], 20);
    const skills = uniqueList([
        ...asStringList(args.skills),
        ...asStringList(args.skillNames),
        ...extractLinkedSelectors(prompt, '$'),
    ], 20);
    const isRuntimeControlPlane = /orchestrat|synapse|architect|runtime|mcp|dispatch|route|routing|queue|queued|scheduler|model routing|worker|hiring|workflow selection|token|budget|memory|lifecycle/.test(lower);
    const wantsResearch = /research|paper|literature|deep-research|source-grounded|cited/.test(lower);
    const wantsMemory = /memory|recall|learning|decay|graph|mcp/.test(lower);
    const wantsPerf = /fast|faster|millisecond|latency|performance|budget|token|optim/i.test(lower);
    const workflows = uniqueList([
        ...asStringList(args.workflows),
        ...asStringList(args.workflowSelectors),
        isRuntimeControlPlane ? 'orchestration-execution-loop' : undefined,
        isRuntimeControlPlane ? 'backend-execution-loop' : undefined,
        isRuntimeControlPlane ? 'testing-execution-loop' : undefined,
        isRuntimeControlPlane && !wantsResearch ? 'typescript-execution-loop' : undefined,
        wantsResearch ? 'research-and-implement' : undefined,
        lower.match(/dashboard|board|ui|ux/) && !isRuntimeControlPlane ? 'frontend-execution-loop' : undefined,
    ], 8);
    const specialists = uniqueList([
        ...asStringList(args.specialists),
        isRuntimeControlPlane ? 'Agents Orchestrator' : undefined,
        isRuntimeControlPlane ? 'Backend Architect' : undefined,
        isRuntimeControlPlane ? 'Workflow Optimizer' : undefined,
        wantsPerf ? 'Performance Benchmarker' : undefined,
        wantsMemory && !wantsPerf ? 'Backend Architect' : undefined,
        lower.match(/dashboard|board|ui|ux/) && !isRuntimeControlPlane ? 'Frontend Developer' : undefined,
        lower.match(/test|verify|qa|release|publish/) ? 'Verification Engineer' : undefined,
    ], 8);
    const crew = String(args.crew ?? args.selectedCrew ?? (lower.match(/orchestrat|synapse|runtime|mcp|dispatch|queue|queued/)
        ? 'Implementation Crew'
        : lower.match(/research|paper|literature/)
            ? 'Research Crew'
            : 'Implementation Crew'));
    const risk = lower.match(/fix|broken|bug|doesn.?t|failed|queued|runtime|synapse|orchestrat/) ? 'high' : 'medium';
    const task = {
        goal: prompt,
        intent: lower.match(/fix|bug|broken|doesn.?t|failed/) ? 'bugfix' : 'feature',
        files,
        skillNames: skills,
        workflowSelectors: workflows,
    };
    const workerManifests = uniqueList([
        specialists.length > 0 ? 'worker_coder' : undefined,
        'worker_verifier',
    ]).map((workerId, index) => ({
        workerId,
        role: index === 0 ? 'coder' : 'verifier',
        specialistName: index === 0 ? specialists[0] : 'Verification Engineer',
        actions: index === 0 ? ['inspect', 'patch'] : ['verify'],
        verifyCommands: index === 1 ? ['npm run lint', 'targeted tests'] : [],
    }));
    const run = {
        runId,
        requestBrief: {
            id: `brief_${runId}`,
            intent: task.intent,
            risk,
            confidence: 0.68,
        },
        plannerResult: {
            selectedFiles: files,
            selectedSkills: skills,
            selectedWorkflows: workflows,
            selectedSpecialists: specialists.map((name) => ({ name })),
            selectedCrew: { name: crew },
        },
        workerManifests,
    };
    const plan = buildSelectionPlan({ run, task });
    return { plan, crew, specialists, workflows };
}
function formatBudgetRoute(plan) {
    const total = Number(plan.budgets?.total ?? 0);
    const code = Number(plan.budgets?.codeBlocks ?? plan.budgets?.codeBlockPolicy?.reservedTokens ?? 0);
    const cap = Number(plan.executionPolicy?.agentFlow?.maxTaskCostUsd ?? plan.executionPolicy?.budgetCapUsd ?? 0);
    return `${total.toLocaleString()} tokens · ${code.toLocaleString()} code-block tokens · $${cap.toFixed(2)} task cap`;
}
function formatQueuedOrchestrateReceipt(args, receipt) {
    const preview = inferQueuedOrchestratePreview(args, receipt.runId);
    const plan = preview.plan;
    const stages = Array.isArray(plan.executionPolicy?.agentFlow?.stages)
        ? plan.executionPolicy.agentFlow.stages
        : [];
    const selected = plan.selected ?? {};
    const specialists = uniqueList([...(selected.specialists ?? []), ...preview.specialists], 8);
    const workflows = uniqueList([...(selected.workflows ?? []), ...preview.workflows], 8);
    const skills = uniqueList(selected.skills ?? [], 8);
    const receiptJson = JSON.stringify({ queued: true, runId: receipt.runId, etaMs: receipt.etaMs }, null, 2);
    const hiredLine = [
        `crew ${preview.crew}`,
        specialists.length ? `specialists ${specialists.join(', ')}` : '',
        workflows.length ? `workflows ${workflows.join(', ')}` : '',
        skills.length ? `skills ${skills.join(', ')}` : '',
    ].filter(Boolean).join(' · ');
    return [
        'Nexus orchestration queued with hiring preflight.',
        `Run: ${receipt.runId}`,
        `ETA: ${receipt.etaMs ? `~${Math.ceil(receipt.etaMs / 1000)}s` : 'background'}`,
        `Hired/selected: ${hiredLine}`,
        `Model route: ${plan.modelRoute?.workerTier ?? 'T1'}${plan.modelRoute?.reviewerTier ? ` + reviewer ${plan.modelRoute.reviewerTier}` : ''}`,
        `Budget route: ${formatBudgetRoute(plan)}`,
        stages.length ? `AgentFlow gates: ${stages.map((stage) => `${stage.stage}:${stage.ownerRole}`).join(' -> ')}` : '',
        'Poll progress with nexus_run_status(runId).',
        '',
        '```json',
        receiptJson,
        '```',
    ].filter((line) => line !== '').join('\n');
}
/**
 * Route a tool call to the appropriate handler group.
 *
 * Returns the handler's result, or throws if no group claims the tool name
 * (which means the outer switch in handleToolCall will fall through to the
 * synapse/architects delegation that already ran before dispatch is called).
 */
export async function dispatchMcpToolCall(hctx, request, args, ctx) {
    const toolName = String(request.params?.name ?? '');
    const startedAt = Date.now();
    // Each handler returns Promise<McpToolResult | undefined>; a Promise is
    // never nullish, so `??` on raw calls would always short-circuit at the first
    // handler. We must await each call before falling through to the next group.
    const runHandlers = async () => {
        const groups = [
            handleOrchestrationGroup,
            handleMemoryGroup,
            handleGovernanceGroup,
            handleRuntimeGroup,
            handleDiscoveryGroup,
            handleWorkforceGroup,
            handleMiscGroup,
        ];
        for (const group of groups) {
            const result = await group(toolName, hctx, request, args, ctx);
            if (result !== undefined)
                return result;
        }
        return undefined;
    };
    const circuit = getCircuitManager();
    try {
        // Backpressure gate — reject early if queue is full
        const bp = checkBackpressure();
        if (!bp.ok) {
            return {
                content: [{
                        type: 'text',
                        text: JSON.stringify({ busy: true, retryAfterMs: bp.retryAfterMs }, null, 2)
                            + '\n\nNexus is at capacity. Retry after a few seconds.',
                    }],
            };
        }
        circuit.recordStart(toolName);
        let rawResult;
        // When work is handed off to the async gate we must NOT decrement
        // in-flight here — the tool is still running in the background, and
        // decrementing early would let checkBackpressure() undercount live work
        // and bypass the QUEUE_CAPACITY guard. Completion is wired to the
        // async_gate.completed/failed events below instead.
        let deferredToAsyncGate = false;
        if (SLOW_TOOLS.has(toolName) || circuit.isForceAsync(toolName)) {
            const gated = await withAsyncGate(async () => {
                const r = await runHandlers();
                return r ?? { content: [{ type: 'text', text: `Tool ${toolName} returned no result` }] };
            }, {
                tool: toolName,
                args,
                maxSyncMs: resolveMaxSyncMs(toolName, args),
                etaMs: TOOL_ETA_MS[toolName],
                alwaysQueue: shouldReturnQueuedReceipt(toolName, args),
            });
            if ('queued' in gated && gated.queued) {
                // Persist orchestration run record for durable stage tracking
                if (toolName === 'nexus_orchestrate') {
                    const goal = String(args.prompt ?? args.goal ?? '').slice(0, 512);
                    const client = hctx.getToolProfile();
                    try {
                        createRun(gated.runId, goal, client);
                        updateStage(gated.runId, 'executing', 20);
                    }
                    catch { /* non-fatal */ }
                }
                // Background work still running — defer completion bookkeeping
                // until async_gate.completed/failed fires for this runId.
                deferredToAsyncGate = true;
                const runId = gated.runId;
                let unsubOk = null;
                let unsubFail = null;
                let finalized = false;
                const finalize = (success) => {
                    if (finalized)
                        return;
                    finalized = true;
                    const durationMs = Date.now() - startedAt;
                    if (toolName === 'nexus_orchestrate') {
                        try {
                            const job = getAsyncGate().getJob(runId);
                            if (success) {
                                completeRun(runId, summarizeAsyncMcpResult(toolName, job?.result));
                            }
                            else {
                                failRun(runId, job?.error ?? `${toolName} failed asynchronously`);
                            }
                        }
                        catch { /* durable run updates are best-effort */ }
                    }
                    try {
                        circuit.recordComplete(toolName, durationMs, success);
                    }
                    catch { /* non-fatal */ }
                    recordToolInvocation(toolName, durationMs, success ? 'ok' : 'error');
                    nexusEventBus.emit('tool.invocation', { toolName, durationMs, status: success ? 'ok' : 'error' });
                    unsubOk?.();
                    unsubFail?.();
                };
                unsubOk = nexusEventBus.on('async_gate.completed', (ev) => {
                    if (ev.runId === runId)
                        finalize(true);
                });
                unsubFail = nexusEventBus.on('async_gate.failed', (ev) => {
                    if (ev.runId === runId)
                        finalize(false);
                });
                const existingJob = getAsyncGate().getJob(runId);
                if (existingJob?.status === 'completed')
                    finalize(true);
                if (existingJob?.status === 'failed')
                    finalize(false);
                // Return async receipt as MCP text so agent can poll
                rawResult = {
                    content: [{
                            type: 'text',
                            text: toolName === 'nexus_orchestrate'
                                ? formatQueuedOrchestrateReceipt(args, gated)
                                : JSON.stringify({ queued: true, runId: gated.runId, etaMs: gated.etaMs }, null, 2)
                                    + '\n\nCall nexus_run_status(runId) to check progress.',
                        }],
                };
            }
            else {
                rawResult = gated;
            }
        }
        else {
            rawResult = await runHandlers();
        }
        if (!deferredToAsyncGate) {
            const durationMs = Date.now() - startedAt;
            circuit.recordComplete(toolName, durationMs, true);
            recordToolInvocation(toolName, durationMs, 'ok');
            nexusEventBus.emit('tool.invocation', { toolName, durationMs, status: 'ok' });
        }
        return rawResult;
    }
    catch (err) {
        const durationMs = Date.now() - startedAt;
        circuit.recordComplete(toolName, durationMs, false);
        recordToolInvocation(toolName, durationMs, 'error');
        nexusEventBus.emit('tool.invocation', { toolName, durationMs, status: 'error' });
        throw err;
    }
}
