"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MCP_INSTALL_SCHEMA_VERSION = exports.MCP_INSTALL_SELECTION_KIND = exports.MCP_INSTALL_RESULT_KIND = void 0;
exports.installMcp = installMcp;
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
const environment_1 = require("../auth-profiles/environment");
const store_1 = require("../auth-profiles/store");
const metadata_1 = require("../catalog/metadata");
const requirement_exclusions_1 = require("../catalog/requirement-exclusions");
const command_1 = require("../core/command");
const search_text_1 = require("../core/search-text");
const catalog_1 = require("./catalog");
const provider_claude_1 = require("./provider-claude");
const provider_codex_1 = require("./provider-codex");
const ref_1 = require("./ref");
exports.MCP_INSTALL_RESULT_KIND = "alpaca.mcpInstallResult";
exports.MCP_INSTALL_SELECTION_KIND = "alpaca.mcpInstallSelectionRequired";
exports.MCP_INSTALL_SCHEMA_VERSION = 2;
async function installMcp(input) {
    if ((input.id === undefined) === (input.query === undefined)) {
        throw new Error("Specify exactly one of id or query.");
    }
    const catalog = await (0, catalog_1.loadEffectiveMcpCatalog)({ repoRoot: input.repoRoot });
    const topics = await (0, metadata_1.loadEffectiveCatalogTopics)({ repoRoot: input.repoRoot });
    const entry = input.id !== undefined
        ? findCatalogEntryById(catalog.entries, input.id)
        : await selectInstallEntryForQuery({
            repoRoot: input.repoRoot,
            query: input.query ?? "",
            target: input.target,
            entries: catalog.entries,
            topicDescriptions: (0, metadata_1.toCatalogTopicMap)(topics),
            ...(input.homeDirectory === undefined
                ? {}
                : { homeDirectory: input.homeDirectory }),
            includeExcluded: input.includeExcluded === true,
        });
    if ("kind" in entry) {
        return entry;
    }
    const serverSpec = (0, catalog_1.resolveMcpCatalogServerForTarget)(entry, input.target);
    if (serverSpec === null) {
        throw new Error(formatMcpCatalogInstallTargetError(entry, input.target));
    }
    assertSafeAutoInstall(entry);
    const serverName = input.serverName ?? entry.id;
    if (!(0, ref_1.isValidMcpServerName)(serverName)) {
        throw new Error(`Invalid MCP server name: ${serverName}`);
    }
    if (input.target !== "codex" && input.target !== "claude") {
        if (input.authProfileId !== undefined) {
            throw new Error(`Auth profile selection is only supported for built-in MCP targets: ${input.target}`);
        }
        return installCustomWorkerMcp({
            input,
            entry,
            serverName,
            serverSpec,
        });
    }
    const profileSelection = selectInstallProfile({
        repoRoot: input.repoRoot,
        query: input.id ?? input.query ?? entry.id,
        target: input.target,
        authProfiles: input.authProfiles ?? [],
        ...(input.authProfileId === undefined
            ? {}
            : { authProfileId: input.authProfileId }),
    });
    if ("kind" in profileSelection) {
        return profileSelection;
    }
    const command = input.target === "codex"
        ? (0, provider_codex_1.buildCodexMcpInstallCommand)(serverName, serverSpec)
        : (0, provider_claude_1.buildClaudeMcpInstallCommand)(serverName, serverSpec);
    const nextSteps = buildNextSteps(entry, input.target, serverName);
    const run = input.run ?? command_1.runCommand;
    const env = buildInstallEnvironment({
        profile: profileSelection.profile,
        env: input.env ?? process.env,
    });
    assertWorkerScopedInstallEnvironment({
        target: input.target,
        env,
    });
    if (input.overwrite === true) {
        await removeExistingMcpServer({
            repoRoot: input.repoRoot,
            target: input.target,
            serverName,
            env,
            run,
        });
    }
    await run({
        command: command[0] ?? "",
        args: command.slice(1),
        cwd: input.repoRoot,
        env,
        timeoutMs: 60_000,
    });
    return {
        kind: exports.MCP_INSTALL_RESULT_KIND,
        schemaVersion: exports.MCP_INSTALL_SCHEMA_VERSION,
        status: "installed",
        target: input.target,
        id: serverName,
        serverName,
        ref: `${input.target}:${serverName}`,
        catalogEntryId: entry.id,
        source: (0, catalog_1.formatMcpCatalogEntrySource)(entry),
        ...(entry.quality?.sourceUrl === undefined
            ? {}
            : { sourceUrl: entry.quality.sourceUrl }),
        command,
        nextSteps,
    };
}
async function installProjectMcpServer(input) {
    const configPath = node_path_1.default.join(input.repoRoot, ".mcp.json");
    const current = await readProjectMcpConfig(configPath);
    const mcpServers = current.mcpServers;
    if (mcpServers[input.serverName] !== undefined && !input.overwrite) {
        throw new Error(`MCP server already exists: ${input.serverName}`);
    }
    mcpServers[input.serverName] = mcpCatalogServerSpecToProjectConfig(input.serverSpec);
    await (0, promises_1.mkdir)(node_path_1.default.dirname(configPath), { recursive: true });
    await (0, promises_1.writeFile)(configPath, `${JSON.stringify({ ...current, mcpServers }, null, 2)}\n`, "utf8");
}
async function readProjectMcpConfig(configPath) {
    try {
        const parsed = JSON.parse(await (0, promises_1.readFile)(configPath, "utf8"));
        if (parsed !== null &&
            typeof parsed === "object" &&
            !Array.isArray(parsed)) {
            const record = parsed;
            const mcpServers = record.mcpServers;
            return {
                ...record,
                mcpServers: mcpServers !== null &&
                    typeof mcpServers === "object" &&
                    !Array.isArray(mcpServers)
                    ? { ...mcpServers }
                    : {},
            };
        }
    }
    catch (error) {
        if (error.code !== "ENOENT") {
            throw error;
        }
    }
    return { mcpServers: {} };
}
function mcpCatalogServerSpecToProjectConfig(spec) {
    if (spec.transport === "stdio") {
        return {
            command: spec.command,
            ...(spec.args !== undefined ? { args: [...spec.args] } : {}),
            ...(spec.env !== undefined ? { env: { ...spec.env } } : {}),
            ...(spec.cwd !== undefined ? { cwd: spec.cwd } : {}),
        };
    }
    return {
        url: spec.url,
        ...(spec.bearerTokenEnvVar !== undefined
            ? { auth: "bearer", bearerTokenEnv: spec.bearerTokenEnvVar }
            : {}),
        ...(spec.env !== undefined ? { env: { ...spec.env } } : {}),
        ...(spec.headers !== undefined ? { headers: { ...spec.headers } } : {}),
    };
}
function assertSafeAutoInstall(entry) {
    if (entry.setup?.safeAutoInstall === true) {
        return;
    }
    throw new Error(`MCP catalog entry "${entry.id}" is not marked safeAutoInstall. Review its setup.humanSteps and update the catalog before installing it automatically.`);
}
function formatMcpCatalogInstallTargetError(entry, target) {
    if (entry.servers === undefined) {
        const source = entry.quality?.sourceUrl ?? (0, catalog_1.formatMcpCatalogEntrySource)(entry);
        return `MCP catalog entry "${entry.id}" is discovery-only and has no install server spec. Review ${source} and add servers before installing it for ${target}.`;
    }
    return `MCP catalog entry "${entry.id}" cannot be installed for ${target}.`;
}
async function installCustomWorkerMcp(input) {
    const command = ["write", ".mcp.json", input.serverName];
    const nextSteps = buildNextSteps(input.entry, input.input.target, input.serverName);
    await installProjectMcpServer({
        repoRoot: input.input.repoRoot,
        serverName: input.serverName,
        serverSpec: input.serverSpec,
        overwrite: input.input.overwrite === true,
    });
    return {
        kind: exports.MCP_INSTALL_RESULT_KIND,
        schemaVersion: exports.MCP_INSTALL_SCHEMA_VERSION,
        status: "installed",
        target: input.input.target,
        id: input.serverName,
        serverName: input.serverName,
        ref: `${input.input.target}:${input.serverName}`,
        catalogEntryId: input.entry.id,
        source: (0, catalog_1.formatMcpCatalogEntrySource)(input.entry),
        ...(input.entry.quality?.sourceUrl === undefined
            ? {}
            : { sourceUrl: input.entry.quality.sourceUrl }),
        command,
        nextSteps,
    };
}
function findCatalogEntryById(entries, id) {
    const entry = entries.find((candidate) => candidate.id === id);
    if (entry === undefined) {
        throw new Error(`MCP catalog entry not found: ${id}`);
    }
    return entry;
}
async function selectInstallEntryForQuery(input) {
    const query = input.query.trim();
    const exactEntry = input.entries.find((entry) => entry.id.toLowerCase() === query.toLowerCase());
    if (exactEntry !== undefined) {
        return exactEntry;
    }
    const catalogMatches = selectCatalogMatchesForQuery(input);
    const exclusions = await (0, requirement_exclusions_1.loadEffectiveRequirementExclusions)({
        repoRoot: input.repoRoot,
        ...(input.homeDirectory === undefined
            ? {}
            : { homeDirectory: input.homeDirectory }),
    });
    const visibleMatches = catalogMatches.filter((entry) => input.includeExcluded ||
        (0, requirement_exclusions_1.findRequirementExclusionMatch)(exclusions, "mcp", {
            id: entry.id,
            source: (0, catalog_1.formatMcpCatalogEntrySource)(entry),
        }) === null);
    const candidates = visibleMatches.filter((entry) => isInstallableCatalogEntryForTarget(entry, input.target));
    const exact = candidates.filter((entry) => entry.id.toLowerCase() === query.toLowerCase());
    if (exact.length === 1) {
        return exact[0];
    }
    if (exact.length === 0 && candidates.length === 1) {
        return candidates[0];
    }
    if (candidates.length === 0 && visibleMatches.length > 0) {
        throw new Error(formatNonInstallableSearchMatchesError({
            query: input.query,
            target: input.target,
            matches: visibleMatches,
        }));
    }
    return {
        kind: exports.MCP_INSTALL_SELECTION_KIND,
        schemaVersion: exports.MCP_INSTALL_SCHEMA_VERSION,
        repositoryRoot: input.repoRoot,
        reason: "catalog",
        query: input.query,
        target: input.target,
        items: candidates.map(catalogEntryToInstallSelectionItem),
    };
}
function catalogEntryToInstallSelectionItem(entry) {
    return {
        id: entry.id,
        title: entry.id,
        description: entry.description,
        origin: entry.origin,
        ...(entry.quality?.stars === undefined
            ? {}
            : { stars: entry.quality.stars }),
        ...(entry.quality?.updatedAt === undefined
            ? {}
            : { updatedAt: entry.quality.updatedAt }),
        ...(entry.quality?.createdAt === undefined
            ? {}
            : { createdAt: entry.quality.createdAt }),
        ...(entry.quality?.sourceUrl === undefined
            ? {}
            : { sourceUrl: entry.quality.sourceUrl }),
    };
}
function selectCatalogMatchesForQuery(input) {
    return input.entries.filter((entry) => (0, search_text_1.searchTextMatches)({
        id: entry.id,
        title: entry.id,
        description: entry.description,
        topics: entry.topics ?? [],
        topicDescriptions: input.topicDescriptions,
    }, input.query));
}
function isInstallableCatalogEntryForTarget(entry, target) {
    return (entry.setup?.safeAutoInstall === true &&
        (0, catalog_1.resolveMcpCatalogServerForTarget)(entry, target) !== null);
}
function formatNonInstallableSearchMatchesError(input) {
    const counts = {
        discoveryOnly: 0,
        unsupportedTarget: 0,
        unsafe: 0,
    };
    for (const entry of input.matches) {
        if (entry.servers === undefined) {
            counts.discoveryOnly += 1;
        }
        else if ((0, catalog_1.resolveMcpCatalogServerForTarget)(entry, input.target) === null) {
            counts.unsupportedTarget += 1;
        }
        else {
            counts.unsafe += 1;
        }
    }
    const reasonParts = [
        counts.discoveryOnly === 0
            ? null
            : `${String(counts.discoveryOnly)} discovery-only`,
        counts.unsupportedTarget === 0
            ? null
            : `${String(counts.unsupportedTarget)} without a ${input.target} server spec`,
        counts.unsafe === 0
            ? null
            : `${String(counts.unsafe)} not marked safeAutoInstall`,
    ].filter((part) => part !== null);
    const sampleIds = input.matches
        .slice(0, 5)
        .map((entry) => entry.id)
        .join(", ");
    const suffix = input.matches.length > 5
        ? `, and ${String(input.matches.length - 5)} more`
        : "";
    return `MCP catalog search "${input.query}" matched ${String(input.matches.length)} catalog entr${input.matches.length === 1 ? "y" : "ies"}, but none can be installed automatically for ${input.target}: ${reasonParts.join("; ")}. Matching entries: ${sampleIds}${suffix}. Run "alpaca mcp search ${input.query}" to inspect discovery-only or unsafe entries, then add a server spec and setup.safeAutoInstall: true before installing.`;
}
function selectInstallProfile(input) {
    const candidates = input.authProfiles.filter((profile) => (0, store_1.getAuthProfileProvider)(profile) === input.target &&
        (input.authProfileId === undefined || profile.id === input.authProfileId));
    if (input.authProfileId !== undefined) {
        const [profile] = candidates;
        if (profile === undefined) {
            throw new Error(`Auth profile not found for ${input.target}: ${input.authProfileId}`);
        }
        return { profile };
    }
    if (candidates.length > 1) {
        return {
            kind: exports.MCP_INSTALL_SELECTION_KIND,
            schemaVersion: exports.MCP_INSTALL_SCHEMA_VERSION,
            repositoryRoot: input.repoRoot,
            reason: "auth-profile",
            query: input.query,
            target: input.target,
            items: candidates.map((profile) => ({
                id: profile.id,
                title: (0, store_1.formatAuthProfileLabel)(profile, candidates),
                description: (0, store_1.formatAuthProfileDetail)(profile),
                origin: "auth-profile",
            })),
        };
    }
    return { profile: candidates[0] ?? null };
}
function buildInstallEnvironment(input) {
    if (input.profile === null) {
        return input.env;
    }
    const env = (0, environment_1.buildAuthProfileEnvironment)(input.profile, input.env);
    if (env === null) {
        throw new Error(`Auth profile cannot build a command environment: ${input.profile.id}`);
    }
    return env;
}
function assertWorkerScopedInstallEnvironment(input) {
    if (input.target !== "claude") {
        return;
    }
    const claudeConfigDir = Reflect.get(input.env, "CLAUDE_CONFIG_DIR");
    if (typeof claudeConfigDir === "string" && claudeConfigDir.trim() !== "") {
        return;
    }
    throw new Error("Claude MCP install requires a worker CLAUDE_CONFIG_DIR. Select a claude-cli auth profile or run from a worker environment; default ~/.claude is not modified.");
}
async function removeExistingMcpServer(input) {
    const command = input.target === "codex"
        ? (0, provider_codex_1.buildCodexMcpRemoveCommand)(input.serverName)
        : (0, provider_claude_1.buildClaudeMcpRemoveCommand)(input.serverName);
    await input.run({
        command: command[0] ?? "",
        args: command.slice(1),
        cwd: input.repoRoot,
        env: input.env,
        allowNonZeroExit: true,
        timeoutMs: 30_000,
    });
}
function buildNextSteps(entry, target, serverName) {
    const ref = `${target}:${serverName}`;
    return [
        `Run alpaca mcp verify --for ${target} --ref ${ref}.`,
        ...((entry.setup?.requiredEnv ?? []).length === 0
            ? []
            : [
                `Configure required environment variables: ${entry.setup?.requiredEnv?.join(", ")}.`,
            ]),
        ...(entry.setup?.humanSteps ?? []),
    ];
}
