import { createHash, randomUUID } from "node:crypto";
import { lstat, mkdir, open } from "node:fs/promises";
import path from "node:path";
import { HatchRuntimeError } from "./errors.js";
export async function downloadArtifact(input) {
    const url = new URL(input.url);
    const loopback = ["localhost", "127.0.0.1", "::1"].includes(url.hostname);
    if (url.username || url.password || (url.protocol !== "https:" && !(loopback && url.protocol === "http:"))) {
        throw new HatchRuntimeError("download_rejected", "Hatch artifacts require HTTPS (or explicit loopback development HTTP).");
    }
    await mkdir(input.stagingRoot, { recursive: true, mode: 0o700 });
    const rootStat = await lstat(input.stagingRoot);
    if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || (rootStat.mode & 0o077) !== 0) {
        throw new HatchRuntimeError("download_rejected", "Hatch staging root must be a private non-symlink directory.");
    }
    if (typeof process.getuid === "function" && rootStat.uid !== process.getuid()) {
        throw new HatchRuntimeError("download_rejected", "Hatch staging root must be owned by the updater user.");
    }
    const response = await (input.fetchImpl ?? fetch)(url, {
        method: "GET",
        redirect: "manual",
        headers: { accept: "application/octet-stream", "cache-control": "no-cache" },
    });
    if (!response.ok || response.status >= 300 || !response.body) {
        throw new HatchRuntimeError("download_rejected", `Artifact download failed with HTTP ${response.status}.`);
    }
    const declaredLength = response.headers.get("content-length");
    if (declaredLength !== null && Number(declaredLength) !== input.expectedSizeBytes) {
        throw new HatchRuntimeError("artifact_mismatch", "Artifact Content-Length does not match the signed manifest.");
    }
    const artifactPath = path.join(input.stagingRoot, `artifact-${randomUUID()}`);
    const handle = await open(artifactPath, "wx", 0o600);
    const digest = createHash("sha256");
    let bytes = 0;
    try {
        const reader = response.body.getReader();
        while (true) {
            const { done, value } = await reader.read();
            if (done)
                break;
            bytes += value.byteLength;
            if (bytes > input.expectedSizeBytes) {
                throw new HatchRuntimeError("artifact_mismatch", "Artifact exceeds its signed size.");
            }
            digest.update(value);
            await handle.write(value);
        }
        await handle.sync();
    }
    catch (error) {
        await handle.close();
        throw error;
    }
    await handle.close();
    if (bytes !== input.expectedSizeBytes || digest.digest("hex") !== input.expectedSha256) {
        throw new HatchRuntimeError("artifact_mismatch", "Artifact bytes do not match the signed size and digest.");
    }
    return { artifactPath, bytes };
}
