import { spawn } from "node:child_process";
import { DeepSeekCodeAbortError, throwIfAborted } from "../utils/abort.js";
export class McpStdioClient {
    command;
    cwd;
    timeoutMs;
    maxStderrChars;
    signal;
    child = null;
    stdoutBuffer = "";
    stderrBuffer = "";
    nextId = 1;
    abortHandler = () => this.abort();
    pending = new Map();
    constructor(command, cwd, timeoutMs = 10_000, maxStderrChars = 8_000, signal) {
        this.command = command;
        this.cwd = cwd;
        this.timeoutMs = timeoutMs;
        this.maxStderrChars = maxStderrChars;
        this.signal = signal;
    }
    async connect() {
        throwIfAborted(this.signal);
        this.child = spawn(this.command, {
            cwd: this.cwd,
            shell: true,
            windowsHide: true,
            stdio: ["pipe", "pipe", "pipe"],
        });
        this.child.stdout.on("data", (chunk) => this.onStdout(chunk));
        this.child.stderr.on("data", (chunk) => {
            this.stderrBuffer = (this.stderrBuffer + chunk.toString()).slice(-this.maxStderrChars);
        });
        this.child.on("error", (error) => this.rejectAll(error));
        this.child.on("close", (code) => {
            if (this.pending.size > 0)
                this.rejectAll(new Error(`MCP server exited with code ${code ?? "unknown"}`));
        });
        if (this.signal?.aborted)
            this.abort();
        else
            this.signal?.addEventListener("abort", this.abortHandler, { once: true });
        const result = await this.request("initialize", {
            protocolVersion: "2024-11-05",
            capabilities: {},
            clientInfo: {
                name: "deepseekcode",
                version: "0.1.0",
            },
        });
        this.notify("notifications/initialized", {});
        return result;
    }
    async listTools() {
        const result = await this.request("tools/list", {});
        if (isRecord(result) && Array.isArray(result.tools))
            return result.tools;
        return [];
    }
    async callTool(name, args) {
        return this.request("tools/call", {
            name,
            arguments: args,
        });
    }
    stderr() {
        return this.stderrBuffer;
    }
    close() {
        this.signal?.removeEventListener("abort", this.abortHandler);
        for (const pending of this.pending.values())
            clearTimeout(pending.timer);
        this.pending.clear();
        this.terminateChild();
        this.child = null;
    }
    request(method, params) {
        throwIfAborted(this.signal);
        const id = this.nextId++;
        this.write({ jsonrpc: "2.0", id, method, params });
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                this.pending.delete(id);
                reject(new Error(`MCP request timed out: ${method}`));
            }, this.timeoutMs);
            this.pending.set(id, {
                resolve: (response) => {
                    clearTimeout(timer);
                    if (response.error) {
                        reject(new Error(response.error.message));
                        return;
                    }
                    resolve(response.result);
                },
                reject,
                timer,
            });
        });
    }
    notify(method, params) {
        this.write({ jsonrpc: "2.0", method, params });
    }
    write(message) {
        if (!this.child)
            throw new Error("MCP server is not connected");
        this.child.stdin.write(`${JSON.stringify(message)}\n`);
    }
    onStdout(chunk) {
        this.stdoutBuffer += chunk.toString();
        const parts = this.stdoutBuffer.split(/\r?\n/);
        this.stdoutBuffer = parts.pop() ?? "";
        for (const part of parts) {
            if (!part.trim())
                continue;
            let message;
            try {
                message = JSON.parse(part);
            }
            catch {
                continue;
            }
            if (!isRecord(message))
                continue;
            if (typeof message.id === "number" && this.pending.has(message.id)) {
                const pending = this.pending.get(message.id);
                this.pending.delete(message.id);
                pending.resolve(message);
            }
            else if (typeof message.id === "number" && typeof message.method === "string") {
                this.writeRaw({
                    jsonrpc: "2.0",
                    id: message.id,
                    error: {
                        code: -32601,
                        message: `Unsupported server request: ${message.method}`,
                    },
                });
            }
        }
    }
    rejectAll(error) {
        for (const [id, pending] of this.pending) {
            clearTimeout(pending.timer);
            pending.reject(error);
            this.pending.delete(id);
        }
    }
    abort() {
        this.rejectAll(new DeepSeekCodeAbortError(this.signal?.reason));
        this.terminateChild();
    }
    terminateChild() {
        if (!this.child || this.child.killed)
            return;
        this.child.kill();
        if (process.platform === "win32" && this.child.pid) {
            spawn("taskkill", ["/pid", String(this.child.pid), "/T", "/F"], {
                windowsHide: true,
                stdio: "ignore",
            }).unref();
        }
    }
    writeRaw(message) {
        if (!this.child)
            throw new Error("MCP server is not connected");
        this.child.stdin.write(`${JSON.stringify(message)}\n`);
    }
}
export async function probeMcpStdioServer(command, cwd, timeoutMs = 10_000) {
    const client = new McpStdioClient(command, cwd, timeoutMs);
    try {
        const server = await client.connect();
        const tools = await client.listTools();
        return {
            server,
            tools,
            stderr: client.stderr(),
        };
    }
    finally {
        client.close();
    }
}
export async function callMcpStdioTool(command, cwd, toolName, args, timeoutMs = 10_000, signal) {
    const client = new McpStdioClient(command, cwd, timeoutMs, 8_000, signal);
    try {
        await client.connect();
        const result = await client.callTool(toolName, args);
        return { result, stderr: client.stderr() };
    }
    finally {
        client.close();
    }
}
function isRecord(value) {
    return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
