"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SKILL_CATALOG_SOURCE_KEYS = exports.SKILL_CATALOG_FILE_VERSION = exports.SKILL_CATALOG_LIST_SCHEMA_VERSION = exports.SKILL_CATALOG_SCHEMA_VERSION = exports.SKILL_CATALOG_IMPORT_KIND = exports.SKILL_CATALOG_UPDATE_KIND = exports.SKILL_CATALOG_KIND = void 0;
exports.getSkillCatalogDirectoryPath = getSkillCatalogDirectoryPath;
exports.getUserSkillCatalogPath = getUserSkillCatalogPath;
exports.getEjectedSystemSkillCatalogPath = getEjectedSystemSkillCatalogPath;
exports.loadEffectiveSkillCatalog = loadEffectiveSkillCatalog;
exports.listSkillCatalog = listSkillCatalog;
exports.resolveSkillCatalogSourceForTarget = resolveSkillCatalogSourceForTarget;
exports.addSkillCatalogEntry = addSkillCatalogEntry;
exports.importGithubSkillCatalogEntries = importGithubSkillCatalogEntries;
exports.removeSkillCatalogEntry = removeSkillCatalogEntry;
// biome-ignore lint/nursery/noExcessiveLinesPerFile: Skill catalog parsing, import, and normalization stay grouped around one catalog file contract.
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 branding_1 = require("../core/branding");
const fs_errors_1 = require("../core/fs-errors");
const paths_1 = require("../core/paths");
const skill_inventory_1 = require("../workflows/skill-inventory");
const source_uri_1 = require("./source-uri");
const catalog_github_import_1 = require("./catalog-github-import");
exports.SKILL_CATALOG_KIND = "alpaca.skillCatalog";
exports.SKILL_CATALOG_UPDATE_KIND = "alpaca.skillCatalogUpdate";
exports.SKILL_CATALOG_IMPORT_KIND = "alpaca.skillCatalogImport";
exports.SKILL_CATALOG_SCHEMA_VERSION = 3;
exports.SKILL_CATALOG_LIST_SCHEMA_VERSION = 4;
exports.SKILL_CATALOG_FILE_VERSION = 3;
exports.SKILL_CATALOG_SOURCE_KEYS = [
    "default",
    "codex",
    "claude",
];
const skillCatalogSourcesSchema = zod_1.z
    .object({
    default: zod_1.z.string().trim().min(1).optional(),
    codex: zod_1.z.string().trim().min(1).optional(),
    claude: zod_1.z.string().trim().min(1).optional(),
})
    .strict()
    .superRefine((sources, context) => {
    const entries = Object.entries(sources).filter((entry) => entry[1] !== undefined);
    if (entries.length === 0) {
        context.addIssue({
            code: "custom",
            message: "sources must include at least one source.",
        });
        return;
    }
    const seen = new Set();
    for (const [sourceKey, source] of entries) {
        try {
            const parsed = (0, source_uri_1.parseSkillSourceUri)(source);
            if (parsed.kind === "claude-plugin" && sourceKey !== "claude") {
                context.addIssue({
                    code: "custom",
                    path: [sourceKey],
                    message: "claude-plugin sources are only valid for sources.claude.",
                });
            }
            if (parsed.kind === "codex-plugin" && sourceKey !== "codex") {
                context.addIssue({
                    code: "custom",
                    path: [sourceKey],
                    message: "codex-plugin sources are only valid for sources.codex.",
                });
            }
        }
        catch (error) {
            context.addIssue({
                code: "custom",
                path: [sourceKey],
                message: error instanceof Error ? error.message : "Invalid source URI.",
            });
        }
        if (seen.has(source)) {
            context.addIssue({
                code: "custom",
                path: [sourceKey],
                message: "duplicate source URI.",
            });
        }
        seen.add(source);
    }
});
const skillCatalogQualitySchema = 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 skillCatalogEntrySchema = zod_1.z
    .object({
    id: zod_1.z.string().trim().min(1).refine(skill_inventory_1.isValidBareSkillId, {
        message: "id must be a lowercase skill id such as masking or openai-docs.",
    }),
    description: zod_1.z.string().trim().min(1),
    topics: zod_1.z.array(zod_1.z.string().trim().min(1)).optional(),
    quality: skillCatalogQualitySchema.optional(),
    sources: skillCatalogSourcesSchema,
})
    .strict();
