"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BUILD_INPUT_MANIFEST_SCHEMA_VERSION = exports.BUILD_INPUT_MANIFEST_FILE = void 0;
exports.writeBuildInputManifest = writeBuildInputManifest;
exports.inspectBuildInputManifest = inspectBuildInputManifest;
const node_crypto_1 = require("node:crypto");
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
exports.BUILD_INPUT_MANIFEST_FILE = ".day-shift-build-input-manifest.json";
exports.BUILD_INPUT_MANIFEST_SCHEMA_VERSION = 1;
const DECLARED_FILES = [
    "package.json",
    "pnpm-lock.yaml",
    "tsconfig.json",
    "scripts/generate-automation-protocol.js",
    "apps/website/scripts/sync-automation-protocol-schemas.mjs"
];
const DECLARED_DIRECTORIES = [
    "src",
    "schemas/automation"
];
function writeBuildInputManifest(repositoryRoot) {
    const root = node_path_1.default.resolve(repositoryRoot);
    const manifest = createBuildInputManifest(root);
    const outputPath = node_path_1.default.join(root, "dist", exports.BUILD_INPUT_MANIFEST_FILE);
    node_fs_1.default.mkdirSync(node_path_1.default.dirname(outputPath), { recursive: true });
    const temporaryPath = `${outputPath}.${process.pid}.${Date.now()}.tmp`;
    node_fs_1.default.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
    node_fs_1.default.renameSync(temporaryPath, outputPath);
    return manifest;
}
function inspectBuildInputManifest(repositoryRoot) {
    const root = node_path_1.default.resolve(repositoryRoot);
    const manifestPath = node_path_1.default.join(root, "dist", exports.BUILD_INPUT_MANIFEST_FILE);
    const missing = (reason) => ({
        status: "missing",
        manifestPath: relativeManifestPath(root),
        aggregateDigest: null,
        distributionIdentity: null,
        reason
    });
    if (!node_fs_1.default.existsSync(manifestPath))
        return missing("manifest-missing");
    let stored;
    try {
        stored = parseBuildInputManifest(node_fs_1.default.readFileSync(manifestPath, "utf8"));
    }
    catch {
        return {
            status: "malformed",
            manifestPath: relativeManifestPath(root),
            aggregateDigest: null,
            distributionIdentity: null,
            reason: "manifest-malformed"
        };
    }
    let current;
    try {
        current = createBuildInputManifest(root);
    }
    catch {
        return {
            status: "unknown",
            manifestPath: relativeManifestPath(root),
            aggregateDigest: stored.aggregateDigest,
            distributionIdentity: stored.distributionIdentity,
            reason: "manifest-inputs-unverifiable"
        };
    }
    if (!sameToolchain(stored.toolchain, current.toolchain)) {
        return inspection(stored, "incompatible", "toolchain-incompatible", root);
    }
    if (stored.aggregateDigest !== current.aggregateDigest) {
        return inspection(stored, "stale", "build-input-digest-mismatch", root);
    }
    if (stored.distributionIdentity !== current.distributionIdentity) {
        return inspection(stored, "mixed", "distribution-identity-mismatch", root);
    }
    return inspection(stored, "current", "manifest-matched", root);
}
function createBuildInputManifest(repositoryRoot) {
    const inputs = collectManifestInputs(repositoryRoot);
    const toolchain = readToolchain(repositoryRoot);
    return {
        schemaVersion: exports.BUILD_INPUT_MANIFEST_SCHEMA_VERSION,
        aggregateDigest: digest([
            `schema:${exports.BUILD_INPUT_MANIFEST_SCHEMA_VERSION}`,
            ...inputs.map((input) => `input:${input.path}:${input.contentIdentity}`),
            `node-major:${toolchain.nodeMajor}`,
            `package-manager:${toolchain.packageManager ?? "none"}`,
            `typescript:${toolchain.typescript ?? "none"}`
        ]),
        distributionIdentity: createDistributionIdentity(repositoryRoot),
        inputs,
        toolchain
    };
}
function collectManifestInputs(repositoryRoot) {
    const paths = new Set();
    for (const file of DECLARED_FILES)
        addRequiredFile(repositoryRoot, file, paths);
    for (const directory of DECLARED_DIRECTORIES)
        addRequiredDirectory(repositoryRoot, directory, paths);
    return [...paths]
        .sort((left, right) => left.localeCompare(right, "en"))
        .map((relativePath) => ({
        path: relativePath,
        contentIdentity: contentIdentity(node_fs_1.default.readFileSync(node_path_1.default.join(repositoryRoot, relativePath)))
    }));
}
function addRequiredFile(repositoryRoot, relativePath, paths) {
    const absolutePath = node_path_1.default.join(repositoryRoot, relativePath);
    if (!node_fs_1.default.existsSync(absolutePath) || !node_fs_1.default.statSync(absolutePath).isFile()) {
        throw new Error(`Missing declared build input: ${relativePath}`);
    }
    paths.add(relativePath);
}
function addRequiredDirectory(repositoryRoot, relativeDirectory, paths) {
    const absoluteDirectory = node_path_1.default.join(repositoryRoot, relativeDirectory);
    if (!node_fs_1.default.existsSync(absoluteDirectory) || !node_fs_1.default.statSync(absoluteDirectory).isDirectory()) {
        throw new Error(`Missing declared build input directory: ${relativeDirectory}`);
    }
    for (const file of collectFiles(absoluteDirectory)) {
        paths.add(node_path_1.default.relative(repositoryRoot, file).replaceAll(node_path_1.default.sep, "/"));
    }
}
function createDistributionIdentity(repositoryRoot) {
    const distRoot = node_path_1.default.join(repositoryRoot, "dist");
    if (!node_fs_1.default.existsSync(distRoot) || !node_fs_1.default.statSync(distRoot).isDirectory()) {
        throw new Error("Missing compiled dist output.");
    }
    const files = collectFiles(distRoot)
        .filter((file) => node_path_1.default.basename(file) !== exports.BUILD_INPUT_MANIFEST_FILE);
    if (files.length === 0)
        throw new Error("Compiled dist output is empty.");
    return digest(files.map((file) => {
        const relativePath = node_path_1.default.relative(distRoot, file).replaceAll(node_path_1.default.sep, "/");
        return `output:${relativePath}:${contentIdentity(node_fs_1.default.readFileSync(file))}`;
    }));
}
function collectFiles(root) {
    const files = [];
    const pending = [root];
    let totalBytes = 0;
    while (pending.length > 0) {
        const directory = pending.pop();
        for (const entry of node_fs_1.default.readdirSync(directory, { withFileTypes: true })) {
            const entryPath = node_path_1.default.join(directory, entry.name);
            if (entry.isSymbolicLink())
                continue;
            if (entry.isDirectory()) {
                pending.push(entryPath);
                continue;
            }
            if (!entry.isFile())
                continue;
            const stat = node_fs_1.default.statSync(entryPath);
            totalBytes += stat.size;
            if (files.length >= 50_000 || totalBytes > 256_000_000) {
                throw new Error("Build-input manifest exceeds the bounded local hashing budget.");
            }
            files.push(entryPath);
        }
    }
    return files.sort((left, right) => left.localeCompare(right, "en"));
}
function readToolchain(repositoryRoot) {
    const packageJson = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(repositoryRoot, "package.json"), "utf8"));
    return {
        nodeMajor: Number.parseInt(process.versions.node.split(".")[0] ?? "", 10),
        packageManager: typeof packageJson.packageManager === "string" ? packageJson.packageManager : null,
        typescript: typeof packageJson.devDependencies?.typescript === "string"
            ? packageJson.devDependencies.typescript
            : null
    };
}
function parseBuildInputManifest(value) {
    const parsed = JSON.parse(value);
    if (parsed.schemaVersion !== exports.BUILD_INPUT_MANIFEST_SCHEMA_VERSION
        || !isIdentity(parsed.aggregateDigest)
        || !isIdentity(parsed.distributionIdentity)
        || !Array.isArray(parsed.inputs)
        || parsed.inputs.some((input) => !isManifestInput(input))
        || !hasCanonicalInputOrder(parsed.inputs)
        || !isToolchain(parsed.toolchain)) {
        throw new Error("Invalid build input manifest.");
    }
    const expectedAggregate = digest([
        `schema:${parsed.schemaVersion}`,
        ...parsed.inputs.map((input) => `input:${input.path}:${input.contentIdentity}`),
        `node-major:${parsed.toolchain.nodeMajor}`,
        `package-manager:${parsed.toolchain.packageManager ?? "none"}`,
        `typescript:${parsed.toolchain.typescript ?? "none"}`
    ]);
    if (expectedAggregate !== parsed.aggregateDigest)
        throw new Error("Manifest aggregate digest mismatch.");
    return parsed;
}
function isManifestInput(value) {
    return typeof value === "object" && value !== null
        && typeof value.path === "string"
        && isSafeRelativePath(value.path)
        && isIdentity(value.contentIdentity);
}
function isToolchain(value) {
    return typeof value === "object" && value !== null
        && Number.isSafeInteger(value.nodeMajor)
        && (value.packageManager === null || typeof value.packageManager === "string")
        && (value.typescript === null || typeof value.typescript === "string");
}
function hasCanonicalInputOrder(inputs) {
    return inputs.every((input, index) => index === 0 || inputs[index - 1].path.localeCompare(input.path, "en") < 0);
}
function sameToolchain(left, right) {
    return left.nodeMajor === right.nodeMajor
        && left.packageManager === right.packageManager
        && left.typescript === right.typescript;
}
function inspection(manifest, status, reason, repositoryRoot) {
    return {
        status,
        manifestPath: relativeManifestPath(repositoryRoot),
        aggregateDigest: manifest.aggregateDigest,
        distributionIdentity: manifest.distributionIdentity,
        reason
    };
}
function relativeManifestPath(repositoryRoot) {
    return node_path_1.default.relative(repositoryRoot, node_path_1.default.join(repositoryRoot, "dist", exports.BUILD_INPUT_MANIFEST_FILE)).replaceAll(node_path_1.default.sep, "/");
}
function contentIdentity(bytes) {
    return `sha256:${(0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex")}`;
}
function digest(entries) {
    const hash = (0, node_crypto_1.createHash)("sha256");
    for (const entry of entries)
        hash.update(`${entry}\n`, "utf8");
    return `sha256:${hash.digest("hex")}`;
}
function isIdentity(value) {
    return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value);
}
function isSafeRelativePath(value) {
    return value.length > 0
        && !node_path_1.default.isAbsolute(value)
        && !value.split("/").includes("..")
        && !value.includes("\\");
}
