import { canonicalJson, isRecord, requiredString, sha256 } from "./canonical.js";
import { HatchError } from "./errors.js";
const DIGEST = /^[a-f0-9]{64}$/u;
export function parseHatchProductPolicy(input) {
    const policy = object(input, "policy");
    exact(policy, [
        "productId", "appId", "packageId", "channel", "publisher", "executableTrust", "installer",
        "allowedSourceRepositories", "sourceBuildConfigurations", "allowedArtifactOrigins",
        "allowedArtifactPathPrefixes", "allowedFeedOrigins", "allowedFeedFormats", "allowedPlatforms",
        "requireCleanSource", "requireExpiry", "maxManifestLifetimeSeconds", "maxArtifactBytes",
        "allowLoopbackHttpForDevelopment",
    ], "policy");
    const publisher = object(policy.publisher, "policy.publisher");
    exact(publisher, ["id", "domain", "allowedKeys"], "policy.publisher");
    const executableTrust = object(policy.executableTrust, "policy.executableTrust");
    exact(executableTrust, ["authority", "verifier", "keyId"], "policy.executableTrust");
    const installer = object(policy.installer, "policy.installer");
    exact(installer, ["adapterId", "kind"], "policy.installer");
    if (executableTrust.authority !== "product-native")
        invalid("Executable trust authority must be product-native.");
    if (!["native-updater", "os-package-manager", "manual"].includes(String(installer.kind))) {
        invalid("Installer kind is unsupported.");
    }
    if (typeof policy.requireCleanSource !== "boolean" || typeof policy.requireExpiry !== "boolean") {
        invalid("Policy security switches must be booleans.");
    }
    if (policy.allowLoopbackHttpForDevelopment !== undefined
        && typeof policy.allowLoopbackHttpForDevelopment !== "boolean") {
        invalid("allowLoopbackHttpForDevelopment must be boolean.");
    }
    if (!Number.isSafeInteger(policy.maxArtifactBytes) || Number(policy.maxArtifactBytes) <= 0) {
        invalid("maxArtifactBytes must be a positive safe integer.");
    }
    if (policy.maxManifestLifetimeSeconds !== undefined
        && (!Number.isSafeInteger(policy.maxManifestLifetimeSeconds)
            || Number(policy.maxManifestLifetimeSeconds) <= 0)) {
        invalid("maxManifestLifetimeSeconds must be a positive safe integer.");
    }
    if (!Array.isArray(publisher.allowedKeys) || publisher.allowedKeys.length === 0 || publisher.allowedKeys.length > 16) {
        invalid("publisher.allowedKeys must contain between one and sixteen product-scoped keys.");
    }
    const allowedKeys = publisher.allowedKeys.map((value, index) => {
        const key = object(value, `policy.publisher.allowedKeys[${index}]`);
        exact(key, ["keyId", "publicKeySpkiSha256"], `policy.publisher.allowedKeys[${index}]`);
        const publicKeySpkiSha256 = requiredString(key.publicKeySpkiSha256, `policy.publisher.allowedKeys[${index}].publicKeySpkiSha256`, "policy_rejected", 64).toLowerCase();
        if (!DIGEST.test(publicKeySpkiSha256))
            invalid("Publisher SPKI digests must be lowercase SHA-256 hex.");
        return {
            keyId: requiredString(key.keyId, `policy.publisher.allowedKeys[${index}].keyId`, "policy_rejected", 200),
            publicKeySpkiSha256,
        };
    }).sort((left, right) => left.keyId.localeCompare(right.keyId));
    if (new Set(allowedKeys.map(({ keyId }) => keyId)).size !== allowedKeys.length) {
        invalid("Publisher key IDs must be unique within a product policy.");
    }
    return {
        productId: token(policy.productId, "policy.productId", 120),
        appId: requiredString(policy.appId, "policy.appId", "policy_rejected", 200),
        packageId: requiredString(policy.packageId, "policy.packageId", "policy_rejected", 200),
        channel: token(policy.channel, "policy.channel", 100),
        publisher: {
            id: requiredString(publisher.id, "policy.publisher.id", "policy_rejected", 200),
            domain: requiredString(publisher.domain, "policy.publisher.domain", "policy_rejected", 253),
            allowedKeys,
        },
        executableTrust: {
            authority: "product-native",
            verifier: token(executableTrust.verifier, "policy.executableTrust.verifier", 200),
            keyId: token(executableTrust.keyId, "policy.executableTrust.keyId", 200),
        },
        installer: {
            adapterId: token(installer.adapterId, "policy.installer.adapterId", 200),
            kind: installer.kind,
        },
        allowedSourceRepositories: stringList(policy.allowedSourceRepositories, "allowedSourceRepositories", 32, 2_000),
        sourceBuildConfigurations: tokenList(policy.sourceBuildConfigurations, "sourceBuildConfigurations", 32, 200),
        allowedArtifactOrigins: originList(policy.allowedArtifactOrigins, "allowedArtifactOrigins", Boolean(policy.allowLoopbackHttpForDevelopment)),
        ...(policy.allowedArtifactPathPrefixes === undefined ? {} : {
            allowedArtifactPathPrefixes: pathList(policy.allowedArtifactPathPrefixes, "allowedArtifactPathPrefixes"),
        }),
        allowedFeedOrigins: originList(policy.allowedFeedOrigins, "allowedFeedOrigins", Boolean(policy.allowLoopbackHttpForDevelopment)),
        allowedFeedFormats: tokenList(policy.allowedFeedFormats, "allowedFeedFormats", 32, 100),
        allowedPlatforms: tokenList(policy.allowedPlatforms, "allowedPlatforms", 32, 100),
        requireCleanSource: policy.requireCleanSource,
        requireExpiry: policy.requireExpiry,
        ...(policy.maxManifestLifetimeSeconds === undefined ? {} : {
            maxManifestLifetimeSeconds: Number(policy.maxManifestLifetimeSeconds),
        }),
        maxArtifactBytes: Number(policy.maxArtifactBytes),
        allowLoopbackHttpForDevelopment: Boolean(policy.allowLoopbackHttpForDevelopment),
    };
}
export function hatchProductPolicyDigest(policy) {
    return sha256(canonicalJson(parseHatchProductPolicy(policy)));
}
function object(value, label) {
    if (!isRecord(value))
        invalid(`${label} must be an object.`);
    return value;
}
function exact(value, allowed, label) {
    const accepted = new Set(allowed);
    const unexpected = Object.keys(value).filter((key) => !accepted.has(key));
    if (unexpected.length)
        invalid(`${label} contains unsupported fields: ${unexpected.sort().join(", ")}.`);
}
function stringList(value, label, maxItems, maxLength) {
    if (!Array.isArray(value) || !value.length || value.length > maxItems)
        invalid(`${label} must be a non-empty bounded array.`);
    const items = value.map((item, index) => requiredString(item, `${label}[${index}]`, "policy_rejected", maxLength)).sort();
    if (new Set(items).size !== items.length)
        invalid(`${label} must not contain duplicates.`);
    return items;
}
function tokenList(value, label, maxItems, maxLength) {
    return stringList(value, label, maxItems, maxLength).map((item) => token(item, label, maxLength));
}
function originList(value, label, allowLoopback) {
    return stringList(value, label, 32, 2_000).map((item) => {
        let url;
        try {
            url = new URL(item);
        }
        catch {
            invalid(`${label} contains an invalid URL origin.`);
        }
        const loopback = ["127.0.0.1", "::1", "localhost"].includes(url.hostname);
        if ((url.protocol !== "https:" && !(allowLoopback && loopback && url.protocol === "http:"))
            || url.username || url.password || url.pathname !== "/" || url.search || url.hash
            || url.origin !== item) {
            invalid(`${label} must contain canonical allowed origins.`);
        }
        return item;
    }).sort();
}
function pathList(value, label) {
    return stringList(value, label, 32, 1_000).map((item) => {
        if (!item.startsWith("/") || item.includes("..") || item.includes("?") || item.includes("#")) {
            invalid(`${label} must contain absolute URL path prefixes without traversal or query fragments.`);
        }
        return item;
    }).sort();
}
function token(value, label, max) {
    const normalized = requiredString(value, label, "policy_rejected", max);
    if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/u.test(normalized) || normalized.includes("..")) {
        invalid(`${label} must be a canonical token.`);
    }
    return normalized;
}
function invalid(message) {
    throw new HatchError("policy_rejected", message);
}