const skillCatalogFileSchema = zod_1.z
    .object({
    version: zod_1.z.literal(exports.SKILL_CATALOG_FILE_VERSION),
    entries: zod_1.z.array(skillCatalogEntrySchema),
})
    .strict();
function getSkillCatalogDirectoryPath(repoRoot) {
    return node_path_1.default.join(repoRoot, branding_1.REPO_HOME_DIRECTORY_NAME, paths_1.CATALOG_DIRECTORY);
}
function getUserSkillCatalogPath(repoRoot) {
    return node_path_1.default.join(getSkillCatalogDirectoryPath(repoRoot), paths_1.SKILL_CATALOG_FILENAME);
}
function getEjectedSystemSkillCatalogPath(repoRoot) {
    return node_path_1.default.join(getSkillCatalogDirectoryPath(repoRoot), paths_1.SYSTEM_CATALOG_DIRECTORY, paths_1.SKILL_CATALOG_FILENAME);
}
async function loadEffectiveSkillCatalog(input) {
    const topics = await (0, metadata_1.loadEffectiveCatalogTopics)({ repoRoot: input.repoRoot });
    const knownTopicIds = new Set(topics.topics.map((topic) => topic.id));
    const activeSystem = await loadActiveSystemSkillCatalog(input.repoRoot, knownTopicIds);
    const userCatalogPath = getUserSkillCatalogPath(input.repoRoot);
    const userCatalog = await readOptionalSkillCatalogFile(userCatalogPath, knownTopicIds);
    assertNoDuplicateCatalogIdsAcrossOrigins(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) => ({
                ...entry,
                topics: [...(entry.topics ?? [])],
                ...(entry.quality === undefined
                    ? {}
                    : { quality: { ...entry.quality } }),
                sources: { ...entry.sources },
                origin: "system",
            })),
            ...(userCatalog?.entries ?? []).map((entry) => ({
                ...entry,
                topics: [...(entry.topics ?? [])],
                ...(entry.quality === undefined
                    ? {}
                    : { quality: { ...entry.quality } }),
                sources: { ...entry.sources },
                origin: "user",
            })),
        ].sort(compareEffectiveCatalogEntries),
    };
}
async function listSkillCatalog(input) {
    const catalog = await loadEffectiveSkillCatalog({ 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.SKILL_CATALOG_KIND,
        schemaVersion: exports.SKILL_CATALOG_LIST_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((entry) => ({
            ...entry,
            topics: [...(entry.topics ?? [])],
            ...(entry.quality === undefined ? {} : { quality: { ...entry.quality } }),
            sources: { ...entry.sources },
        })),
    };
}
function resolveSkillCatalogSourceForTarget(entry, target) {
    const source = entry.sources[target] ?? entry.sources.default;
    return source ?? null;
}
async function addSkillCatalogEntry(input) {
    const topics = await (0, metadata_1.loadEffectiveCatalogTopics)({ repoRoot: input.repoRoot });
    const knownTopicIds = new Set(topics.topics.map((topic) => topic.id));
    const addition = normalizeSkillCatalogEntry({
        id: input.id,
        description: input.description,
        topics: [],
        sources: buildSkillCatalogSources({
            default: input.defaultSource,
            codex: input.codexSource,
            claude: input.claudeSource,
        }),
    }, getUserSkillCatalogPath(input.repoRoot), knownTopicIds);
    await assertNotSystemManagedEntry(input.repoRoot, addition.id);
    const userCatalogPath = getUserSkillCatalogPath(input.repoRoot);
    const userCatalog = (await readOptionalSkillCatalogFile(userCatalogPath, knownTopicIds)) ??
        emptySkillCatalog();
    const existingIndex = userCatalog.entries.findIndex((entry) => entry.id === addition.id);
    const entries = existingIndex < 0
        ? [...userCatalog.entries, addition]
        : userCatalog.entries.map((existingEntry, index) => index === existingIndex ? addition : existingEntry);
    await writeSkillCatalogFile(userCatalogPath, {
        version: exports.SKILL_CATALOG_FILE_VERSION,
        entries: entries.sort(compareSkillCatalogEntries),
    });
    return {
        kind: exports.SKILL_CATALOG_UPDATE_KIND,
        schemaVersion: exports.SKILL_CATALOG_SCHEMA_VERSION,
        repositoryRoot: input.repoRoot,
        action: "add",
        userCatalogPath,
        entry: addition,
    };
}
async function importGithubSkillCatalogEntries(input) {
    const env = input.env ?? process.env;
    const repo = (0, catalog_github_import_1.parseGithubRepo)(input.repo);
    const rootPath = (0, catalog_github_import_1.normalizeGithubDirectoryPath)(input.path, "path");
    const skillPaths = await (0, catalog_github_import_1.fetchGithubSkillMarkdownPaths)({
        repo,
        ref: input.ref,
        rootPath,
        env,
    });
    const importedCandidates = [];
    const skippedEntries = [];
    const seenImportIds = new Set();
    const topics = await (0, metadata_1.loadEffectiveCatalogTopics)({ repoRoot: input.repoRoot });
    const knownTopicIds = new Set(topics.topics.map((topic) => topic.id));
    for (const skillPath of skillPaths) {
        const directoryPath = node_path_1.default.posix.dirname(skillPath);
        const fallbackId = node_path_1.default.posix.basename(directoryPath);
        const metadata = await (0, catalog_github_import_1.readGithubSkillMetadata)({
            repo,
            ref: input.ref,
            skillPath,
            env,
        });
        if (metadata === null) {
            skippedEntries.push({
                path: skillPath,
                reason: "missing or invalid SKILL.md frontmatter.",
            });
            continue;
        }
        const id = (0, catalog_github_import_1.resolveImportedSkillId)(metadata.name, fallbackId);
        if (id === null) {
            skippedEntries.push({
                path: skillPath,
                reason: "missing valid skill id.",
            });
            continue;
        }
        if (metadata.description === null || metadata.description.trim() === "") {
            skippedEntries.push({
                path: skillPath,
                id,
                reason: "missing description.",
            });
            continue;
        }
        if (seenImportIds.has(id)) {
            skippedEntries.push({
                path: skillPath,
                id,
                reason: "duplicate imported id.",
            });
            continue;
        }
        seenImportIds.add(id);
        const source = (0, source_uri_1.formatGithubDirectorySourceUri)({
            repo: `${repo.owner}/${repo.repo}`,
            ref: input.ref,
            directoryPath,
        });
        importedCandidates.push({
            skillPath,
            entry: normalizeSkillCatalogEntry({
                id,
                description: metadata.description,
                topics: [],
                sources: buildImportSources(source, input.target),
            }, `github import ${input.repo}:${skillPath}`, knownTopicIds),
        });
    }
    const activeSystem = await loadActiveSystemSkillCatalog(input.repoRoot, knownTopicIds);
    const systemIds = new Set(activeSystem.catalog.entries.map((entry) => entry.id));
    const systemDuplicates = importedCandidates
        .map((candidate) => candidate.entry.id)
        .filter((id) => systemIds.has(id));
    if (systemDuplicates.length > 0) {
        throw new Error(`Skill catalog entries are system-managed: ${systemDuplicates.join(", ")}. Run alpaca eject catalog to customize system catalog entries.`);
    }
    const userCatalogPath = getUserSkillCatalogPath(input.repoRoot);
    const userCatalog = (await readOptionalSkillCatalogFile(userCatalogPath, knownTopicIds)) ??
        emptySkillCatalog();
    const existingById = new Map(userCatalog.entries.map((entry) => [entry.id, entry]));
    const importedEntries = [];
    for (const { skillPath, entry } of importedCandidates) {
        if (existingById.has(entry.id) && !input.overwrite) {
            skippedEntries.push({
                path: skillPath,
                id: entry.id,
                reason: "user entry already exists.",
            });
            continue;
        }
        existingById.set(entry.id, entry);
        importedEntries.push(entry);
    }
    if (importedEntries.length > 0) {
        await writeSkillCatalogFile(userCatalogPath, {
            version: exports.SKILL_CATALOG_FILE_VERSION,
            entries: [...existingById.values()].sort(compareSkillCatalogEntries),
        });
    }
    return {
        kind: exports.SKILL_CATALOG_IMPORT_KIND,
        schemaVersion: exports.SKILL_CATALOG_SCHEMA_VERSION,
        repositoryRoot: input.repoRoot,
        userCatalogPath,
        importedEntries,
        skippedEntries,
    };
}
async function removeSkillCatalogEntry(input) {
    const topics = await (0, metadata_1.loadEffectiveCatalogTopics)({ repoRoot: input.repoRoot });
    const knownTopicIds = new Set(topics.topics.map((topic) => topic.id));
    const userCatalogPath = getUserSkillCatalogPath(input.repoRoot);
    const normalizedId = normalizeSkillCatalogId(input.id, userCatalogPath);
    const existingUserCatalog = await readOptionalSkillCatalogFile(userCatalogPath, knownTopicIds);
    const userCatalog = existingUserCatalog ?? emptySkillCatalog();
    const existingIndex = userCatalog.entries.findIndex((entry) => entry.id === normalizedId);
    if (existingIndex < 0) {
        await assertNotSystemManagedEntry(input.repoRoot, normalizedId, knownTopicIds);
        throw new Error(`Skill catalog user entry not found: ${normalizedId}`);
    }
    const entry = userCatalog.entries[existingIndex];
    if (entry === undefined) {
        throw new Error(`Skill catalog user entry not found: ${normalizedId}`);
    }
    await writeSkillCatalogFile(userCatalogPath, {
        version: exports.SKILL_CATALOG_FILE_VERSION,
        entries: userCatalog.entries
            .filter((_, index) => index !== existingIndex)
            .sort(compareSkillCatalogEntries),
    });
    return {
        kind: exports.SKILL_CATALOG_UPDATE_KIND,
        schemaVersion: exports.SKILL_CATALOG_SCHEMA_VERSION,
        repositoryRoot: input.repoRoot,
        action: "remove",
        userCatalogPath,
        entry,
    };
}
async function loadActiveSystemSkillCatalog(repoRoot, knownTopicIds) {
    const ejectedPath = getEjectedSystemSkillCatalogPath(repoRoot);
    const ejectedCatalog = await readOptionalSkillCatalogFile(ejectedPath, knownTopicIds);
    if (ejectedCatalog !== null) {
        return {
            path: ejectedPath,
            ejected: true,
            catalog: ejectedCatalog,
        };
    }
    const packagePath = (0, paths_1.getPackageSystemSkillCatalogPath)();
    return {
        path: packagePath,
        ejected: false,
        catalog: await readSkillCatalogFile(packagePath, knownTopicIds),
    };
}
function emptySkillCatalog() {
    return {
        version: exports.SKILL_CATALOG_FILE_VERSION,
        entries: [],
    };
}
async function readOptionalSkillCatalogFile(filePath, knownTopicIds) {
    try {
        return await readSkillCatalogFile(filePath, knownTopicIds);
    }
    catch (error) {
        if ((0, fs_errors_1.isMissingFileError)(error)) {
            return null;
        }
        throw error;
    }
}
async function readSkillCatalogFile(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 skill catalog ${filePath}: ${String(error)}`);
    }
    let json;
    try {
        json = JSON.parse(content);
    }
    catch (error) {
        throw new Error(`Invalid skill catalog ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
    }
    return normalizeSkillCatalogFile(json, filePath, knownTopicIds);
}
function normalizeSkillCatalogFile(value, filePath, knownTopicIds) {
    const parsed = skillCatalogFileSchema.safeParse(value);
    if (!parsed.success) {
        throw new Error(`Invalid skill catalog ${filePath}: ${formatZodIssues(parsed.error.issues)}`);
    }
    const entries = parsed.data.entries.map((entry) => ({
        id: entry.id,
        description: entry.description,
        topics: (0, metadata_1.normalizeCatalogTopicIds)(entry.topics ?? []),
        ...(entry.quality === undefined
            ? {}
            : { quality: normalizeSkillCatalogQuality(entry.quality) }),
        sources: sortSkillCatalogSources(entry.sources),
    }));
    assertNoDuplicateCatalogIds(entries, filePath);
    if (knownTopicIds !== undefined) {
        (0, metadata_1.assertKnownCatalogTopicIds)({
            filePath,
            knownTopicIds,
            entries,
        });
    }
    return {
        version: exports.SKILL_CATALOG_FILE_VERSION,
        entries,
    };
}
function normalizeSkillCatalogEntry(value, filePath, knownTopicIds) {
    const entry = normalizeSkillCatalogFile({
        version: exports.SKILL_CATALOG_FILE_VERSION,
        entries: [value],
    }, filePath, knownTopicIds).entries[0];
    if (entry === undefined) {
        throw new Error(`Invalid skill catalog ${filePath}: missing entry.`);
    }
    return entry;
}
function normalizeSkillCatalogId(id, filePath) {
    const normalizedId = id.trim();
    if (!(0, skill_inventory_1.isValidBareSkillId)(normalizedId)) {
        throw new Error(`Invalid skill catalog ${filePath}: id must be a lowercase skill id such as masking or openai-docs.`);
    }
    return normalizedId;
}
function normalizeSkillCatalogQuality(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() }),
    };
}
async function writeSkillCatalogFile(filePath, catalog) {
    await (0, promises_1.mkdir)(node_path_1.default.dirname(filePath), { recursive: true });
    await (0, promises_1.writeFile)(filePath, `${JSON.stringify(normalizeWritableSkillCatalog(catalog), null, 2)}\n`, "utf8");
}
function normalizeWritableSkillCatalog(catalog) {
    return normalizeSkillCatalogFile(catalog, "skill catalog write");
}
function assertNoDuplicateCatalogIds(entries, filePath) {
    const seen = new Set();
    for (const entry of entries) {
        if (seen.has(entry.id)) {
            throw new Error(`Invalid skill catalog ${filePath}: duplicate entry id "${entry.id}".`);
        }
        seen.add(entry.id);
    }
}
function assertNoDuplicateCatalogIdsAcrossOrigins(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 skill catalog ${userCatalogPath}: user entries duplicate system entry ids (${duplicates.join(", ")}). Run alpaca eject catalog to customize system catalog entries.`);
    }
}
async function assertNotSystemManagedEntry(repoRoot, id, knownTopicIds) {
    const activeSystem = await loadActiveSystemSkillCatalog(repoRoot, knownTopicIds);
    if (activeSystem.catalog.entries.some((entry) => entry.id === id)) {
        throw new Error(`Skill catalog entry "${id}" is system-managed. Run alpaca eject catalog to customize the system catalog.`);
    }
}
function buildSkillCatalogSources(input) {
    return sortSkillCatalogSources({
        ...(input.default === undefined ? {} : { default: input.default }),
        ...(input.codex === undefined ? {} : { codex: input.codex }),
        ...(input.claude === undefined ? {} : { claude: input.claude }),
    });
}
function buildImportSources(source, target) {
    switch (target) {
        case "all":
            return { default: source };
        case "codex":
            return { codex: source };
        case "claude":
            return { claude: source };
        default:
            return { default: source };
    }
}
function sortSkillCatalogSources(sources) {
    const sorted = {};
    for (const key of exports.SKILL_CATALOG_SOURCE_KEYS) {
        const source = sources[key];
        if (source !== undefined) {
            sorted[key] = source;
        }
    }
    return sorted;
}
function compareSkillCatalogEntries(left, right) {
    return left.id.localeCompare(right.id);
}
function compareEffectiveCatalogEntries(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("; ");
}
