import { randomUUID } from "node:crypto";
import { assertKeys, canonicalJson, isRecord, requiredString, sha256 } from "./canonical.js";
import { HatchError } from "./errors.js";
import { verifySignedReleaseManifest } from "./manifest.js";
import { hatchProductPolicyDigest, parseHatchProductPolicy } from "./policy.js";
export function createUpdatePlan(input) {
    const policy = parseHatchProductPolicy(input.policy);
    const release = verifySignedReleaseManifest(input.manifest, {
        trustedPublishers: input.trustedPublishers,
        policy,
        now: input.now,
    });
    const canonicalInstalled = parseInstalledIdentity(input.installed, policy);
    if (release.productId !== policy.productId || release.policyDigest !== hatchProductPolicyDigest(policy)) {
        throw new HatchError("installed_identity_mismatch", "The release and product policy do not identify the same product.");
    }
    const artifacts = release.payload.artifacts.filter((artifact) => artifact.platform === canonicalInstalled.platform
        && artifact.architectures?.includes(canonicalInstalled.architecture));
    if (!artifacts.length) {
        throw new HatchError("artifact_unavailable", "No artifact matches the installed platform and architecture.");
    }
    if (artifacts.length !== 1) {
        throw new HatchError("artifact_ambiguous", "More than one artifact matches the installed platform and architecture.");
    }
    const artifact = artifacts[0];
    if (!artifact.kind) {
        throw new HatchError("artifact_unavailable", "The matching artifact does not declare an installer-relevant kind.");
    }
    if (release.payload.build === canonicalInstalled.build
        && canonicalInstalled.artifactSha256
        && canonicalInstalled.artifactSha256 !== artifact.sha256) {
        throw new HatchError("build_conflict", "The installed build and candidate use the same number with different bytes.");
    }
    const status = release.payload.build > canonicalInstalled.build
        ? "update_available"
        : release.payload.build < canonicalInstalled.build ? "manifest_older" : "current";
    const candidate = status === "update_available" ? {
        version: release.payload.version,
        build: release.payload.build,
        platform: canonicalInstalled.platform,
        architecture: canonicalInstalled.architecture,
        artifactKind: artifact.kind,
        artifactUrl: artifact.url,
        artifactSha256: artifact.sha256,
        artifactSizeBytes: artifact.sizeBytes,
        feedOrigin: new URL(release.payload.updateFeed.url).origin,
        manifestSha256: release.signedPayloadSha256,
        publisherKeyId: release.envelope.publisher.keyId,
        executableVerifier: policy.executableTrust.verifier,
        executableKeyId: policy.executableTrust.keyId,
        installerAdapterId: policy.installer.adapterId,
        minimumSystemVersion: release.payload.minimumSystemVersion ?? null,
        minimumUpdaterVersion: release.payload.minimumUpdaterVersion ?? null,
        compatibilityBoundary: "product-adapter-must-verify",
        publishedAt: release.payload.publishedAt,
        expiresAt: release.payload.expiresAt ?? null,
    } : null;
    const digestable = {
        type: "ok.hatch.update-plan.v1",
        schemaVersion: 1,
        status,
        productId: policy.productId,
        policyDigest: release.policyDigest,
        installed: canonicalInstalled,
        candidate,
        requiresNativeConsent: status === "update_available",
        authorizationBoundary: "verify-and-plan-only",
    };
    return { ...digestable, planDigest: sha256(canonicalJson(digestable)) };
}
export function createVerifiedUpdateObservation(input) {
    const plan = createUpdatePlan(input);
    assertDigest(input.deviceScopeSha256, "deviceScopeSha256");
    const digestable = {
        type: "ok.hatch.local-verification-receipt.v1",
        schemaVersion: 1,
        trust: "local-self-attestation",
        receiptId: input.receiptId ?? `hatch_${randomUUID()}`,
        productId: plan.productId,
        deviceScopeSha256: input.deviceScopeSha256,
        status: plan.status,
        installedBuild: plan.installed.build,
        candidateBuild: plan.candidate?.build ?? null,
        planDigest: plan.planDigest,
        policyDigest: plan.policyDigest,
        observedAt: (input.observedAt ?? new Date()).toISOString(),
    };
    return {
        plan,
        receipt: { ...digestable, receiptDigest: sha256(canonicalJson(digestable)) },
    };
}
function parseInstalledIdentity(installed, policy) {
    if (!isRecord(installed)) {
        throw new HatchError("installed_identity_mismatch", "The installed identity is invalid.");
    }
    assertKeys(installed, [
        "productId", "appId", "packageId", "version", "build", "channel", "platform",
        "architecture", "updaterVersion", "systemVersion", "artifactSha256",
    ], "installed identity", "installed_identity_mismatch");
    if (installed.productId !== policy.productId
        || installed.appId !== policy.appId
        || installed.packageId !== policy.packageId
        || installed.channel !== policy.channel
        || !policy.allowedPlatforms.includes(installed.platform)) {
        throw new HatchError("installed_identity_mismatch", "The installed identity is outside this product policy.");
    }
    if (!Number.isSafeInteger(installed.build) || installed.build <= 0) {
        throw new HatchError("installed_identity_mismatch", "The installed build or architecture is invalid.");
    }
    if (installed.artifactSha256)
        assertDigest(installed.artifactSha256, "installed.artifactSha256");
    return {
        productId: requiredString(installed.productId, "installed.productId", "installed_identity_mismatch", 120),
        appId: requiredString(installed.appId, "installed.appId", "installed_identity_mismatch", 200),
        packageId: requiredString(installed.packageId, "installed.packageId", "installed_identity_mismatch", 200),
        version: requiredString(installed.version, "installed.version", "installed_identity_mismatch", 100),
        build: installed.build,
        channel: requiredString(installed.channel, "installed.channel", "installed_identity_mismatch", 100),
        platform: requiredString(installed.platform, "installed.platform", "installed_identity_mismatch", 100),
        architecture: requiredString(installed.architecture, "installed.architecture", "installed_identity_mismatch", 100),
        updaterVersion: requiredString(installed.updaterVersion, "installed.updaterVersion", "installed_identity_mismatch", 100),
        ...(installed.systemVersion === undefined ? {} : {
            systemVersion: requiredString(installed.systemVersion, "installed.systemVersion", "installed_identity_mismatch", 100),
        }),
        ...(installed.artifactSha256 === undefined ? {} : { artifactSha256: installed.artifactSha256 }),
    };
}
function assertDigest(value, label) {
    if (!/^[a-f0-9]{64}$/u.test(value))
        throw new HatchError("receipt_invalid", `${label} must be lowercase SHA-256 hex.`);
}
