//! Boot-image assembly (docs/build-packages-plan.md, phase 3): everything
//! between "here are the built binaries" and "here is a bootable volume".
//! The FHS-shaped zig-out install tree, the boot manifest, the boot capsule,
//! the FAT32 USB image (+ its serial-enabled twin for the QEMU run steps),
//! and the release ISO — with their check steps. The root build.zig decides
//! WHAT ships (the bundled list); this file owns HOW it becomes an image.

const std = @import("std");

/// One user binary and its FHS home on the boot volume (and in zig-out).
pub const BundledBinary = struct { path: []const u8, binary: std.Build.LazyPath };

pub const Options = struct {
    /// The installed/flashable kernel (serial follows the root -Dserial).
    kernel: *std.Build.Step.Compile,
    /// The serial-enabled kernel variant the `run-x86-64` image boots.
    kernel_serial: *std.Build.Step.Compile,
    /// The UEFI loader (BOOTX64).
    efi: *std.Build.Step.Compile,
    /// Every user binary and data file at its FHS path.
    bundled: []const BundledBinary,
};

/// Wire up the install tree, both FAT32 boot images, the release ISO, and the
/// check steps. Returns the serial-enabled FAT image for the QEMU run steps.
pub fn addImageSteps(b: *std.Build, options: Options) std.Build.LazyPath {
    // Everything installs into a FHS-shaped zig-out: it IS the danos filesystem *and*
    // the boot volume. Each binary lands at its addressed, leaf-collapsed path — the
    // kernel at zig-out/system/kernel (from system/kernel/kernel.zig), init at
    // zig-out/system/services/init, and so on (see docs/README.md). The bootloader
    // then loads these FHS paths off the volume.
    const kernel_install = b.addInstallArtifact(options.kernel, .{ .dest_dir = .{ .override = .{ .custom = "system" } } });
    b.getInstallStep().dependOn(&kernel_install.step);

    // UEFI firmware requires the removable-media loader at exactly \EFI\BOOT\BOOTX64.efi,
    // so that path is fixed by the firmware (it is /boot's EFI stub, conceptually).
    const efi_install = b.addInstallArtifact(options.efi, .{ .dest_dir = .{ .override = .{ .custom = "EFI/BOOT" } } });
    b.getInstallStep().dependOn(&efi_install.step);

    // The boot manifest: the FHS path of every bundled binary, one per line. The
    // EFI loader reads THIS by name and opens each listed path by name — FAT
    // name lookup is case-insensitive and firmware-portable, unlike directory
    // ENUMERATION, whose returned names vary by firmware (bare 8.3 entries come
    // back uppercase on some FAT drivers). The tree walk remains only as the
    // loader's fallback for hand-assembled sticks without a manifest.
    var manifest_text: std.ArrayListUnmanaged(u8) = .empty;
    for (options.bundled) |item| {
        manifest_text.append(b.allocator, '/') catch @panic("OOM");
        manifest_text.appendSlice(b.allocator, item.path) catch @panic("OOM");
        manifest_text.append(b.allocator, '\n') catch @panic("OOM");
    }
    const manifest_files = b.addWriteFiles();
    const manifest_file = manifest_files.add("manifest", manifest_text.items);
    const manifest_install = b.addInstallFileWithDir(manifest_file, .prefix, "system/manifest");
    b.getInstallStep().dependOn(&manifest_install.step);

    // The boot capsule: the same bundled list packed into ONE file (v2
    // initial_ramdisk format), because a single open + sequential read is the
    // only firmware file I/O shape that is fast everywhere — a per-file tree
    // walk measured MINUTES on real firmware. The loader tries this first,
    // then the manifest, then the walk; the running system cannot tell the
    // difference (it always receives the same in-RAM table). Derived from the
    // tree in the same build graph, so the two cannot drift.
    const mk_capsule = b.addSystemCommand(&.{"python3"});
    mk_capsule.addFileArg(b.path("tools/pack-system-image.py"));
    const capsule_img = mk_capsule.addOutputFileArg("system.img");
    for (options.bundled) |item| {
        mk_capsule.addArg(item.path);
        mk_capsule.addFileArg(item.binary);
    }
    const capsule_install = b.addInstallFile(capsule_img, "boot/system.img");
    b.getInstallStep().dependOn(&capsule_install.step);

    // Install every bundled binary to its FHS home, so zig-out is a true image of
    // the filesystem — the same tree make-fat-image.py lays out on the boot volume.
    for (options.bundled) |item| {
        const install = b.addInstallFileWithDir(item.binary, .prefix, item.path);
        b.getInstallStep().dependOn(&install.step);
    }

    // --- danos-usb.img: the bootable FAT32 USB image ---
    // Format a real FAT32 image (the in-repo Python builder, no external tools)
    // holding the EFI stub, the kernel, and the whole /system tree of user
    // binaries at their FHS paths. QEMU presents this image as a USB mass-storage
    // device the guest boots from (see run-x86-64 and the test harness), and the
    // danos fat driver mounts the same image at /volumes/usb.
    const fat_image = addBootImage(b, options.kernel.getEmittedBin(), options.efi.getEmittedBin(), manifest_file, capsule_img, options.bundled);
    const fat_image_install = b.addInstallFile(fat_image, "danos-usb.img");
    b.getInstallStep().dependOn(&fat_image_install.step);

    // The image `run-x86-64` boots: identical to the flashable one but with the
    // serial log sink compiled in, so a developer always gets the machine-readable
    // log captured to serial0 — without baking serial into the image users flash.
    // Built lazily (only when `run-x86-64` is requested), and never installed.
    const fat_image_serial = addBootImage(b, options.kernel_serial.getEmittedBin(), options.efi.getEmittedBin(), manifest_file, capsule_img, options.bundled);

    // `zig build check-fat-image` — validate the produced image is a real FAT32
    // with the EFI stub present (the builder's own --verify, no external tools).
    const check_fat = b.addSystemCommand(&.{"python3"});
    check_fat.addFileArg(b.path("tools/make-fat-image.py"));
    check_fat.addArg("--verify");
    check_fat.addFileArg(fat_image);
    const check_fat_step = b.step("check-fat-image", "Verify the FAT32 USB image is valid and bootable");
    check_fat_step.dependOn(&check_fat.step);

    // --- release-x86-64: danos-x86-64.iso, the flashable release image ---
    // Wrap the FAT32 boot volume in a hybrid ISO (the in-repo Python builder
    // again, no xorriso/isohybrid): an ISO9660 whose El Torito EFI boot entry
    // and MBR ESP partition entry both point at the embedded FAT image. One
    // file then boots every way release media is consumed — flashed raw to a
    // USB stick with Etcher or dd, or burned to optical media — while
    // danos-usb.img stays the raw superfloppy QEMU and the test harness boot.
    const mk_iso = b.addSystemCommand(&.{"python3"});
    mk_iso.addFileArg(b.path("tools/make-iso-image.py"));
    const iso_image = mk_iso.addOutputFileArg("danos-x86-64.iso");
    mk_iso.addFileArg(fat_image);
    const iso_install = b.addInstallFile(iso_image, "danos-x86-64.iso");
    const release_step = b.step("release-x86-64", "Build the flashable x86-64 release ISO (zig-out/danos-x86-64.iso; flash with Etcher or dd)");
    release_step.dependOn(&iso_install.step);

    // `zig build check-iso-image` — the ISO builder's own --verify (mirroring
    // check-fat-image): the MBR partition, the El Torito catalog, and the
    // embedded FAT32 image must all agree.
    const check_iso = b.addSystemCommand(&.{"python3"});
    check_iso.addFileArg(b.path("tools/make-iso-image.py"));
    check_iso.addArg("--verify");
    check_iso.addFileArg(iso_image);
    const check_iso_step = b.step("check-iso-image", "Verify the release ISO is a valid hybrid (MBR ESP partition + El Torito EFI entry)");
    check_iso_step.dependOn(&check_iso.step);

    return fat_image_serial;
}

