"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BUILT_IN_MCP_CATALOG_SERVER_KEYS = exports.MCP_CATALOG_FILE_VERSION = exports.MCP_CATALOG_SCHEMA_VERSION = exports.MCP_CATALOG_UPDATE_KIND = exports.MCP_CATALOG_KIND = void 0;
exports.formatMcpCatalogEntrySource = formatMcpCatalogEntrySource;
exports.getMcpCatalogDirectoryPath = getMcpCatalogDirectoryPath;
exports.getUserMcpCatalogPath = getUserMcpCatalogPath;
exports.getEjectedSystemMcpCatalogPath = getEjectedSystemMcpCatalogPath;
exports.loadEffectiveMcpCatalog = loadEffectiveMcpCatalog;
exports.listMcpCatalog = listMcpCatalog;
exports.addMcpCatalogEntry = addMcpCatalogEntry;
exports.removeMcpCatalogEntry = removeMcpCatalogEntry;
exports.resolveMcpCatalogServerForTarget = resolveMcpCatalogServerForTarget;
exports.normalizeMcpCatalogEntry = normalizeMcpCatalogEntry;
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
const zod_1 = require("zod");
const metadata_1 = require("../catalog/metadata");
const control_plane_config_1 = require("../config/control-plane-config");
const branding_1 = require("../core/branding");
const fs_errors_1 = require("../core/fs-errors");
const paths_1 = require("../core/paths");
const structured_clone_1 = require("../core/structured-clone");
const catalog_secret_validation_1 = require("./catalog-secret-validation");
const ref_1 = require("./ref");
exports.MCP_CATALOG_KIND = "alpaca.mcpCatalog";
exports.MCP_CATALOG_UPDATE_KIND = "alpaca.mcpCatalogUpdate";
exports.MCP_CATALOG_SCHEMA_VERSION = 3;
exports.MCP_CATALOG_FILE_VERSION = 3;
exports.BUILT_IN_MCP_CATALOG_SERVER_KEYS = [
    "default",
    "codex",
    "claude",
];
function formatMcpCatalogEntrySource(entry) {
    return entry.quality?.sourceUrl ?? `${entry.origin} catalog:${entry.id}`;
}
const envSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.string());
const stdioServerSchema = zod_1.z
    .object({
    transport: zod_1.z.literal("stdio"),
    command: zod_1.z.string().trim().min(1),
    args: zod_1.z.array(zod_1.z.string()).optional(),
    env: envSchema.optional(),
    cwd: zod_1.z.string().trim().min(1).optional(),
})
    .strict()
    .superRefine((server, context) => {
    addSecretStorageIssues(context, (0, catalog_secret_validation_1.collectStdioSecretIssues)(server));
    addSecretStorageIssues(context, (0, catalog_secret_validation_1.collectEnvSecretIssues)(server.env));
});
const remoteServerSchema = zod_1.z
    .object({
    transport: zod_1.z.union([zod_1.z.literal("http"), zod_1.z.literal("sse")]),
    url: zod_1.z.string().trim().min(1),
    bearerTokenEnvVar: zod_1.z.string().trim().min(1).optional(),
    env: envSchema.optional(),
    headers: envSchema.optional(),
})
    .strict()
    .superRefine((server, context) => {
    addSecretStorageIssues(context, (0, catalog_secret_validation_1.collectRemoteSecretIssues)(server));
    addSecretStorageIssues(context, (0, catalog_secret_validation_1.collectEnvSecretIssues)(server.env));
    addSecretStorageIssues(context, (0, catalog_secret_validation_1.collectHeaderSecretIssues)(server.headers));
});
const serverSchema = zod_1.z.union([stdioServerSchema, remoteServerSchema]);
const serverKeySchema = zod_1.z
    .string()
    .refine((key) => exports.BUILT_IN_MCP_CATALOG_SERVER_KEYS.includes(key) ||
    control_plane_config_1.customWorkerIdSchema.safeParse(key).success, {
    message: "server key must be default, a built-in worker, or a valid custom worker id.",
});
const serversSchema = zod_1.z
    .record(serverKeySchema, serverSchema)
    .superRefine((servers, context) => {
    if (Object.values(servers).every((server) => server === undefined)) {
        context.addIssue({
            code: "custom",
            message: "servers must include at least one server spec.",
        });
    }
});
const setupSchema = zod_1.z
    .object({
    safeAutoInstall: zod_1.z.boolean().optional(),
    requiredEnv: zod_1.z.array(zod_1.z.string().trim().min(1)).optional(),
    humanSteps: zod_1.z.array(zod_1.z.string().trim().min(1)).optional(),
})
    .strict()
    .superRefine((setup, context) => {
    addSecretStorageIssues(context, (0, catalog_secret_validation_1.collectSetupSecretIssues)(setup));
});
const qualitySchema = zod_1.z
    .object({
    stars: zod_1.z.number().int().min(0).optional(),
    updatedAt: zod_1.z.string().trim().datetime({ offset: true }).optional(),
    createdAt: zod_1.z.string().trim().datetime({ offset: true }).optional(),
    sourceUrl: zod_1.z.string().trim().url().optional(),
})
    .strict()
    .superRefine((quality, context) => {
    if (Object.values(quality).every((value) => value === undefined)) {
        context.addIssue({
            code: "custom",
            message: "quality must include at least one metadata field.",
        });
    }
});
const entrySchema = zod_1.z
    .object({
    id: zod_1.z.string().trim().min(1).refine(ref_1.isValidMcpServerName, {
        message: "id must be a valid MCP server name.",
    }),
    description: zod_1.z.string().trim().min(1),
    topics: zod_1.z.array(zod_1.z.string().trim().min(1)).optional(),
    quality: qualitySchema.optional(),
    servers: serversSchema.optional(),
    setup: setupSchema.optional(),
})
    .strict();
