/**
 * Subagent Delegation Extension for Spectral
 *
 * Registers a `subagent` tool that allows the coding agent to delegate tasks
 * to specialized sub-agents running in-process via `agentLoop()`.
 *
 * Each subagent invocation runs with its own fresh AgentContext (isolated
 * context window), keeping the main conversation lean and focused.
 *
 * Three execution modes:
 *   - Single:   { agent: "name", task: "..." }
 *   - Parallel: { tasks: [{ agent, task }, ...] }  (max 8, 4 concurrent)
 *   - Chain:    { chain: [{ agent, task: "... {previous} ..." }, ...] }
 *
 * Agent definitions live as markdown files with YAML frontmatter:
 *   - ~/.spectral/agent/agents/*.md    (user-level, always loaded)
 *   - .spectral/agents/*.md            (project-level, opt-in via agentScope)
 *
 * Subagents use an in-process `agentLoop()` for:
 *   - Clean context isolation (fresh messages array)
 *   - Combined system prompt (main agent + subagent-specific instructions)
 *   - Tool filtering (only the tools specified in agent config)
 *   - Model selection (session model preferred, agent-configured as fallback)
 *   - Signal propagation (abort passthrough)
 *   - Access to observational memory via the recall tool
 */
import { agentLoop } from "../sdk/agent-core/index.js";
import { StringEnum } from "../sdk/ai/index.js";
import { Type } from "typebox";
import { discoverAgents } from "./agents.js";
// ---------------------------------------------------------------------------
// Built-in tool resolution (no spawn needed — tools are registered in-process)
// ---------------------------------------------------------------------------
import { createTool, allToolNames } from "../sdk/coding-agent/core/tools/index.js";
import { wrapToolDefinition } from "../sdk/coding-agent/core/tools/tool-definition-wrapper.js";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const MAX_PARALLEL_TASKS = 8;
const MAX_CONCURRENCY = 4;
// ---------------------------------------------------------------------------
// Helpers — formatting
// ---------------------------------------------------------------------------
function formatTokens(count) {
    if (count < 1000)
        return count.toString();
    if (count < 10000)
        return `${(count / 1000).toFixed(1)}k`;
    if (count < 1000000)
        return `${Math.round(count / 1000)}k`;
    return `${(count / 1000000).toFixed(1)}M`;
}
function formatUsageStats(usage, model) {
    const parts = [];
    if (usage.turns)
        parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
    if (usage.input)
        parts.push(`↑${formatTokens(usage.input)}`);
    if (usage.output)
        parts.push(`↓${formatTokens(usage.output)}`);
    if (usage.cacheRead)
        parts.push(`R${formatTokens(usage.cacheRead)}`);
    if (usage.cacheWrite)
        parts.push(`W${formatTokens(usage.cacheWrite)}`);
    if (usage.cost)
        parts.push(`$${usage.cost.toFixed(4)}`);
    if (usage.contextTokens && usage.contextTokens > 0) {
        parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
    }
    if (model)
        parts.push(model);
    return parts.join(" ");
}
// ---------------------------------------------------------------------------
// Helpers — message extraction
// ---------------------------------------------------------------------------
function getFinalOutput(messages) {
    for (let i = messages.length - 1; i >= 0; i--) {
        const msg = messages[i];
        if (msg.role === "assistant") {
            for (const part of msg.content) {
                if (part.type === "text")
                    return part.text;
            }
        }
    }
    return "";
}
// ---------------------------------------------------------------------------
// Helpers — model resolution
// ---------------------------------------------------------------------------
/**
 * Resolve a model string (e.g. "claude-sonnet-4-5" or "anthropic/claude-sonnet-4-5")
 * to a Model object from the registry.
 */
