import fs from "node:fs";
import path from "node:path";
import { normalizeMcpName, parseMcpConfig, renderMcpConfig, validateMcpServer, } from "../../mcp/config.js";
import { callMcpHttpTool, probeMcpHttpServer } from "../../mcp/httpClient.js";
import { McpSessionPool } from "../../mcp/sessionPool.js";
import { callMcpStdioTool, probeMcpStdioServer } from "../../mcp/stdioClient.js";
import { throwIfAborted } from "../../utils/abort.js";
export class McpService {
    projectPath;
    static pools = new Map();
    constructor(projectPath) {
        this.projectPath = projectPath;
    }
    get configPath() {
        return path.join(this.projectPath, ".deepseekcode", "mcp.json");
    }
    list() {
        return this.readConfig().servers;
    }
    addStdio(input) {
        const name = normalizeMcpName(input.name);
        if (!name)
            throw new Error("MCP server name is empty");
        const command = input.command.trim();
        if (!command)
            throw new Error("MCP stdio command is empty");
        return this.upsert({
            name,
            type: "stdio",
            command,
            args: input.args ?? [],
            env: {},
            enabled: true,
            description: input.description ?? "",
        }, Boolean(input.overwrite));
    }
    addHttp(input) {
        const name = normalizeMcpName(input.name);
        if (!name)
            throw new Error("MCP server name is empty");
        const url = input.url.trim();
        if (!url)
            throw new Error("MCP server url is empty");
        return this.upsert({
            name,
            type: input.type ?? "http",
            url,
            args: [],
            env: {},
            enabled: true,
            description: input.description ?? "",
        }, Boolean(input.overwrite));
    }
    remove(name) {
        const config = this.readConfig();
        const normalized = normalizeMcpName(name);
        const before = config.servers.length;
        config.servers = config.servers.filter((server) => server.name !== normalized);
        if (config.servers.length === before)
            return false;
        this.writeConfig(config);
        return true;
    }
    setEnabled(name, enabled) {
        const config = this.readConfig();
        const normalized = normalizeMcpName(name);
        const server = config.servers.find((candidate) => candidate.name === normalized);
        if (!server)
            return null;
        server.enabled = enabled;
        this.writeConfig(config);
        return server;
    }
    validate(name) {
        const normalized = name ? normalizeMcpName(name) : "";
        const servers = normalized
            ? this.list().filter((server) => server.name === normalized)
            : this.list();
        if (normalized && servers.length === 0) {
            return [{ name: normalized, ok: false, errors: [`MCP server not found: ${normalized}`], warnings: [] }];
        }
        return servers.map(validateMcpServer);
    }
    async probe(name, options) {
        const server = this.requireServer(name);
        if (!server.enabled)
            throw new Error(`MCP server is disabled: ${server.name}`);
        if (this.pool().has(server.name))
            return this.pool().probe(server.name);
        if (server.type === "http") {
            return probeMcpHttpServer(this.serverUrl(server), options.timeoutMs ?? 10_000);
        }
        if (server.type !== "stdio")
            throw new Error(`MCP probe currently supports stdio and http servers only: ${server.name}`);
        if (!options.allowShell)
            throw new Error("MCP stdio probing requires shell permission. Run /shell on first.");
        return probeMcpStdioServer(this.serverCommand(server), this.projectPath, options.timeoutMs ?? 10_000);
    }
    async probeWithRetry(name, options) {
        const maxAttempts = Math.max(1, Math.min(5, Math.trunc(options.attempts ?? 2)));
        const backoffMs = Math.max(0, Math.trunc(options.backoffMs ?? 250));
        const started = Date.now();
        let lastError;
        for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
            try {
                const probe = await this.probe(name, options);
                return { probe, attempts: attempt, latencyMs: Date.now() - started };
            }
            catch (error) {
                lastError = error;
                if (attempt < maxAttempts && backoffMs > 0)
                    await sleep(backoffMs * attempt);
            }
        }
        throw lastError instanceof Error ? lastError : new Error(String(lastError));
    }
    async health(name, options) {
        const normalized = name ? normalizeMcpName(name) : "";
        const servers = normalized
            ? this.list().filter((server) => server.name === normalized)
            : this.list();
        if (normalized && servers.length === 0) {
            throw new Error(`MCP server not found: ${normalized}`);
        }
        const results = [];
        for (const server of servers) {
            if (!server.enabled) {
                results.push({
                    name: server.name,
                    type: server.type,
                    enabled: false,
                    status: "disabled",
                    attempts: 0,
                    latencyMs: 0,
                    toolCount: 0,
                });
                continue;
            }
            const started = Date.now();
            try {
                const result = await this.probeWithRetry(server.name, options);
                results.push({
                    name: server.name,
                    type: server.type,
                    enabled: true,
                    status: "ok",
                    attempts: result.attempts,
                    latencyMs: result.latencyMs,
                    toolCount: result.probe.tools.length,
                    stderr: result.probe.stderr,
                });
            }
            catch (error) {
                results.push({
                    name: server.name,
                    type: server.type,
                    enabled: true,
                    status: "failed",
                    attempts: Math.max(1, Math.min(5, Math.trunc(options.attempts ?? 2))),
                    latencyMs: Date.now() - started,
                    toolCount: 0,
                    error: error instanceof Error ? error.message : String(error),
                });
            }
        }
        return results;
    }
    async callTool(name, toolName, args, options) {
        throwIfAborted(options.signal);
        const server = this.requireServer(name);
        if (!server.enabled)
            throw new Error(`MCP server is disabled: ${server.name}`);
        if (this.pool().has(server.name)) {
            const result = await this.pool().callTool(server.name, toolName, args);
            throwIfAborted(options.signal);
            return result;
        }
        if (server.type === "http") {
            return callMcpHttpTool(this.serverUrl(server), toolName, args, options.timeoutMs ?? 10_000, options.signal);
        }
        if (server.type !== "stdio")
            throw new Error(`MCP call currently supports stdio and http servers only: ${server.name}`);
        if (!options.allowShell)
            throw new Error("MCP stdio tool calls require shell permission. Run /shell on first.");
        return callMcpStdioTool(this.serverCommand(server), this.projectPath, toolName, args, options.timeoutMs ?? 10_000, options.signal);
    }
    requiresShell(name) {
        return this.requireServer(name).type === "stdio";
    }
    async connect(name, options) {
        const server = this.requireServer(name);
        if (!server.enabled)
            throw new Error(`MCP server is disabled: ${server.name}`);
        return this.pool().connect(server, options);
    }
    sessions() {
        return this.pool().list();
    }
    closeSession(name) {
        return this.pool().close(name);
    }
    upsert(server, overwrite) {
        const config = this.readConfig();
        const index = config.servers.findIndex((candidate) => candidate.name === server.name);
        if (index >= 0 && !overwrite)
            throw new Error(`MCP server already exists: ${server.name}`);
        if (index >= 0)
            config.servers[index] = server;
        else
            config.servers.push(server);
        this.writeConfig(config);
        return server;
    }
    requireServer(name) {
        const normalized = normalizeMcpName(name);
        const server = this.list().find((candidate) => candidate.name === normalized);
        if (!server)
            throw new Error(`MCP server not found: ${normalized}`);
        return server;
    }
    serverCommand(server) {
        return [server.command, ...(server.args ?? [])].filter(Boolean).join(" ");
    }
    serverUrl(server) {
        if (!server.url)
            throw new Error(`MCP server has no url: ${server.name}`);
        return server.url;
    }
    readConfig() {
        if (!fs.existsSync(this.configPath))
            return { servers: [] };
        return parseMcpConfig(JSON.parse(fs.readFileSync(this.configPath, "utf8")));
    }
    writeConfig(config) {
        fs.mkdirSync(path.dirname(this.configPath), { recursive: true });
        fs.writeFileSync(this.configPath, renderMcpConfig(config), "utf8");
    }
    pool() {
        const key = path.resolve(this.projectPath);
        let pool = McpService.pools.get(key);
        if (!pool) {
            pool = new McpSessionPool(key);
            McpService.pools.set(key, pool);
        }
        return pool;
    }
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