const catalogFileSchema = zod_1.z
    .object({
    version: zod_1.z.literal(exports.MCP_CATALOG_FILE_VERSION),
    entries: zod_1.z.array(entrySchema),
})
    .strict();
function addSecretStorageIssues(context, issues) {
    for (const issue of issues) {
        context.addIssue({
            code: "custom",
            path: issue.path,
            message: issue.message,
        });
    }
}
function getMcpCatalogDirectoryPath(repoRoot) {
    return node_path_1.default.join(repoRoot, branding_1.REPO_HOME_DIRECTORY_NAME, paths_1.CATALOG_DIRECTORY);
}
function getUserMcpCatalogPath(repoRoot) {
    return node_path_1.default.join(getMcpCatalogDirectoryPath(repoRoot), paths_1.MCP_CATALOG_FILENAME);
}
function getEjectedSystemMcpCatalogPath(repoRoot) {
    return node_path_1.default.join(getMcpCatalogDirectoryPath(repoRoot), paths_1.SYSTEM_CATALOG_DIRECTORY, paths_1.MCP_CATALOG_FILENAME);
}
async function loadEffectiveMcpCatalog(input) {
    const topics = await (0, metadata_1.loadEffectiveCatalogTopics)({ repoRoot: input.repoRoot });
    const knownTopicIds = new Set(topics.topics.map((topic) => topic.id));
    const activeSystem = await loadActiveSystemMcpCatalog(input.repoRoot, knownTopicIds);
    const userCatalogPath = getUserMcpCatalogPath(input.repoRoot);
    const userCatalog = await readOptionalMcpCatalogFile(userCatalogPath, knownTopicIds);
    assertNoDuplicateIdsAcrossOrigins(activeSystem.catalog.entries, userCatalog?.entries ?? [], userCatalogPath);
    return {
        repositoryRoot: input.repoRoot,
        systemCatalogPath: activeSystem.path,
        userCatalogPath,
        systemCatalogEjected: activeSystem.ejected,
        userCatalogExists: userCatalog !== null,
        entries: [
            ...activeSystem.catalog.entries.map((entry) => ({
                ...cloneMcpCatalogEntry(entry),
                origin: "system",
            })),
            ...(userCatalog?.entries ?? []).map((entry) => ({
                ...cloneMcpCatalogEntry(entry),
                origin: "user",
            })),
        ].sort(compareEffectiveMcpCatalogEntries),
    };
}
async function listMcpCatalog(input) {
    const catalog = await loadEffectiveMcpCatalog({ repoRoot: input.repoRoot });
    const includeSystem = input.includeSystem ?? false;
    const includeSystemEntries = catalog.systemCatalogEjected || includeSystem;
    const visibleEntries = catalog.entries.filter((entry) => entry.origin === "user" || includeSystemEntries);
    return {
        kind: exports.MCP_CATALOG_KIND,
        schemaVersion: exports.MCP_CATALOG_SCHEMA_VERSION,
        repositoryRoot: input.repoRoot,
        systemCatalog: {
            path: includeSystemEntries ? catalog.systemCatalogPath : null,
            ejected: catalog.systemCatalogEjected,
            visible: includeSystemEntries,
            included: includeSystemEntries,
        },
        userCatalog: {
            path: catalog.userCatalogPath,
            exists: catalog.userCatalogExists,
        },
        items: visibleEntries.map(cloneEffectiveMcpCatalogEntry),
    };
}
async function addMcpCatalogEntry(input) {
    const topics = await (0, metadata_1.loadEffectiveCatalogTopics)({ repoRoot: input.repoRoot });
    const knownTopicIds = new Set(topics.topics.map((topic) => topic.id));
    const addition = normalizeMcpCatalogEntry(input.entry, getUserMcpCatalogPath(input.repoRoot), knownTopicIds);
    await assertNotSystemManagedEntry(input.repoRoot, addition.id);
    const userCatalogPath = getUserMcpCatalogPath(input.repoRoot);
    const userCatalog = (await readOptionalMcpCatalogFile(userCatalogPath, knownTopicIds)) ??
        emptyMcpCatalog();
    const existingIndex = userCatalog.entries.findIndex((entry) => entry.id === addition.id);
    if (existingIndex >= 0 && input.overwrite !== true) {
        throw new Error(`MCP catalog user entry already exists: ${addition.id}`);
    }
    const entries = existingIndex < 0
        ? [...userCatalog.entries, addition]
        : userCatalog.entries.map((entry, index) => index === existingIndex ? addition : entry);
    await writeMcpCatalogFile(userCatalogPath, {
        version: exports.MCP_CATALOG_FILE_VERSION,
        entries: entries.sort(compareMcpCatalogEntries),
    });
    return {
        kind: exports.MCP_CATALOG_UPDATE_KIND,
        schemaVersion: exports.MCP_CATALOG_SCHEMA_VERSION,
        repositoryRoot: input.repoRoot,
        action: "add",
        userCatalogPath,
        entry: addition,
    };
}
async function removeMcpCatalogEntry(input) {
    const topics = await (0, metadata_1.loadEffectiveCatalogTopics)({ repoRoot: input.repoRoot });
    const knownTopicIds = new Set(topics.topics.map((topic) => topic.id));
    const userCatalogPath = getUserMcpCatalogPath(input.repoRoot);
    const id = normalizeMcpCatalogId(input.id, userCatalogPath);
    const existingUserCatalog = await readOptionalMcpCatalogFile(userCatalogPath, knownTopicIds);
    const userCatalog = existingUserCatalog ?? emptyMcpCatalog();
    const existingIndex = userCatalog.entries.findIndex((entry) => entry.id === id);
    if (existingIndex < 0) {
        await assertNotSystemManagedEntry(input.repoRoot, id, knownTopicIds);
        throw new Error(`MCP catalog user entry not found: ${id}`);
    }
    const entry = userCatalog.entries[existingIndex];
    if (entry === undefined) {
        throw new Error(`MCP catalog user entry not found: ${id}`);
    }
    await writeMcpCatalogFile(userCatalogPath, {
        version: exports.MCP_CATALOG_FILE_VERSION,
        entries: userCatalog.entries
            .filter((_, index) => index !== existingIndex)
            .sort(compareMcpCatalogEntries),
    });
    return {
        kind: exports.MCP_CATALOG_UPDATE_KIND,
        schemaVersion: exports.MCP_CATALOG_SCHEMA_VERSION,
        repositoryRoot: input.repoRoot,
        action: "remove",
        userCatalogPath,
        entry,
    };
}
function resolveMcpCatalogServerForTarget(entry, target) {
    return entry.servers?.[target] ?? entry.servers?.default ?? null;
}
async function loadActiveSystemMcpCatalog(repoRoot, knownTopicIds) {
    const ejectedPath = getEjectedSystemMcpCatalogPath(repoRoot);
    const ejectedCatalog = await readOptionalMcpCatalogFile(ejectedPath, knownTopicIds);
    if (ejectedCatalog !== null) {
        return {
            path: ejectedPath,
            ejected: true,
            catalog: ejectedCatalog,
        };
    }
    const packagePath = (0, paths_1.getPackageSystemMcpCatalogPath)();
    return {
        path: packagePath,
        ejected: false,
        catalog: await readMcpCatalogFile(packagePath, knownTopicIds),
    };
}
function emptyMcpCatalog() {
    return {
        version: exports.MCP_CATALOG_FILE_VERSION,
        entries: [],
    };
}
async function readOptionalMcpCatalogFile(filePath, knownTopicIds) {
    try {
        return await readMcpCatalogFile(filePath, knownTopicIds);
    }
    catch (error) {
        if ((0, fs_errors_1.isMissingFileError)(error)) {
            return null;
        }
        throw error;
    }
}
async function readMcpCatalogFile(filePath, knownTopicIds) {
    let content;
    try {
        content = await (0, promises_1.readFile)(filePath, "utf8");
    }
    catch (error) {
        if ((0, fs_errors_1.isMissingFileError)(error)) {
            throw error;
        }
        throw new Error(`Failed to read MCP catalog ${filePath}: ${String(error)}`);
    }
    let json;
    try {
        json = JSON.parse(content);
    }
    catch (error) {
        throw new Error(`Invalid MCP catalog ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
    }
    return normalizeMcpCatalogFile(json, filePath, knownTopicIds);
}
function normalizeMcpCatalogFile(value, filePath, knownTopicIds) {
    const parsed = catalogFileSchema.safeParse(value);
    if (!parsed.success) {
        throw new Error(`Invalid MCP catalog ${filePath}: ${formatZodIssues(parsed.error.issues)}`);
    }
    assertNoDuplicateIds(parsed.data.entries, filePath);
    const entries = parsed.data.entries.map(normalizeParsedMcpCatalogEntry);
    if (knownTopicIds !== undefined) {
        (0, metadata_1.assertKnownCatalogTopicIds)({
            filePath,
            knownTopicIds,
            entries: entries.map((entry) => ({
                id: entry.id,
                topics: entry.topics ?? [],
            })),
        });
    }
    return {
        version: exports.MCP_CATALOG_FILE_VERSION,
        entries,
    };
}
function normalizeMcpCatalogEntry(value, filePath, knownTopicIds) {
    const entry = normalizeMcpCatalogFile({
        version: exports.MCP_CATALOG_FILE_VERSION,
        entries: [value],
    }, filePath, knownTopicIds).entries[0];
    if (entry === undefined) {
        throw new Error(`Invalid MCP catalog ${filePath}: missing entry.`);
    }
    return entry;
}
function normalizeParsedMcpCatalogEntry(entry) {
    return {
        id: entry.id,
        description: entry.description,
        topics: (0, metadata_1.normalizeCatalogTopicIds)(entry.topics ?? []),
        ...(entry.quality === undefined
            ? {}
            : { quality: normalizeMcpCatalogQuality(entry.quality) }),
        ...(entry.servers === undefined
            ? {}
            : { servers: sortParsedMcpCatalogServers(entry.servers) }),
        ...(entry.setup === undefined
            ? {}
            : { setup: normalizeParsedMcpCatalogSetup(entry.setup) }),
    };
}
function normalizeParsedMcpCatalogSetup(setup) {
    return {
        ...(setup.safeAutoInstall === undefined
            ? {}
            : { safeAutoInstall: setup.safeAutoInstall }),
        ...(setup.requiredEnv === undefined
            ? {}
            : { requiredEnv: [...setup.requiredEnv] }),
        ...(setup.humanSteps === undefined
            ? {}
            : { humanSteps: [...setup.humanSteps] }),
    };
}
function normalizeMcpCatalogQuality(quality) {
    return {
        ...(quality.stars === undefined ? {} : { stars: quality.stars }),
        ...(quality.updatedAt === undefined
            ? {}
            : { updatedAt: quality.updatedAt.trim() }),
        ...(quality.createdAt === undefined
            ? {}
            : { createdAt: quality.createdAt.trim() }),
        ...(quality.sourceUrl === undefined
            ? {}
            : { sourceUrl: quality.sourceUrl.trim() }),
    };
}
function normalizeMcpCatalogId(id, filePath) {
    const normalizedId = id.trim();
    if (!(0, ref_1.isValidMcpServerName)(normalizedId)) {
        throw new Error(`Invalid MCP catalog ${filePath}: id must be a valid MCP server name.`);
    }
    return normalizedId;
}
async function writeMcpCatalogFile(filePath, catalog) {
    await (0, promises_1.mkdir)(node_path_1.default.dirname(filePath), { recursive: true });
    await (0, promises_1.writeFile)(filePath, `${JSON.stringify(normalizeMcpCatalogFile(catalog, "MCP catalog write"), null, 2)}\n`, "utf8");
}
async function assertNotSystemManagedEntry(repoRoot, id, knownTopicIds) {
    const activeSystem = await loadActiveSystemMcpCatalog(repoRoot, knownTopicIds);
    if (activeSystem.catalog.entries.some((entry) => entry.id === id)) {
        throw new Error(`MCP catalog entry "${id}" is system-managed. Run alpaca eject catalog to customize the system catalog.`);
    }
}
function assertNoDuplicateIds(entries, filePath) {
    const seen = new Set();
    for (const entry of entries) {
        if (seen.has(entry.id)) {
            throw new Error(`Invalid MCP catalog ${filePath}: duplicate entry id "${entry.id}".`);
        }
        seen.add(entry.id);
    }
}
function assertNoDuplicateIdsAcrossOrigins(systemEntries, userEntries, userCatalogPath) {
    const systemIds = new Set(systemEntries.map((entry) => entry.id));
    const duplicates = userEntries
        .map((entry) => entry.id)
        .filter((id) => systemIds.has(id));
    if (duplicates.length > 0) {
        throw new Error(`Invalid MCP catalog ${userCatalogPath}: user entries duplicate system entry ids (${duplicates.join(", ")}). Run alpaca eject catalog to customize system catalog entries.`);
    }
}
function sortParsedMcpCatalogServers(servers) {
    const sorted = {};
    for (const key of exports.BUILT_IN_MCP_CATALOG_SERVER_KEYS) {
        const server = servers[key];
        if (server !== undefined) {
            sorted[key] = normalizeParsedMcpCatalogServerSpec(server);
        }
    }
    for (const key of Object.keys(servers).sort()) {
        if (exports.BUILT_IN_MCP_CATALOG_SERVER_KEYS.includes(key)) {
            continue;
        }
        const server = servers[key];
        if (server !== undefined) {
            sorted[key] = normalizeParsedMcpCatalogServerSpec(server);
        }
    }
    return sorted;
}
function normalizeParsedMcpCatalogServerSpec(server) {
    if (server.transport === "stdio") {
        return {
            transport: "stdio",
            command: server.command,
            ...(server.args === undefined ? {} : { args: [...server.args] }),
            ...(server.env === undefined ? {} : { env: { ...server.env } }),
            ...(server.cwd === undefined ? {} : { cwd: server.cwd }),
        };
    }
    return {
        transport: server.transport,
        url: server.url,
        ...(server.bearerTokenEnvVar === undefined
            ? {}
            : { bearerTokenEnvVar: server.bearerTokenEnvVar }),
        ...(server.env === undefined ? {} : { env: { ...server.env } }),
        ...(server.headers === undefined ? {} : { headers: { ...server.headers } }),
    };
}
function cloneMcpCatalogEntry(entry) {
    return {
        id: entry.id,
        description: entry.description,
        topics: [...(entry.topics ?? [])],
        ...(entry.quality === undefined ? {} : { quality: { ...entry.quality } }),
        ...(entry.servers === undefined
            ? {}
            : { servers: (0, structured_clone_1.cloneStructured)(entry.servers) }),
        ...(entry.setup === undefined
            ? {}
            : { setup: (0, structured_clone_1.cloneStructured)(entry.setup) }),
    };
}
function cloneEffectiveMcpCatalogEntry(entry) {
    return {
        ...cloneMcpCatalogEntry(entry),
        origin: entry.origin,
    };
}
function compareMcpCatalogEntries(left, right) {
    return left.id.localeCompare(right.id);
}
function compareEffectiveMcpCatalogEntries(left, right) {
    return (left.id.localeCompare(right.id) || left.origin.localeCompare(right.origin));
}
function formatZodIssues(issues) {
    return issues
        .map((issue) => {
        const issuePath = issue.path.length === 0 ? "<root>" : issue.path.join(".");
        return `${issuePath}: ${issue.message}`;
    })
        .join("; ");
}