function resolveModelString(modelStr, modelRegistry) {
    if (!modelStr)
        return undefined;
    const allModels = modelRegistry.getAll();
    // Try "provider/modelId" format
    const slashIdx = modelStr.indexOf("/");
    if (slashIdx !== -1) {
        const provider = modelStr.substring(0, slashIdx);
        const modelId = modelStr.substring(slashIdx + 1);
        const match = allModels.find((m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase());
        if (match)
            return match;
    }
    // Try exact model ID match
    const idMatch = allModels.find((m) => m.id.toLowerCase() === modelStr.toLowerCase());
    if (idMatch)
        return idMatch;
    // Try fuzzy match (model ID contains the string)
    const fuzzy = allModels.find((m) => m.id.toLowerCase().includes(modelStr.toLowerCase()));
    return fuzzy;
}
// ---------------------------------------------------------------------------
// Helpers — tool resolution
// ---------------------------------------------------------------------------
/**
 * Resolve tool names from an agent definition into AgentTool objects.
 *
 * Built-in tools (read, bash, edit, write, grep, find, ls) are created via
 * `createTool()`. Extension-registered tools (e.g., recall) are resolved
 * via the ExtensionAPI and wrapped.
 */
function resolveSubagentTools(toolNames, cwd, ext, ctx) {
    if (!toolNames || toolNames.length === 0)
        return [];
    const tools = [];
    for (const name of toolNames) {
        // Built-in tools
        if (allToolNames.has(name)) {
            try {
                const tool = createTool(name, cwd);
                tools.push(tool);
            }
            catch {
                // Tool creation failed — skip
            }
            continue;
        }
        // Extension-registered tools (recall, designer_*, etc.)
        // ext at runtime is the embedded ExtensionAPI which has getToolDefinition;
        // the npm types don't expose it, so we cast.
        const richApi = ext;
        const toolDef = richApi.getToolDefinition(name);
        if (toolDef) {
            tools.push(wrapToolDefinition(toolDef, () => ctx));
        }
    }
    return tools;
}
// ---------------------------------------------------------------------------
// Helpers — concurrency
// ---------------------------------------------------------------------------
async function mapWithConcurrencyLimit(items, concurrency, fn) {
    if (items.length === 0)
        return [];
    const limit = Math.max(1, Math.min(concurrency, items.length));
    const results = new Array(items.length);
    let nextIndex = 0;
    const workers = new Array(limit).fill(null).map(async () => {
        while (true) {
            const current = nextIndex++;
            if (current >= items.length)
                return;
            results[current] = await fn(items[current], current);
        }
    });
    await Promise.all(workers);
    return results;
}
// ---------------------------------------------------------------------------
// Core — run a single subagent in-process via agentLoop
// ---------------------------------------------------------------------------
async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, ext, ctx) {
    const agent = agents.find((a) => a.name === agentName);
    if (!agent) {
        const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
        return {
            agent: agentName,
            agentSource: "unknown",
            task,
            exitCode: 1,
            messages: [],
            stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
            usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
            step,
        };
    }
    // Build combined system prompt: main agent's + subagent's
    const mainSystemPrompt = ctx.getSystemPrompt();
    const combinedPrompt = mainSystemPrompt
        ? `${mainSystemPrompt}\n\n---\n\nSubagent: ${agent.name}\n${agent.systemPrompt}`
        : agent.systemPrompt;
    // Resolve model — session model preferred, agent-configured as fallback
    const sessionModel = ctx.model && ctx.modelRegistry.hasConfiguredAuth(ctx.model) ? ctx.model : undefined;
    const model = sessionModel ?? resolveModelString(agent.model, ctx.modelRegistry);
    if (!model) {
        return {
            agent: agentName,
            agentSource: agent.source,
            task,
            exitCode: 1,
            messages: [],
            stderr: `Cannot resolve model "${agent.model ?? "(none specified)"}". Make sure the model is configured.`,
            usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
            model: agent.model,
            step,
            errorMessage: `Model not found: ${agent.model ?? "unspecified"}`,
        };
    }
    // Resolve auth for the model
    const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
    if (!auth.ok || !auth.apiKey) {
        return {
            agent: agentName,
            agentSource: agent.source,
            task,
            exitCode: 1,
            messages: [],
            stderr: `No API key configured for model "${model.provider}/${model.id}".`,
            usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
            model: agent.model,
            step,
            errorMessage: `No API key for ${model.provider}/${model.id}`,
        };
    }
    // Resolve tools
    const effectiveCwd = cwd ?? defaultCwd;
    const tools = resolveSubagentTools(agent.tools, effectiveCwd, ext, ctx);
    const currentResult = {
        agent: agentName,
        agentSource: agent.source,
        task,
        exitCode: -1, // -1 = still running; 0 = success; 1 = error
        messages: [],
        stderr: "",
        usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
        model: model.id,
        step,
    };
    // Accumulated streaming text from text_delta events (reset per assistant turn).
    let streamingText = "";
    const emitUpdate = () => {
        if (onUpdate) {
            onUpdate({
                content: [{ type: "text", text: getFinalOutput(currentResult.messages) || streamingText || "(running...)" }],
                details: makeDetails([currentResult]),
            });
        }
    };
    try {
        const config = {
            model,
            apiKey: auth.apiKey,
            headers: auth.headers,
            convertToLlm: (msgs) => msgs.filter((m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult"),
            toolExecution: "parallel",
        };
        const stream = agentLoop([{ role: "user", content: [{ type: "text", text: task }], timestamp: Date.now() }], {
            systemPrompt: combinedPrompt,
            messages: [],
            tools,
        }, config, signal);
        // Collect messages from the stream
        const collectedMessages = [];
        let hadError = false;
        for await (const event of stream) {
            if (signal?.aborted) {
                hadError = true;
                break;
            }
            if (event.type === "message_update" && event.message?.role === "assistant") {
                // Forward streaming text deltas so the UI can show the subagent's
                // response as it's generated, not just the final result.
                const inner = event.assistantMessageEvent;
                if (inner?.type === "text_delta" && inner.delta) {
                    streamingText += inner.delta;
                    emitUpdate();
                }
                continue;
            }
            if (event.type === "message_end" && event.message) {
                const msg = event.message;
                collectedMessages.push(msg);
                if (msg.role === "assistant") {
                    // Reset streaming accumulator — final text is now in messages
                    streamingText = "";
                    currentResult.usage.turns++;
                    const usage = msg.usage;
                    if (usage) {
                        currentResult.usage.input += usage.input || 0;
                        currentResult.usage.output += usage.output || 0;
                        currentResult.usage.cacheRead += usage.cacheRead || 0;
                        currentResult.usage.cacheWrite += usage.cacheWrite || 0;
                        currentResult.usage.cost += usage.cost?.total || 0;
                        currentResult.usage.contextTokens = usage.totalTokens || 0;
                    }
                    if (!currentResult.model && msg.model)
                        currentResult.model = msg.model;
                    if (msg.stopReason)
                        currentResult.stopReason = msg.stopReason;
                    if (msg.errorMessage)
                        currentResult.errorMessage = msg.errorMessage;
                    if (msg.stopReason === "error" || msg.stopReason === "aborted") {
                        hadError = true;
                    }
                }
                currentResult.messages = [...collectedMessages];
                emitUpdate();
            }
            if (event.type === "tool_execution_end" && event.isError) {
                emitUpdate();
            }
        }
        // Drain the stream result (already consumed by iteration, but ensure cleanup)
        try {
            await stream.result();
        }
        catch {
            // Stream result error — already handled by message events
        }
        currentResult.exitCode = hadError ? 1 : 0;
        if (signal?.aborted) {
            currentResult.exitCode = 1;
            currentResult.stopReason = "aborted";
            currentResult.errorMessage = "Subagent was aborted";
        }
        return currentResult;
    }
    catch (err) {
        const errorMsg = err instanceof Error ? err.message : String(err);
        currentResult.exitCode = 1;
        currentResult.stopReason = "error";
        currentResult.errorMessage = errorMsg;
        currentResult.stderr = errorMsg;
        return currentResult;
    }
}
// ---------------------------------------------------------------------------
// Tool parameter schemas
// ---------------------------------------------------------------------------
const TaskItem = Type.Object({
    agent: Type.String({ description: "Name of the agent to invoke" }),
    task: Type.String({ description: "Task to delegate to the agent" }),
    cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
});
const ChainItem = Type.Object({
    agent: Type.String({ description: "Name of the agent to invoke" }),
    task: Type.String({
        description: "Task with optional {previous} placeholder for prior output",
    }),
    cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
});
const AgentScopeSchema = StringEnum(["user", "project", "both"], {
    description: 'Which agent directories to use. Default: "user". Use "both" to include project-local agents.',
    default: "user",
});
const SubagentParams = Type.Object({
    agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (for single mode)" })),
    task: Type.Optional(Type.String({ description: "Task to delegate (for single mode)" })),
    tasks: Type.Optional(Type.Array(TaskItem, {
        description: "Array of {agent, task} for parallel execution",
    })),
    chain: Type.Optional(Type.Array(ChainItem, {
        description: "Array of {agent, task} for sequential execution",
    })),
    agentScope: Type.Optional(AgentScopeSchema),
    confirmProjectAgents: Type.Optional(Type.Boolean({
        description: "Prompt before running project-local agents. Default: true.",
        default: true,
    })),
    cwd: Type.Optional(Type.String({
        description: "Working directory for the agent process (single mode)",
    })),
});
// ---------------------------------------------------------------------------
// Extension entry point
// ---------------------------------------------------------------------------
export default function subagentExtension(ext) {
    ext.registerTool({
        name: "subagent",
        label: "Subagent",
        description: [
            "Delegate tasks to specialized subagents with isolated context.",
            "Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
            'Default agent scope is "user" (from ~/.spectral/agent/agents).',
            'To enable project-local agents in .spectral/agents, set agentScope: "both" (or "project").',
        ].join(" "),
        promptSnippet: "Run a subagent to investigate, plan, review, or implement code changes",
        promptGuidelines: [
            "Use the subagent tool to delegate complex investigations to specialized subagents instead of reading many files in the main conversation. Each subagent runs in an isolated context window.",
        ],
        parameters: SubagentParams,
        async execute(_toolCallId, params, signal, onUpdate, ctx) {
            const agentScope = params.agentScope ?? "user";
            const discovery = discoverAgents(ctx.cwd, agentScope);
            const agents = discovery.agents;
            const confirmProjectAgents = params.confirmProjectAgents ?? true;
            const hasChain = (params.chain?.length ?? 0) > 0;
            const hasTasks = (params.tasks?.length ?? 0) > 0;
            const hasSingle = Boolean(params.agent && params.task);
            const modeCount = Number(hasChain) + Number(hasTasks) + Number(hasSingle);
            const makeDetails = (mode) => (results) => ({
                mode,
                agentScope,
                projectAgentsDir: discovery.projectAgentsDir,
                results,
            });
            if (modeCount !== 1) {
                const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
                return {
                    content: [
                        {
                            type: "text",
                            text: `Invalid parameters. Provide exactly one mode.\nAvailable agents: ${available}`,
                        },
                    ],
                    details: makeDetails("single")([]),
                };
            }
            // Security: if project-local agents are enabled, confirm with user.
            // In headless mode (no TUI), project-local agents are silently allowed —
            // the caller is already trusted (it's the coding agent itself).
            if ((agentScope === "project" || agentScope === "both") &&
                confirmProjectAgents &&
                ctx.hasUI) {
                const requestedAgentNames = new Set();
                if (params.chain)
                    for (const step of params.chain)
                        requestedAgentNames.add(step.agent);
                if (params.tasks)
                    for (const t of params.tasks)
                        requestedAgentNames.add(t.agent);
                if (params.agent)
                    requestedAgentNames.add(params.agent);
                const projectAgentsRequested = Array.from(requestedAgentNames)
                    .map((name) => agents.find((a) => a.name === name))
                    .filter((a) => a?.source === "project");
                if (projectAgentsRequested.length > 0) {
                    const names = projectAgentsRequested.map((a) => a.name).join(", ");
                    const dir = discovery.projectAgentsDir ?? "(unknown)";
                    const ok = await ctx.ui.confirm("Run project-local agents?", `Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`);
                    if (!ok)
                        return {
                            content: [
                                { type: "text", text: "Canceled: project-local agents not approved." },
                            ],
                            details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
                        };
                }
            }
            // ── Chain mode ──────────────────────────────────────────────
            if (params.chain && params.chain.length > 0) {
                const results = [];
                let previousOutput = "";
                for (let i = 0; i < params.chain.length; i++) {
                    const step = params.chain[i];
                    const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
                    const chainUpdate = onUpdate
                        ? (partial) => {
                            const currentResult = partial.details?.results[0];
                            if (currentResult) {
                                const allResults = [...results, currentResult];
                                onUpdate({
                                    content: partial.content,
                                    details: makeDetails("chain")(allResults),
                                });
                            }
                        }
                        : undefined;
                    const result = await runSingleAgent(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, signal, chainUpdate, makeDetails("chain"), ext, ctx);
                    results.push(result);
                    const isError = result.exitCode !== 0 ||
                        result.stopReason === "error" ||
                        result.stopReason === "aborted";
                    if (isError) {
                        const errorMsg = result.errorMessage ||
                            result.stderr ||
                            getFinalOutput(result.messages) ||
                            "(no output)";
                        return {
                            content: [
                                {
                                    type: "text",
                                    text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}`,
                                },
                            ],
                            details: makeDetails("chain")(results),
                            isError: true,
                        };
                    }
                    previousOutput = getFinalOutput(result.messages);
                }
                return {
                    content: [
                        {
                            type: "text",
                            text: getFinalOutput(results[results.length - 1].messages) || "(no output)",
                        },
                    ],
                    details: makeDetails("chain")(results),
                };
            }
            // ── Parallel mode ───────────────────────────────────────────
            if (params.tasks && params.tasks.length > 0) {
                if (params.tasks.length > MAX_PARALLEL_TASKS)
                    return {
                        content: [
                            {
                                type: "text",
                                text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
                            },
                        ],
                        details: makeDetails("parallel")([]),
                    };
                const allResults = new Array(params.tasks.length);
                for (let i = 0; i < params.tasks.length; i++) {
                    allResults[i] = {
                        agent: params.tasks[i].agent,
                        agentSource: "unknown",
                        task: params.tasks[i].task,
                        exitCode: -1,
                        messages: [],
                        stderr: "",
                        usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
                    };
                }
                const emitParallelUpdate = () => {
                    if (onUpdate) {
                        const running = allResults.filter((r) => r.exitCode === -1).length;
                        const done = allResults.filter((r) => r.exitCode !== -1).length;
                        onUpdate({
                            content: [
                                {
                                    type: "text",
                                    text: `Parallel: ${done}/${allResults.length} done, ${running} running...`,
                                },
                            ],
                            details: makeDetails("parallel")([...allResults]),
                        });
                    }
                };
                const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
                    const result = await runSingleAgent(ctx.cwd, agents, t.agent, t.task, t.cwd, undefined, signal, (partial) => {
                        if (partial.details?.results[0]) {
                            allResults[index] = partial.details.results[0];
                            emitParallelUpdate();
                        }
                    }, makeDetails("parallel"), ext, ctx);
                    allResults[index] = result;
                    emitParallelUpdate();
                    return result;
                });
                const successCount = results.filter((r) => r.exitCode === 0).length;
                const summaries = results.map((r) => {
                    const output = getFinalOutput(r.messages);
                    const preview = output.slice(0, 100) + (output.length > 100 ? "..." : "");
                    return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`;
                });
                return {
                    content: [
                        {
                            type: "text",
                            text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
                        },
                    ],
                    details: makeDetails("parallel")(results),
                };
            }
            // ── Single mode ─────────────────────────────────────────────
            if (params.agent && params.task) {
                const result = await runSingleAgent(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, signal, onUpdate, makeDetails("single"), ext, ctx);
                const isError = result.exitCode !== 0 ||
                    result.stopReason === "error" ||
                    result.stopReason === "aborted";
                if (isError) {
                    const errorMsg = result.errorMessage ||
                        result.stderr ||
                        getFinalOutput(result.messages) ||
                        "(no output)";
                    return {
                        content: [
                            {
                                type: "text",
                                text: `Agent ${result.stopReason || "failed"}: ${errorMsg}`,
                            },
                        ],
                        details: makeDetails("single")([result]),
                        isError: true,
                    };
                }
                return {
                    content: [
                        {
                            type: "text",
                            text: getFinalOutput(result.messages) || "(no output)",
                        },
                    ],
                    details: makeDetails("single")([result]),
                };
            }
            const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
            return {
                content: [
                    {
                        type: "text",
                        text: `Invalid parameters. Available agents: ${available}`,
                    },
                ],
                details: makeDetails("single")([]),
            };
        },
    });
}
