/**
 * Aexol MCP spectral extension.
 *
 * Loaded by the spectral wrapper via `--extension <abs-path>`. On startup it:
 *   1. Reads ~/.spectral/config.json (writen by `spectral login`).
 *   2. Calls tools/list against the MCP backend once.
 *   3. Reads the local Studio binding (`.aexol/aexol.jsonc`) so remote tools
 *      auto-inherit `teamId` / `projectId` without needing `remote_set_context`.
 *   4. Registers every returned remote tool through ext.registerTool() so the
 *      model can call them like built-in tools.
 *   5. Registers local binding-management tools (`local_bind_project`,
 *      `local_unbind_project`, `local_get_binding`) so users can bind a
 *      directory to an Aexol Studio project from within the agent.
 *
 * Each registered tool is just a thin proxy: when spectral's model invokes it, we
 * translate the call into a JSON-RPC tools/call POST and unwrap the response
 * into the `{ content: [{ type: "text", text }], details }` shape spectral expects.
 *
 * Failure modes are intentionally non-fatal: if config is missing or the
 * backend is unreachable we log a warning and return, leaving spectral to start
 * without Aexol tools rather than crashing the whole agent.
 */
import { getApiUrl, getConfigFile, readConfig } from "../config.js";
import { AexolMcpClient, AexolMcpError } from "../mcp-client.js";
import { readStudioBinding, writeStudioBinding, deleteStudioBinding, } from "../studio-binding.js";
/**
 * Render a backend tool result into a single string for ext.
 *
 * The Aexol backend wraps results as `{ content: [{ type: "json", json: ... }] }`
 * but may also send `{ type: "text", text: ... }`. We pick the first content
 * item, prefer JSON when present (pretty-print so the model can read it),
 * fall back to text, and last-resort stringify the whole content array.
 */
function renderContentToString(content) {
    if (content.length === 0)
        return "";
    const first = content[0];
    if (first.json !== undefined) {
        try {
            return JSON.stringify(first.json, null, 2);
        }
        catch {
            return String(first.json);
        }
    }
    if (typeof first.text === "string")
        return first.text;
    // For image/audio/binary content types, return a metadata placeholder
    // instead of serializing the raw base64 data into the model context.
    if (first.type === "image" || first.type === "audio") {
        const mimeType = typeof first.mimeType === "string" ? first.mimeType : first.type;
        const dataLen = typeof first.data === "string" ? first.data.length : 0;
        return `[${first.type}: ${mimeType}${dataLen ? `, ${dataLen.toLocaleString()} bytes base64` : ""}]`;
    }
    try {
        // Strip large binary fields from serialization to prevent context overflow.
        const safe = content.map((c) => {
            const copy = { ...c };
            if ("data" in copy && typeof copy.data === "string" && copy.data.length > 200) {
                copy.data = `[${copy.data.length.toLocaleString()} bytes base64 omitted]`;
            }
            return copy;
        });
        return JSON.stringify(safe, null, 2);
    }
    catch {
        return String(content);
    }
}
/** Build a one-line human label from a tool name like "remote_cloud_search". */
function toLabel(name) {
    return name
        .split(/[_\s-]+/)
        .filter(Boolean)
        .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
        .join(" ") || name;
}
// ---------------------------------------------------------------------------
// Studio binding – in-memory cache, refreshed on startup & by local tools
// ---------------------------------------------------------------------------
let currentBinding = null;
/**
 * Augment tool-call params with `teamId` and `projectId` from the current
 * Studio binding when they are not already set by the model. Only applies
 * to remote tools (names that start with `remote_`).
 */
