#!/usr/bin/env bash
# =============================================================================
# build.sh — Build the M-Archy Arch Linux ISO + netboot artifacts
#
# PURPOSE:
#   This is the primary build entry point for the M-Archy custom Arch Linux
#   installer ISO. It takes the official archiso "releng" base profile and
#   merges the M-Archy overlay on top of it, embedding custom installer
#   scripts and an optional pre-filled answerfile for fully automated
#   unattended installation. It also generates iPXE scripts for netboot-based
#   deployment (PXE/WDS environments).
#
# WORKFLOW OVERVIEW:
#   1. Parse arguments (optional answerfile path, netboot URL, output dir).
#   2. Install archiso if missing.
#   3. Copy the upstream releng profile to a working directory.
#   4. Apply the M-Archy overlay (custom scripts, profiledef, mkinitcpio config).
#   5. Merge extra packages (packages.extra) into the releng package list.
#   6. Embed the installer shell scripts into the ISO's /root/installer/.
#   7. Optionally embed an answerfile.json for automated installs.
#   8. Run mkarchiso to produce the final .iso and netboot tarball.
#   9. Optionally write an iPXE chainload script for netboot.xyz / WDS.
#
# USAGE:
#   bash build.sh [--preconf [FILE]] [--netboot-url URL] [OUT_DIR]
#
#   --preconf          Embed ~/answerfile.json into the ISO at /answerfile.json
#   --preconf FILE     Embed the specified answerfile instead
#   --netboot-url URL  Base URL where netboot artifacts will be served
#                      (generates m-archy-netboot.ipxe in OUT_DIR)
#   OUT_DIR            Output directory (default: ~/m-archy-out, or $OUT_DIR env)
# =============================================================================

set -euo pipefail
# set -e  → abort on any command that exits non-zero
# set -u  → abort if an unset variable is referenced (catches typos)
# set -o pipefail → a pipe fails if any command in it fails (not just the last)

# Resolve this script's directory and the dotfiles root, regardless of where
# the caller's working directory is. Using BASH_SOURCE[0] rather than $0 is
# safer when the script is sourced or called with a relative path.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOTFILES_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
# The archiso overlay (airootfs, profiledef.sh, packages.extra) is profile data
# that lives in setup/archiso/overlay/, not alongside this script in setup/tools/.
OVERLAY_DIR="$DOTFILES_DIR/setup/archiso/overlay"

# ── Argument parsing ───────────────────────────────────────────────────────────
# We support both --flag value and --flag=value syntax.
# PRECONF_FILE   → path to JSON answerfile to embed (empty = no answerfile)
# NETBOOT_URL    → HTTP base URL for generated iPXE script (empty = skip)
# OUT_ARG        → positional output directory override
PRECONF_FILE=""
NETBOOT_URL=""
OUT_ARG=""

while [[ $# -gt 0 ]]; do
    case "$1" in
        --preconf)
            # If the next argument looks like a path (doesn't start with -),
            # treat it as the answerfile path; otherwise use the default location.
            if [[ $# -gt 1 && "${2:0:1}" != "-" ]]; then
                PRECONF_FILE="$2"; shift
            else
                PRECONF_FILE="$HOME/answerfile.json"
            fi
            shift
            ;;
        --preconf=*)
            # Handle --preconf=/path/to/file syntax (strip prefix up to =)
            PRECONF_FILE="${1#--preconf=}"
            shift
            ;;
        --netboot-url)
            [[ $# -gt 1 ]] || { echo "ERROR: --netboot-url requires a URL argument" >&2; exit 1; }
            NETBOOT_URL="$2"; shift 2
            ;;
        --netboot-url=*)
            NETBOOT_URL="${1#--netboot-url=}"
            shift
            ;;
        -*)
            echo "Unknown flag: $1" >&2; exit 1
            ;;
        *)
            # Any non-flag argument is treated as the output directory
            OUT_ARG="$1"; shift
            ;;
    esac
