import { createHash, createPublicKey, verify as verifySignature, } from "node:crypto";
import { stableStringify } from "../../utils/json.js";
import { TufVerificationError, } from "./tufTypes.js";
export class TufMetadataVerifier {
    #trustedRootKeyIds;
    #clock;
    #minimumVersions;
    constructor(options) {
        this.#trustedRootKeyIds = new Set(options.trustedRootKeyIds);
        this.#clock = options.clock ?? (() => new Date());
        this.#minimumVersions = options.minimumVersions ?? {};
    }
    verifyTarget(metadata, targetPath) {
        const { root, targets, snapshot, timestamp } = metadata;
        this.#assertType(root.signed, "root");
        this.#assertType(targets.signed, "targets");
        this.#assertType(snapshot.signed, "snapshot");
        this.#assertType(timestamp.signed, "timestamp");
        this.#assertNotExpired("root", root.signed.expires);
        this.#assertNotExpired("targets", targets.signed.expires);
        this.#assertNotExpired("snapshot", snapshot.signed.expires);
        this.#assertNotExpired("timestamp", timestamp.signed.expires);
        this.#assertMinimumVersion("root", root.signed.version);
        this.#assertMinimumVersion("targets", targets.signed.version);
        this.#assertMinimumVersion("snapshot", snapshot.signed.version);
        this.#assertMinimumVersion("timestamp", timestamp.signed.version);
        this.#verifyRoot(root);
        this.#verifyRole("targets", targets, root.signed);
        this.#verifyRole("snapshot", snapshot, root.signed);
        this.#verifyRole("timestamp", timestamp, root.signed);
        if (snapshot.signed.meta.root.version !== root.signed.version) {
            throw new TufVerificationError("snapshot_version_mismatch", `Snapshot root version ${snapshot.signed.meta.root.version} does not match root version ${root.signed.version}.`);
        }
        if (snapshot.signed.meta.targets.version !== targets.signed.version) {
            throw new TufVerificationError("snapshot_version_mismatch", `Snapshot targets version ${snapshot.signed.meta.targets.version} does not match targets version ${targets.signed.version}.`);
        }
        if (timestamp.signed.meta.snapshot.version !== snapshot.signed.version) {
            throw new TufVerificationError("timestamp_version_mismatch", `Timestamp snapshot version ${timestamp.signed.meta.snapshot.version} does not match snapshot version ${snapshot.signed.version}.`);
        }
        const target = targets.signed.targets[targetPath];
        if (!target) {
            throw new TufVerificationError("unknown_target", `OpenCode target ${targetPath} is not present in targets metadata.`);
        }
        this.#assertTargetAllowed(target, targets.signed);
        return {
            ...target,
            targetPath,
            metadataVersions: {
                root: root.signed.version,
                targets: targets.signed.version,
                snapshot: snapshot.signed.version,
                timestamp: timestamp.signed.version,
            },
        };
    }
    verifyArtifactBytes(target, bytes) {
        const digest = createHash("sha256").update(bytes).digest("hex");
        if (digest !== target.sha256) {
            throw new TufVerificationError("revoked_artifact", `Artifact hash mismatch. Expected ${target.sha256}, got ${digest}.`);
        }
        if (bytes.byteLength !== target.size) {
            throw new TufVerificationError("revoked_artifact", `Artifact size mismatch. Expected ${target.size}, got ${bytes.byteLength}.`);
        }
    }
    #verifyRoot(root) {
        const trustedRole = {
            keyIds: root.signed.roles.root.keyIds.filter((keyId) => this.#trustedRootKeyIds.has(keyId)),
            threshold: root.signed.roles.root.threshold,
        };
        if (trustedRole.keyIds.length === 0) {
            throw new TufVerificationError("missing_trusted_root_key", "Root metadata contains no embedded trusted root key.");
        }
        this.#verifySignatures(root, trustedRole, root.signed);
    }
    #verifyRole(role, envelope, root) {
        const roleSpec = root.roles[role];
        if (!roleSpec) {
            throw new TufVerificationError("missing_role", `Missing role ${role}.`);
        }
        this.#verifySignatures(envelope, roleSpec, root);
    }
    #verifySignatures(envelope, roleSpec, root) {
        const signedBytes = Buffer.from(stableStringify(envelope.signed), "utf8");
        const validKeyIds = new Set();
        for (const signature of envelope.signatures) {
            if (!roleSpec.keyIds.includes(signature.keyId))
                continue;
            if (root.revokedKeyIds.includes(signature.keyId)) {
                throw new TufVerificationError("revoked_key", `Signing key ${signature.keyId} is revoked.`);
            }
            const key = root.keys[signature.keyId];
            if (!key)
                continue;
            try {
                const publicKey = createPublicKey(key.publicKeyPem);
                const isValid = verifySignature(null, signedBytes, publicKey, Buffer.from(signature.sig, "base64"));
                if (isValid)
                    validKeyIds.add(signature.keyId);
            }
            catch {
                throw new TufVerificationError("signature_invalid", `Invalid signature for key ${signature.keyId}.`);
            }
        }
        if (validKeyIds.size < roleSpec.threshold) {
            throw new TufVerificationError("signature_threshold_not_met", `Only ${validKeyIds.size} valid signature(s), threshold is ${roleSpec.threshold}.`);
        }
    }
    #assertNotExpired(role, expires) {
        const expiry = new Date(expires);
        if (!Number.isFinite(expiry.getTime())) {
            throw new TufVerificationError("expired_metadata", `${role} metadata has invalid expiry ${expires}.`);
        }
        if (expiry <= this.#clock()) {
            throw new TufVerificationError("expired_metadata", `${role} metadata expired at ${expires}.`);
        }
    }
    #assertMinimumVersion(role, version) {
        const minimum = this.#minimumVersions[role];
        if (minimum !== undefined && version < minimum) {
            throw new TufVerificationError("rollback_attempt", `${role} metadata version ${version} is lower than required ${minimum}.`);
        }
    }
    #assertType(value, expected) {
        if (value._type !== expected) {
            throw new TufVerificationError("invalid_metadata_type", `Expected ${expected} metadata, got ${value._type}.`);
        }
    }
    #assertTargetAllowed(target, targets) {
        if (targets.revokedVersions.includes(target.version)) {
            throw new TufVerificationError("revoked_version", `OpenCode version ${target.version} is revoked.`);
        }
        if (targets.revokedArtifactHashes.includes(target.sha256)) {
            throw new TufVerificationError("revoked_artifact", `OpenCode artifact hash ${target.sha256} is revoked.`);
        }
    }
}