function augmentParams(toolName, params) {
    if (!currentBinding || !toolName.startsWith("remote_"))
        return params;
    const augmented = { ...params };
    if (currentBinding.teamId && !("teamId" in params)) {
        augmented.teamId = currentBinding.teamId;
    }
    if (currentBinding.projectId && !("projectId" in params)) {
        augmented.projectId = currentBinding.projectId;
    }
    return augmented;
}
export default async function aexolMcpExtension(ext) {
    const cfg = await readConfig();
    if (!cfg) {
        // Pre-flight in cli.ts should have caught this. Logging is enough.
        process.stderr.write(`[aexol-mcp] No config found at ${getConfigFile()}; Aexol tools disabled. Run \`spectral login\`.\n`);
        return;
    }
    const apiUrl = getApiUrl(cfg.apiUrl);
    const token = cfg.teamApiKey || cfg.userJwt || "";
    if (!token) {
        process.stderr.write(`[aexol-mcp] No auth token in ${getConfigFile()}; Aexol tools disabled. Run \`spectral login\`.\n`);
        return;
    }
    const client = new AexolMcpClient(apiUrl, token);
    // ---- Load local Studio binding -------------------------------------------
    try {
        currentBinding = await readStudioBinding();
        if (currentBinding) {
            const displayName = currentBinding.name ?? currentBinding.projectId;
            process.stderr.write(`[aexol-mcp] Studio binding found: ${displayName} (${currentBinding.projectId})` +
                (currentBinding.teamId ? `, team ${currentBinding.teamId}` : "") +
                `\n`);
        }
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        process.stderr.write(`[aexol-mcp] Failed to read Studio binding: ${msg}\n`);
        // Non-fatal — remote tools still work with explicit teamId/projectId.
    }
    let tools;
    try {
        tools = await client.listTools();
    }
    catch (err) {
        const msg = err instanceof AexolMcpError || err instanceof Error ? err.message : String(err);
        process.stderr.write(`[aexol-mcp] Failed to fetch tool catalog from ${apiUrl}: ${msg}\n`);
        // Attempt auto-reauth via OAuth browser flow on auth errors
        if (err instanceof AexolMcpError && (err.status === 401 || err.status === 403)) {
            const { ensureAuthenticated } = await import("../auth-helper.js");
            const authResult = await ensureAuthenticated();
            if (authResult.ok && authResult.config) {
                const newToken = authResult.config.teamApiKey || authResult.config.userJwt || "";
                if (newToken && newToken !== token) {
                    process.stderr.write("[aexol-mcp] Re-authenticated — retrying tool catalog fetch...\n");
                    const newClient = new AexolMcpClient(apiUrl, newToken);
                    try {
                        tools = await newClient.listTools();
                    }
                    catch (retryErr) {
                        const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
                        process.stderr.write(`[aexol-mcp] Retry also failed: ${retryMsg}\n`);
                        process.stderr.write("[aexol-mcp] Continuing without Aexol tools.\n");
                        return;
                    }
                }
                else {
                    process.stderr.write("[aexol-mcp] Re-authentication did not produce a new token. Continuing without Aexol tools.\n");
                    return;
                }
            }
            else {
                process.stderr.write(`[aexol-mcp] Re-authentication failed: ${authResult.reason ?? "unknown"}. Continuing without Aexol tools.\n`);
                return;
            }
        }
        else {
            process.stderr.write("[aexol-mcp] Continuing without Aexol tools.\n");
            return;
        }
    }
    let registered = 0;
    for (const tool of tools) {
        // Defensive defaults: an empty-object schema is valid JSON Schema and
        // will pass the agent's validator if the model sends `{}`.
        const parameters = (tool.inputSchema ?? { type: "object", properties: {} });
        const definition = {
            name: tool.name,
            label: toLabel(tool.name),
            description: tool.description ?? "",
            parameters,
            async execute(_toolCallId, params) {
                const augmented = augmentParams(tool.name, params);
                const res = await client.callTool(tool.name, augmented);
                const text = renderContentToString(res.content);
                if (res.isError === true) {
                    // Surface backend tool errors as a textual result, not a thrown
                    // exception. Pi treats this as the tool's natural reply; the model
                    // sees the `[Aexol MCP error] ...` marker and can recover. Throwing
                    // here would force spectral to wrap the message and risk crashing
                    // callers that don't expect agent-tool exceptions.
                    const errorText = `[Aexol MCP error] ${text || "Tool reported an error with no message."}`;
                    return {
                        content: [{ type: "text", text: errorText }],
                        details: { isError: true },
                    };
                }
                return {
                    content: [{ type: "text", text }],
                    details: {},
                };
            },
        };
        try {
            ext.registerTool(definition);
            registered++;
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            process.stderr.write(`[aexol-mcp] Failed to register tool "${tool.name}": ${msg}\n`);
        }
    }
    // ---- Register local binding-management tools -----------------------------
    // These allow the model to bind / unbind / inspect the Studio project
    // binding directly from within the agent, without the user having to drop
    // out to the terminal and run `spectral bind` / `spectral unbind`.
    try {
        ext.registerTool({
            name: "local_bind_project",
            label: "Bind Project",
            description: "Link the current working directory to an Aexol Studio project. " +
                "After binding, all remote_* tools will automatically use this project context " +
                "(no need to set teamId/projectId on every call). " +
                "Use this when you want to work within a specific Aexol Studio project.",
            parameters: {
                type: "object",
                properties: {
                    projectId: {
                        type: "string",
                        description: "The Aexol Studio project ID to bind to (required). Copy this from the project URL in Studio.",
                    },
                    teamId: {
                        type: "string",
                        description: "Optional team ID. If omitted, remote tools use only the projectId for context.",
                    },
                    name: {
                        type: "string",
                        description: "Optional human-readable name for the binding (shown in logs and status messages).",
                    },
                },
                required: ["projectId"],
            },
            async execute(_toolCallId, params) {
                const p = params;
                if (!p.projectId || typeof p.projectId !== "string") {
                    return {
                        content: [{ type: "text", text: "Error: projectId (string) is required." }],
                        details: { isError: true },
                    };
                }
                const binding = {
                    projectId: p.projectId,
                    ...(p.teamId ? { teamId: p.teamId } : {}),
                    ...(p.name ? { name: p.name } : {}),
                };
                try {
                    await writeStudioBinding(binding);
                    currentBinding = binding;
                    const displayName = binding.name ?? binding.projectId;
                    return {
                        content: [{
                                type: "text",
                                text: `✓ Bound to Studio project: ${displayName} (${binding.projectId})` +
                                    (binding.teamId ? `, team ${binding.teamId}` : "") +
                                    `\nAll remote_* tools will now use this project context by default.`,
                            }],
                        details: { isError: false },
                    };
                }
                catch (err) {
                    const msg = err instanceof Error ? err.message : String(err);
                    return {
                        content: [{ type: "text", text: `Error writing binding: ${msg}` }],
                        details: { isError: true },
                    };
                }
            },
        });
        registered++;
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        process.stderr.write(`[aexol-mcp] Failed to register local_bind_project: ${msg}\n`);
    }
    try {
        ext.registerTool({
            name: "local_unbind_project",
            label: "Unbind Project",
            description: "Remove the Aexol Studio project binding from the current working directory. " +
                "After unbinding, remote_* tools will no longer receive automatic project context " +
                "— you must pass teamId/projectId explicitly or use local_bind_project to re-bind.",
            parameters: {
                type: "object",
                properties: {},
            },
            async execute() {
                if (!currentBinding) {
                    return {
                        content: [{ type: "text", text: "No Studio binding found in this directory — nothing to unbind." }],
                        details: { isError: false },
                    };
                }
                const displayName = currentBinding.name ?? currentBinding.projectId;
                const projectId = currentBinding.projectId;
                try {
                    const deleted = await deleteStudioBinding();
                    currentBinding = null;
                    if (deleted) {
                        return {
                            content: [{
                                    type: "text",
                                    text: `✓ Unbound from Studio project: ${displayName} (${projectId}).`,
                                }],
                            details: { isError: false },
                        };
                    }
                    return {
                        content: [{ type: "text", text: "Binding file was already removed." }],
                        details: { isError: false },
                    };
                }
                catch (err) {
                    const msg = err instanceof Error ? err.message : String(err);
                    return {
                        content: [{ type: "text", text: `Error removing binding: ${msg}` }],
                        details: { isError: true },
                    };
                }
            },
        });
        registered++;
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        process.stderr.write(`[aexol-mcp] Failed to register local_unbind_project: ${msg}\n`);
    }
    try {
        ext.registerTool({
            name: "local_get_binding",
            label: "Get Binding",
            description: "Show the current Aexol Studio project binding for this directory. " +
                "Returns the bound projectId, teamId, and name (if set), or reports that no binding exists.",
            parameters: {
                type: "object",
                properties: {},
            },
            async execute() {
                if (!currentBinding) {
                    return {
                        content: [{
                                type: "text",
                                text: "No Aexol Studio binding found in this directory.\n" +
                                    "Use local_bind_project to create one, or `spectral bind <project-id>` from the terminal.",
                            }],
                        details: { isError: false },
                    };
                }
                return {
                    content: [{
                            type: "text",
                            text: JSON.stringify(currentBinding, null, 2),
                        }],
                    details: { isError: false },
                };
            },
        });
        registered++;
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        process.stderr.write(`[aexol-mcp] Failed to register local_get_binding: ${msg}\n`);
    }
    process.stderr.write(`[aexol-mcp] Registered ${registered} Aexol MCP tool(s) from ${apiUrl}.\n`);
}