done

# WORK_DIR: scratch space where mkarchiso does its work (can be huge — squashfs
#            compression of the full airootfs happens here).
WORK_DIR="${WORK_DIR:-$HOME/m-archy-build}"

# OUT_DIR: where the final .iso and netboot tarball are written.
#   Priority: positional arg > $OUT_DIR env var > default ~/m-archy-out
OUT_DIR="${OUT_ARG:-${OUT_DIR:-$HOME/m-archy-out}}"

# PROFILE: the assembled profile directory passed to mkarchiso.
#   We copy releng here and then apply our overlay on top.
PROFILE="$WORK_DIR/profile"

# RELENG: the official Arch Linux "release engineering" base profile, installed
#   with the archiso package. It provides sane defaults for pacman.conf, EFI
#   boot entries, and a working live environment.
RELENG="/usr/share/archiso/configs/releng"

# ── Ensure archiso is installed ───────────────────────────────────────────────
# mkarchiso is the tool that actually assembles the ISO from a profile directory.
# It is only available after installing the archiso package.
if ! command -v mkarchiso &>/dev/null; then
    echo "Installing archiso..."
    sudo pacman -S --noconfirm archiso
fi

# Sanity check: the releng base profile must exist.
# If archiso installed correctly, this path will always be present.
[[ -d "$RELENG" ]] || { echo "ERROR: $RELENG not found — is archiso installed?"; exit 1; }

# ── Validate answerfile early (fail fast before the long build) ───────────────
# If --preconf was given, verify the file exists and is valid JSON now rather
# than discovering the problem after a 10-minute build.
if [[ -n "$PRECONF_FILE" ]]; then
    [[ -f "$PRECONF_FILE" ]] \
        || { echo "ERROR: answerfile not found: $PRECONF_FILE"; exit 1; }
    # jq empty parses JSON and exits 0 if valid, non-zero if malformed.
    # Fall through gracefully if jq is not installed.
    command -v jq &>/dev/null \
        && jq empty "$PRECONF_FILE" \
        || echo "Warning: jq not available — skipping answerfile JSON validation"
    echo "Answerfile to embed: $PRECONF_FILE"
fi

# ── Clean and create working directories ─────────────────────────────────────
# Remove any previous build artifacts to ensure a clean, reproducible build.
# mkarchiso can behave unexpectedly if the profile or work directory has stale state.
# Use sudo: mkarchiso runs as root below and leaves root-owned files in WORK_DIR
# if a prior build was interrupted before its own end-of-script chown; a plain
# `rm -rf` would then fail on those files and (under set -e) abort the script
# before mkarchiso ever runs.
sudo rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR" "$OUT_DIR"

# ── Assemble the profile from releng + M-Archy overlay ───────────────────────
echo "Copying releng base profile..."
# Start with a full copy of upstream releng so we inherit all its boot
# entries, pacman.conf, syslinux/systemd-boot configs, etc.
cp -r "$RELENG" "$PROFILE"

echo "Applying M-Archy overlay..."
# Merge our custom airootfs overlay ON TOP of the releng copy.
# Files in our overlay replace or extend the releng defaults.
# The trailing /. ensures we copy the directory contents, not the directory itself.
cp -r "$OVERLAY_DIR/airootfs/." "$PROFILE/airootfs/"

echo "Replacing profiledef..."
# profiledef.sh controls ISO metadata (name, label, build modes, boot modes,
# compression settings, and file permissions in the final image).
# We replace the releng default entirely with our customized version.
cp "$OVERLAY_DIR/profiledef.sh" "$PROFILE/profiledef.sh"

