import { createHash } from "node:crypto";
import { HatchError } from "./errors.js";
export function isRecord(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function stableValue(value) {
    if (Array.isArray(value))
        return value.map(stableValue);
    if (!isRecord(value))
        return value;
    return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
}
export function canonicalJson(value) {
    return encodeCanonical(value, "$");
}
function encodeCanonical(value, path) {
    if (value === null)
        return "null";
    if (typeof value === "boolean")
        return value ? "true" : "false";
    if (typeof value === "string") {
        if (value.normalize("NFC") !== value) {
            throw new HatchError("plan_invalid", `${path} must use Unicode NFC.`);
        }
        return JSON.stringify(value);
    }
    if (typeof value === "number") {
        if (!Number.isSafeInteger(value) || Object.is(value, -0)) {
            throw new HatchError("plan_invalid", `${path} must be a safe integer.`);
        }
        return String(value);
    }
    if (Array.isArray(value)) {
        return `[${value.map((item, index) => encodeCanonical(item, `${path}[${index}]`)).join(",")}]`;
    }
    if (isRecord(value)) {
        const keys = Object.keys(value).sort();
        if (keys.some((key) => !/^[A-Za-z][A-Za-z0-9]*$/u.test(key))) {
            throw new HatchError("plan_invalid", `${path} contains a non-canonical field name.`);
        }
        return `{${keys.map((key) => `${JSON.stringify(key)}:${encodeCanonical(value[key], `${path}.${key}`)}`).join(",")}}`;
    }
    throw new HatchError("plan_invalid", `${path} contains an unsupported canonical value.`);
}
export function sha256(value) {
    return createHash("sha256").update(value).digest("hex");
}
export function decodeCanonicalBase64Url(value, label, code) {
    if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/u.test(value)) {
        throw new HatchError(code, `${label} must be canonical base64url.`);
    }
    const bytes = Buffer.from(value, "base64url");
    if (bytes.length === 0 || bytes.toString("base64url") !== value) {
        throw new HatchError(code, `${label} must be canonical base64url.`);
    }
    return bytes;
}
export function assertKeys(value, allowed, label, code = "manifest_invalid") {
    const accepted = new Set(allowed);
    const unexpected = Object.keys(value).filter((key) => !accepted.has(key));
    if (unexpected.length) {
        throw new HatchError(code, `${label} contains unsupported fields: ${unexpected.sort().join(", ")}.`);
    }
}
export function requiredString(value, label, code = "manifest_invalid", max = 1_000) {
    if (typeof value !== "string" || !value.trim() || value.trim().length > max) {
        throw new HatchError(code, `${label} must be a non-empty string of at most ${max} characters.`);
    }
    const normalized = value.trim();
    if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(normalized)) {
        throw new HatchError(code, `${label} contains unsupported control characters.`);
    }
    return normalized;
}