/// Assemble the bootable FAT32 image (the in-repo Python builder) holding the
/// EFI stub, the kernel, and every user binary at its FHS path — the volume's
/// /system tree IS the system image; the EFI loader walks it at boot and builds
/// the in-RAM initial_ramdisk from it. Factored so the serial-enabled
/// `run-x86-64` variant can bundle its own serial kernel while sharing the
/// loader and user tree (the loader's boot breadcrumbs and init's heartbeat both
/// follow the top-level -Dserial). Returns the image's LazyPath.
fn addBootImage(
    b: *std.Build,
    kernel_bin: std.Build.LazyPath,
    efi_bin: std.Build.LazyPath,
    manifest: std.Build.LazyPath,
    capsule: std.Build.LazyPath,
    bundled: []const BundledBinary,
) std.Build.LazyPath {
    const mk_fat = b.addSystemCommand(&.{"python3"});
    mk_fat.addFileArg(b.path("tools/make-fat-image.py"));
    const fat_image = mk_fat.addOutputFileArg("danos-usb.img");
    mk_fat.addArg("64"); // MiB
    mk_fat.addArg("EFI/BOOT/BOOTX64.efi");
    mk_fat.addFileArg(efi_bin);
    mk_fat.addArg("system/kernel");
    mk_fat.addFileArg(kernel_bin);
    mk_fat.addArg("system/manifest");
    mk_fat.addFileArg(manifest);
    mk_fat.addArg("boot/system.img");
    mk_fat.addFileArg(capsule);
    for (bundled) |item| {
        mk_fat.addArg(item.path);
        mk_fat.addFileArg(item.binary);
    }
    return fat_image;
}