echo "Adding extra packages..."
# packages.extra lists additional packages beyond releng's defaults.
# We merge them into packages.x86_64 (the file mkarchiso reads for pacman).
# Strategy: read line-by-line, skip blank lines and comments (#),
# and append each package only if it is not already in the list (idempotent).
while IFS= read -r pkg || [[ -n "$pkg" ]]; do
    [[ -z "$pkg" || "$pkg" == \#* ]] && continue
    grep -qxF "$pkg" "$PROFILE/packages.x86_64" || echo "$pkg" >> "$PROFILE/packages.x86_64"
done < "$OVERLAY_DIR/packages.extra"

# ── Embed installer scripts ────────────────────────────────────────────────────
# These three scripts live in the main setup/ directory of the dotfiles repo
# and implement the actual Arch Linux installation logic. They are placed in
# /root/installer/ on the live ISO so the auto-launch scripts can find them.
echo "Embedding installer scripts..."
mkdir -p "$PROFILE/airootfs/root/installer"
# Guided interactive installer — walks the user through partitioning, locale, etc.
cp "$DOTFILES_DIR/setup/archbaseos-guided-install.sh" "$PROFILE/airootfs/root/installer/"
# Automated unattended installer — reads /answerfile.json and installs silently.
cp "$DOTFILES_DIR/setup/arch-autoinstall.sh"          "$PROFILE/airootfs/root/installer/"
# Reset script — wipes and reinstalls the system while preserving /home.
cp "$DOTFILES_DIR/setup/reset-arch.sh"                "$PROFILE/airootfs/root/installer/"

echo "Embedding resources (branding assets used by post-install modules)..."
# resources/ contains shared assets (SVGs, etc.) referenced by installer modules
# such as the Plymouth splash logo.  Embedding them here means the ISO carries
# everything needed so post-install steps never require the user to supply files.
mkdir -p "$PROFILE/airootfs/root/installer/resources"
cp -r "$DOTFILES_DIR/resources/." "$PROFILE/airootfs/root/installer/resources/"

# Make all scripts executable. The archiso tool preserves these bits in the
# squashfs, so they will be executable on the live system too.
chmod 755 \
    "$PROFILE/airootfs/root/launch.sh" \
    "$PROFILE/airootfs/root/.automated_script.sh" \
    "$PROFILE/airootfs/usr/local/bin/install-arch" \
    "$PROFILE/airootfs/root/installer/"*.sh

# ── Embed answerfile (--preconf) ───────────────────────────────────────────────
# An answerfile baked into the ISO lets machines boot and install completely
# hands-free — useful for bulk deployment via PXE. Without it, the guided
# installer starts automatically instead.
if [[ -n "$PRECONF_FILE" ]]; then
    echo "Embedding answerfile: $PRECONF_FILE → /answerfile.json"
    # install -m 644 creates the destination with the given permissions,
    # equivalent to cp + chmod but in one atomic step.
    install -m 644 "$PRECONF_FILE" "$PROFILE/airootfs/answerfile.json"
fi

echo "Building ISO (this may take a while)..."
# mkarchiso needs root to mount loopback devices, write to /proc-style paths,
# and set file ownership inside the squashfs correctly.
# -v: verbose (shows progress)
# -w: work directory (large temporary build area)
# -o: output directory for the final .iso and netboot tarball
sudo mkarchiso -v -w "$WORK_DIR/mkarchiso" -o "$OUT_DIR" "$PROFILE"

# After the build, mkarchiso leaves files owned by root. Fix ownership so the
# calling user can manipulate the output without sudo.
sudo chmod -R 777 "$WORK_DIR" "$OUT_DIR"
sudo chown -R "$(id -u):$(id -g)" "$WORK_DIR" "$OUT_DIR"

echo
echo "Done."
# List the produced ISO file(s). The || true prevents the script from failing
# when no .iso files exist (e.g., if only netboot mode was built).
ls -lh "$OUT_DIR/"*.iso 2>/dev/null || true

# Inform the user whether an automated or guided install will start on boot.
if [[ -n "$PRECONF_FILE" ]]; then
    echo "Answerfile embedded — automated install will activate on boot."
else
    echo "No answerfile — guided (manual) installer will start automatically on boot."
fi

# ── How to write the ISO to USB ────────────────────────────────────────────────
# This is an isohybrid image: it MUST be written block-for-block. Copying the
# file onto a mounted USB, or using Rufus "ISO" mode / UNetbootin, rewrites the
# layout and the BIOS bootloader then fails with "failed to load ldlinux.c32".
# Glob (not ls) so filenames are handled safely; date-named ISOs sort ascending,
# so the last match is the newest build.
_isos=("$OUT_DIR"/*.iso)
_built_iso="${_isos[-1]}"
if [[ -e "$_built_iso" ]]; then
    echo
    echo "To write it to a USB stick (block-level — required for BIOS boot):"
    echo "    bash $SCRIPT_DIR/write-usb.sh \"$_built_iso\" /dev/sdX"
    echo "  or directly:  sudo dd if=\"$_built_iso\" of=/dev/sdX bs=4M conv=fsync oflag=direct status=progress"
    echo "  (oflag=direct makes the progress counter honest; without it dd races to"
    echo "   ~100% then pauses at the end while the slow USB flushes — not a hang.)"
    echo "  Do NOT drag-and-drop the .iso onto the USB, and avoid Rufus 'ISO' mode"
    echo "  (use 'DD' mode) — those cause 'failed to load ldlinux.c32' at boot."
fi

# ── Netboot artifacts ──────────────────────────────────────────────────────────
# mkarchiso's 'netboot' build mode (set in profiledef.sh) produces a tarball
# containing the kernel, initramfs, and airootfs.sfs ready for HTTP-based PXE
# boot. We detect it here and optionally generate an iPXE chainload script.
NETBOOT_TARBALL="$(ls "$OUT_DIR/"*-netboot-*.tar.gz 2>/dev/null | head -n1 || true)"
if [[ -n "$NETBOOT_TARBALL" ]]; then
    echo
    echo "Netboot artifact: $NETBOOT_TARBALL"
    echo "  Extract and serve its contents from an HTTP server, then boot via PXE."
    echo "  Internal layout (relative to tarball root):"
    # Show the tarball contents indented for readability.
    tar -tzf "$NETBOOT_TARBALL" | sed 's/^/    /'

    if [[ -n "$NETBOOT_URL" ]]; then
        # Strip trailing slash so our URL concatenations are consistent.
        BASE_URL="${NETBOOT_URL%/}"
        IPXE_FILE="$OUT_DIR/m-archy-netboot.ipxe"

        # Generate an iPXE script that can be chainloaded from netboot.xyz or
        # served directly from a WDS/TFTP server. The script tells iPXE where
        # to find the kernel, initramfs, and squashfs root over HTTP.
        cat > "$IPXE_FILE" <<IPXE
#!ipxe
# M-Archy Arch Linux Installer — PXE Boot
# Generated by build.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)
#
# Deploy:
#   1. Extract the netboot tarball to the web root at: ${BASE_URL}/
#   2. Add this script as a custom entry in netboot.xyz, or chainload it directly.
#
# netboot.xyz custom menu entry (paste into your netboot.xyz config):
#   item m-archy M-Archy Arch Linux Installer
#   goto m-archy
#   :m-archy
#   chain ${BASE_URL}/m-archy-netboot.ipxe

set base-url ${BASE_URL}
kernel \${base-url}/arch/boot/x86_64/vmlinuz-linux \\
    archiso_http_srv=\${base-url}/ \\
    archisobasedir=arch \\
    ip=dhcp
initrd \${base-url}/arch/boot/x86_64/initramfs-linux.img
boot
IPXE
        echo
        echo "iPXE script written to: $IPXE_FILE"
        echo "  Serve it alongside the netboot tarball contents at: ${BASE_URL}/"
    else
        echo
        echo "Tip: rerun with --netboot-url <http://your-server/path> to generate an iPXE script."
    fi
fi
