"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CATALOG_TOPICS_SCHEMA_VERSION = exports.CATALOG_TOPICS_KIND = exports.CATALOG_METADATA_FILE_VERSION = void 0;
exports.isValidCatalogTopicId = isValidCatalogTopicId;
exports.getCatalogDirectoryPath = getCatalogDirectoryPath;
exports.getUserCatalogMetadataPath = getUserCatalogMetadataPath;
exports.getEjectedSystemCatalogMetadataPath = getEjectedSystemCatalogMetadataPath;
exports.loadEffectiveCatalogMetadata = loadEffectiveCatalogMetadata;
exports.loadEffectiveCatalogTopics = loadEffectiveCatalogTopics;
exports.listCatalogTopics = listCatalogTopics;
exports.toCatalogTopicMap = toCatalogTopicMap;
exports.assertKnownCatalogTopicIds = assertKnownCatalogTopicIds;
exports.normalizeCatalogTopicIds = normalizeCatalogTopicIds;
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
const zod_1 = require("zod");
const branding_1 = require("../core/branding");
const fs_errors_1 = require("../core/fs-errors");
const paths_1 = require("../core/paths");
const requirement_exclusion_model_1 = require("./requirement-exclusion-model");
exports.CATALOG_METADATA_FILE_VERSION = 3;
exports.CATALOG_TOPICS_KIND = "alpaca.catalogTopics";
exports.CATALOG_TOPICS_SCHEMA_VERSION = 1;
const TOPIC_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
const metadataFileSchema = zod_1.z
    .object({
    version: zod_1.z.literal(exports.CATALOG_METADATA_FILE_VERSION),
    topics: zod_1.z.record(zod_1.z.string(), zod_1.z.string().trim().min(1)),
    requirementExclusions: requirement_exclusion_model_1.requirementExclusionsSchema.optional(),
})
    .strict();
