import fs from "fs";
import os from "os";
import path from "path";
import { execFileSync } from "child_process";
import { parseEnvEntry } from "../utils/env.js";
import { assertSafeWritePath } from "./rootfs.js";
export class DownloadFileError extends Error {
    /** requested download URL */
    url;
    /** upstream HTTP status code when available */
    status;
    constructor(url, options = {}) {
        super(options.message ??
            (options.status !== undefined
                ? `Failed to download ${url}: HTTP ${options.status}`
                : `Failed to download ${url}`), options.cause !== undefined ? { cause: options.cause } : undefined);
        this.name = "DownloadFileError";
        this.url = url;
        this.status = options.status;
    }
}
/** Download helper using Node's built-in fetch */
export async function downloadFile(url, dest) {
    const res = await fetch(url, { redirect: "follow" });
    if (!res.ok) {
        throw new DownloadFileError(url, { status: res.status });
    }
    const buf = Buffer.from(await res.arrayBuffer());
    fs.writeFileSync(dest, buf);
}
export function copyExecutable(src, dest, rootDir) {
    if (rootDir) {
        assertSafeWritePath(dest, rootDir);
    }
    fs.mkdirSync(path.dirname(dest), { recursive: true });
    fs.copyFileSync(src, dest);
    fs.chmodSync(dest, 0o755);
}
export function writeExecutable(dest, content, rootDir) {
    if (rootDir) {
        assertSafeWritePath(dest, rootDir);
    }
    fs.mkdirSync(path.dirname(dest), { recursive: true });
    fs.writeFileSync(dest, content, { mode: 0o755 });
}
export function injectBeforeSandboxdExec(script, snippet) {
    const marker = "\nexec /usr/bin/sandboxd\n";
    const idx = script.lastIndexOf(marker);
    if (idx !== -1) {
        return (script.slice(0, idx) + "\n" + snippet.trimEnd() + "\n" + script.slice(idx));
    }
    return script.trimEnd() + "\n" + snippet.trimEnd() + "\n";
}
export function generateImageEnvScript(env) {
    const entries = normalizeEnvEntries(env);
    if (entries.length === 0)
        return null;
    const lines = entries.map(([key, value]) => `export ${key}=${shSingleQuote(value)}`);
    return ("# Generated by gondolin build\n" +
        "# shellcheck shell=sh\n" +
        lines.join("\n") +
        "\n");
}
function normalizeEnvEntries(env) {
    const map = new Map();
    if (Array.isArray(env)) {
        for (const entry of env) {
            const [key, value] = parseEnvEntry(entry);
            validateEnvKey(key);
            map.set(key, value);
        }
    }
    else {
        for (const [key, value] of Object.entries(env)) {
            validateEnvKey(key);
            map.set(key, value);
        }
    }
    return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
}
function validateEnvKey(key) {
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
        throw new Error(`Invalid env var name for image env: ${JSON.stringify(key)}`);
    }
}
function shSingleQuote(value) {
    return `'${value.replace(/'/g, `'"'"'`)}'`;
}
/** Find mke2fs / mkfs.ext4 binary */
export function findMke2fs() {
    for (const cmd of ["mke2fs", "mkfs.ext4"]) {
        try {
            execFileSync("which", [cmd], { stdio: "pipe" });
            return cmd;
        }
        catch {
            // continue
        }
    }
    if (process.platform === "darwin") {
        const candidates = [
            "/opt/homebrew/opt/e2fsprogs/sbin/mke2fs",
            "/opt/homebrew/opt/e2fsprogs/bin/mke2fs",
            "/opt/homebrew/opt/e2fsprogs/sbin/mkfs.ext4",
            "/opt/homebrew/opt/e2fsprogs/bin/mkfs.ext4",
            "/usr/local/opt/e2fsprogs/sbin/mke2fs",
            "/usr/local/opt/e2fsprogs/bin/mke2fs",
            "/usr/local/opt/e2fsprogs/sbin/mkfs.ext4",
            "/usr/local/opt/e2fsprogs/bin/mkfs.ext4",
        ];
        for (const candidate of candidates) {
            if (fs.existsSync(candidate)) {
                return candidate;
            }
        }
    }
    throw new Error("Missing required command: mke2fs (install e2fsprogs)\n" +
        "On macOS: brew install e2fsprogs\n" +
        "Then ensure mke2fs is on your PATH (Homebrew: brew --prefix e2fsprogs)");
}
/** Create an ext4 rootfs image from a directory tree */
export function createRootfsImage(mkfsCmd, imagePath, sourceDir, label, fixedSizeMb, ownershipEntries = []) {
    let sizeMb;
    if (fixedSizeMb !== undefined) {
        sizeMb = fixedSizeMb;
    }
    else {
        const sizeKb = getDirSizeKb(sourceDir);
        const paddedKb = sizeKb + Math.floor(sizeKb / 5) + 65536;
        sizeMb = Math.ceil(paddedKb / 1024);
    }
    execFileSync(mkfsCmd, [
        "-t",
        "ext4",
        "-d",
        sourceDir,
        "-L",
        label,
        "-m",
        "0",
        "-O",
        "^has_journal",
        "-E",
        "lazy_itable_init=0,lazy_journal_init=0",
        "-b",
        "4096",
        "-F",
        imagePath,
        `${sizeMb}M`,
    ], { stdio: "pipe" });
    applyOwnershipMetadataToRootfsImage(mkfsCmd, imagePath, sourceDir, ownershipEntries);
}
function applyOwnershipMetadataToRootfsImage(mkfsCmd, imagePath, sourceDir, ownershipEntries) {
    if (ownershipEntries.length === 0) {
        return;
    }
    if (typeof process.getuid === "function" && process.getuid() === 0) {
        return;
    }
    const debugfs = findDebugfs(mkfsCmd);
    if (!debugfs) {
        throw new Error("OCI rootfs ownership restoration requires debugfs (install e2fsprogs, and on Alpine add e2fsprogs-extra)");
    }
    const commands = [];
    for (const entry of ownershipEntries) {
        const hostPath = path.join(sourceDir, ...entry.path.split("/"));
        let st;
        try {
            st = fs.lstatSync(hostPath);
        }
        catch (err) {
            const e = err;
            if (e.code === "ENOENT" || e.code === "ENOTDIR") {
                continue;
            }
            throw new Error(`Failed to inspect extracted OCI rootfs path '${entry.path}' before ownership fixup: ${e.message}`);
        }
        if (st.uid === entry.uid && st.gid === entry.gid) {
            continue;
        }
        const imageEntryPath = `/${entry.path}`;
        const quotedPath = quoteDebugfsPath(imageEntryPath);
        commands.push(`sif ${quotedPath} uid ${entry.uid}`);
        commands.push(`sif ${quotedPath} gid ${entry.gid}`);
    }
    if (commands.length === 0) {
        return;
    }
    const cmdFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "gondolin-debugfs-")), "commands.txt");
    try {
        fs.writeFileSync(cmdFile, `${commands.join("\n")}\n`);
        execFileSync(debugfs, ["-w", "-f", cmdFile, imagePath], {
            stdio: ["ignore", "ignore", "pipe"],
            encoding: "utf8",
            maxBuffer: 64 * 1024 * 1024,
        });
    }
    catch (err) {
        const e = err;
        if (e.code === "ENOENT") {
            throw new Error("OCI rootfs ownership restoration requires debugfs (install e2fsprogs, and on Alpine add e2fsprogs-extra)");
        }
        const stdout = typeof e.stdout === "string"
            ? e.stdout
            : Buffer.isBuffer(e.stdout)
                ? e.stdout.toString("utf8")
                : "";
        const stderr = typeof e.stderr === "string"
            ? e.stderr
            : Buffer.isBuffer(e.stderr)
                ? e.stderr.toString("utf8")
                : "";
        throw new Error(`Failed to apply OCI rootfs ownership metadata with debugfs (exit ${String(e.status ?? "?")}):\n` +
            (stdout || stderr ? `${stdout}${stderr}` : ""));
    }
    finally {
        fs.rmSync(path.dirname(cmdFile), { recursive: true, force: true });
    }
}
function findDebugfs(mkfsCmd) {
    const candidates = [];
    if (path.isAbsolute(mkfsCmd)) {
        candidates.push(path.join(path.dirname(mkfsCmd), "debugfs"));
    }
    candidates.push("debugfs");
    if (process.platform === "darwin") {
        candidates.push("/opt/homebrew/opt/e2fsprogs/sbin/debugfs", "/opt/homebrew/opt/e2fsprogs/bin/debugfs", "/usr/local/opt/e2fsprogs/sbin/debugfs", "/usr/local/opt/e2fsprogs/bin/debugfs");
    }
    for (const candidate of candidates) {
        try {
            execFileSync(candidate, ["-V"], { stdio: "ignore" });
            return candidate;
        }
        catch {
            // continue
        }
    }
    return null;
}
function quoteDebugfsPath(pathSpec) {
    if (/[\x00-\x1f\x7f]/.test(pathSpec)) {
        throw new Error(`OCI rootfs ownership path contains unsupported control characters: ${JSON.stringify(pathSpec)}`);
    }
    return `"${pathSpec.replace(/([\\"])/g, "\\$1")}"`;
}
/** Create a compressed initramfs from a directory tree */
export function createInitramfs(sourceDir, outputPath) {
    execFileSync("sh", [
        "-c",
        `cd "${sourceDir}" && find . -print0 | cpio --null -ov --format=newc | lz4 -l -c > "${outputPath}"`,
    ], { stdio: "pipe" });
}
function getDirSizeKb(dir) {
    try {
        const output = execFileSync("du", ["-sk", dir], {
            encoding: "utf8",
            stdio: ["pipe", "pipe", "pipe"],
        });
        return parseInt(output.split(/\s/)[0], 10) || 0;
    }
    catch {
        return Math.ceil(walkDirSize(dir) / 1024);
    }
}
function walkDirSize(dir) {
    let size = 0;
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
        const full = path.join(dir, entry.name);
        if (entry.isSymbolicLink()) {
            continue;
        }
        if (entry.isDirectory()) {
            size += walkDirSize(full);
            continue;
        }
        if (entry.isFile()) {
            size += fs.statSync(full).size;
        }
    }
    return size;
}
//# sourceMappingURL=utils.js.map