function isValidCatalogTopicId(value) {
    return TOPIC_ID_PATTERN.test(value);
}
function getCatalogDirectoryPath(repoRoot) {
    return node_path_1.default.join(repoRoot, branding_1.REPO_HOME_DIRECTORY_NAME, paths_1.CATALOG_DIRECTORY);
}
function getUserCatalogMetadataPath(repoRoot) {
    return node_path_1.default.join(getCatalogDirectoryPath(repoRoot), paths_1.CATALOG_METADATA_FILENAME);
}
function getEjectedSystemCatalogMetadataPath(repoRoot) {
    return node_path_1.default.join(getCatalogDirectoryPath(repoRoot), paths_1.SYSTEM_CATALOG_DIRECTORY, paths_1.CATALOG_METADATA_FILENAME);
}
async function loadEffectiveCatalogMetadata(input) {
    const activeSystem = await loadActiveSystemMetadata(input.repoRoot);
    const userMetadataPath = getUserCatalogMetadataPath(input.repoRoot);
    const userMetadata = await readOptionalCatalogMetadataFile(userMetadataPath);
    const topics = [
        ...topicsFileToItems(activeSystem.metadata, "system"),
        ...topicsFileToItems(userMetadata, "user"),
    ].sort(compareCatalogTopics);
    const requirementExclusions = [
        ...(0, requirement_exclusion_model_1.requirementExclusionsFileToItems)(activeSystem.metadata, "system", activeSystem.path),
        ...(0, requirement_exclusion_model_1.requirementExclusionsFileToItems)(userMetadata, "user", userMetadataPath),
    ].sort(requirement_exclusion_model_1.compareRequirementExclusions);
    assertNoDuplicateTopicIdsAcrossOrigins(activeSystem.metadata, userMetadata, userMetadataPath);
    return {
        repositoryRoot: input.repoRoot,
        systemMetadataPath: activeSystem.path,
        userMetadataPath,
        systemMetadataEjected: activeSystem.ejected,
        userMetadataExists: userMetadata !== null,
        topics,
        requirementExclusions,
    };
}
async function loadEffectiveCatalogTopics(input) {
    const metadata = await loadEffectiveCatalogMetadata({
        repoRoot: input.repoRoot,
    });
    return {
        repositoryRoot: metadata.repositoryRoot,
        systemCatalogPath: metadata.systemMetadataPath,
        userCatalogPath: metadata.userMetadataPath,
        systemCatalogEjected: metadata.systemMetadataEjected,
        userCatalogExists: metadata.userMetadataExists,
        topics: metadata.topics,
    };
}
async function listCatalogTopics(input) {
    const topics = await loadEffectiveCatalogTopics({ repoRoot: input.repoRoot });
    return {
        kind: exports.CATALOG_TOPICS_KIND,
        schemaVersion: exports.CATALOG_TOPICS_SCHEMA_VERSION,
        repositoryRoot: topics.repositoryRoot,
        systemCatalog: {
            path: topics.systemCatalogPath,
            ejected: topics.systemCatalogEjected,
            visible: true,
            included: true,
        },
        userCatalog: {
            path: topics.userCatalogPath,
            exists: topics.userCatalogExists,
        },
        topics: topics.topics.map((topic) => ({ ...topic })),
    };
}
function toCatalogTopicMap(topics) {
    const items = "topics" in topics ? topics.topics : topics;
    return new Map(items.map((topic) => [topic.id, topic.description]));
}
function assertKnownCatalogTopicIds(input) {
    for (const entry of input.entries) {
        for (const topic of entry.topics) {
            if (!input.knownTopicIds.has(topic)) {
                throw new Error(`Invalid catalog ${input.filePath}: entry "${entry.id}" references unknown topic "${topic}".`);
            }
        }
    }
}
function normalizeCatalogTopicIds(topics) {
    return [...new Set(topics.map((topic) => topic.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right));
}
async function loadActiveSystemMetadata(repoRoot) {
    const ejectedPath = getEjectedSystemCatalogMetadataPath(repoRoot);
    const ejectedMetadata = await readOptionalCatalogMetadataFile(ejectedPath);
    if (ejectedMetadata !== null) {
        return { path: ejectedPath, ejected: true, metadata: ejectedMetadata };
    }
    const packagePath = (0, paths_1.getPackageSystemCatalogMetadataPath)();
    return {
        path: packagePath,
        ejected: false,
        metadata: await readCatalogMetadataFile(packagePath),
    };
}
async function readOptionalCatalogMetadataFile(filePath) {
    try {
        return await readCatalogMetadataFile(filePath);
    }
    catch (error) {
        if ((0, fs_errors_1.isMissingFileError)(error)) {
            return null;
        }
        throw error;
    }
}
async function readCatalogMetadataFile(filePath) {
    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 catalog metadata ${filePath}: ${String(error)}`);
    }
    let json;
    try {
        json = JSON.parse(content);
    }
    catch (error) {
        throw new Error(`Invalid catalog metadata ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
    }
    return normalizeCatalogMetadataFile(json, filePath);
}
function normalizeCatalogMetadataFile(value, filePath) {
    const parsed = metadataFileSchema.safeParse(value);
    if (!parsed.success) {
        throw new Error(`Invalid catalog metadata ${filePath}: ${formatZodIssues(parsed.error.issues)}`);
    }
    const normalizedTopics = {};
    for (const [id, description] of Object.entries(parsed.data.topics).sort(([left], [right]) => left.localeCompare(right))) {
        if (!isValidCatalogTopicId(id)) {
            throw new Error(`Invalid catalog metadata ${filePath}: topic id must be kebab-case: ${id}`);
        }
        normalizedTopics[id] = description.trim();
    }
    return {
        version: exports.CATALOG_METADATA_FILE_VERSION,
        topics: normalizedTopics,
        requirementExclusions: (0, requirement_exclusion_model_1.normalizeRequirementExclusions)(parsed.data.requirementExclusions),
    };
}
function topicsFileToItems(metadata, origin) {
    return Object.entries(metadata?.topics ?? {}).map(([id, description]) => ({
        id,
        description,
        origin,
    }));
}
function assertNoDuplicateTopicIdsAcrossOrigins(systemMetadata, userMetadata, userMetadataPath) {
    if (userMetadata === null) {
        return;
    }
    const systemIds = new Set(Object.keys(systemMetadata.topics));
    const duplicates = Object.keys(userMetadata.topics).filter((id) => systemIds.has(id));
    if (duplicates.length > 0) {
        throw new Error(`Invalid catalog metadata ${userMetadataPath}: duplicate system topic ids: ${duplicates.join(", ")}`);
    }
}
function compareCatalogTopics(left, right) {
    return left.id.localeCompare(right.id);
}
function formatZodIssues(issues) {
    return issues
        .map((issue) => {
        const location = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
        return `${location}${issue.message}`;
    })
        .join("; ");
}
