//! iPXE script generator.
//!
//! ## Menu hierarchy (per Phase 2 spec)
//!
//! ```text
//! Top level:
//!   Default
//!     > Boot from Local HDD
//!   Installers
//!     > Linux Installers   -> submenu of Linux ISOs
//!     > Windows Installers -> submenu of Windows ISOs  (gated by Settings::windows_enabled)
//!   Tools
//!     > Utilities          -> memtest, etc. (embedded assets only)
//!     > PXEForge Shell     -> drop to iPXE shell with branded prompt
//!     > Network Card Info  -> ifstat / config / route dump
//!   Gated Deployment       -> join the gate queue
//! ```
//!
//! ## iPXE is entirely backend — users do not see or write iPXE
//!
//! All user-facing knobs live in `Settings`. Script generation translates
//! those knobs into iPXE primitives (chain, menu, item, choose, etc.).
//! There is intentionally no UI path to upload a custom `.ipxe` script.

use pxeforge_core::{Settings, TimeoutAction};
use pxeforge_iso_store::{BootEntry, BootKind, IsoMeta};
use pxeforge_iso_store::introspect::DistroFamily;
use std::fmt::Write as _;

/// Top-level PXEForge boot menu. Serialized identically for BIOS and UEFI
/// clients because iPXE normalises the menu primitives across firmwares.
#[must_use]
pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> String {
    let mut s = String::new();
    let base = base_url.trim_end_matches('/');
    let timeout_ms = settings.boot_menu_timeout_secs.saturating_mul(1000);
    let default_item = match settings.timeout_action {
        TimeoutAction::LocalHdd => "local",
        TimeoutAction::GatedDeployment => "gate",
        // Stay -> iPXE's `--timeout 0` is "no timeout". Pick any default
        // label; the client waits for keypress.
        TimeoutAction::Stay => "local",
    };

    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "# PXEForge top-level menu - auto-generated, do not edit");
    let _ = writeln!(s, "set base-url {base}");
    let _ = writeln!(s, "set esc:hex 1b");
    let _ = writeln!(s, "set cls ${{esc:string}}[2J");
    let _ = writeln!(s, ":menu");
    let _ = writeln!(s, "menu PXEForge - network boot menu");
    let _ = writeln!(s, "item --gap -- ------------------------- Default -------------------------");
    let _ = writeln!(s, "item local            Boot from Local HDD");
    let _ = writeln!(s, "item --gap -- ----------------------- Installers -----------------------");
    if has_family(isos, is_linux_family) {
        let _ = writeln!(s, "item linux            Linux Installers  >");
    } else {
        let _ = writeln!(s, "item --gap -- (no Linux ISOs uploaded)");
    }
    if settings.windows_enabled && has_family(isos, is_windows_family) {
        let _ = writeln!(s, "item windows          Windows Installers  >");
    } else if settings.windows_enabled {
        let _ = writeln!(s, "item --gap -- (no Windows ISOs uploaded)");
    } else {
        let _ = writeln!(s, "item --gap -- (Windows support disabled in Settings)");
    }
    let _ = writeln!(s, "item --gap -- -------------------------- Tools --------------------------");
    let _ = writeln!(s, "item tools            Tools  >");
    let _ = writeln!(s, "item --gap -- ---------------------- Gated Deployment ---------------------");
    let _ = writeln!(s, "item gate             Gated Deployment  (join queue)");
    let _ = writeln!(s, "item --gap");
    let _ = writeln!(s, "item --key x exit     Exit iPXE");

    if matches!(settings.timeout_action, TimeoutAction::Stay) {
        let _ = writeln!(s, "choose --default {default_item} target || goto menu");
    } else {
        let _ = writeln!(s, "choose --default {default_item} --timeout {timeout_ms} target || goto menu");
    }

    // iPXE's `||` is strict about what follows. Each test uses `goto menu`
    // as the fallthrough target so the parser never sees a bare `||` with
    // trailing whitespace — some iPXE builds reject that.
    let _ = writeln!(s, "iseq ${{target}} local   && chain {base}/boot/_local.ipxe        || goto menu");
    let _ = writeln!(s, "iseq ${{target}} linux   && chain {base}/boot/_linux_menu.ipxe   || goto menu");
    let _ = writeln!(s, "iseq ${{target}} windows && chain {base}/boot/_windows_menu.ipxe || goto menu");
    let _ = writeln!(s, "iseq ${{target}} tools   && chain {base}/boot/_tools_menu.ipxe   || goto menu");
    let _ = writeln!(s, "iseq ${{target}} gate    && chain {base}/boot/_gate.ipxe         || goto menu");
    let _ = writeln!(s, "iseq ${{target}} exit    && exit                                 || goto menu");
    let _ = writeln!(s, "goto menu");
    s
}

/// Per-family submenu (Linux or Windows). Each item shows the ISO size
/// in MiB, iVentoy-style (`[ 4376 MB] ubuntu-22.04-desktop-amd64`).
#[must_use]
pub fn render_family_menu(isos: &[IsoMeta], base_url: &str, is_windows: bool) -> String {
    let base = base_url.trim_end_matches('/');
    let title = if is_windows { "Windows Installers" } else { "Linux Installers" };
    let label = if is_windows { "windows" } else { "linux" };
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "set base-url {base}");
    let _ = writeln!(s, ":menu");
    let _ = writeln!(s, "menu PXEForge - {title}");
    let filter: fn(DistroFamily) -> bool =
        if is_windows { is_windows_family } else { is_linux_family };
    let mut count = 0;
    for iso in isos {
        if !filter(iso.introspection.family) { continue; }
        for entry in &iso.boot_entries {
            let size_label = fmt_size_mib(iso.size_bytes);
            let key = hotkey_for_index(count);
            let _ = writeln!(
                s, "item {}{} [{:>6}] {}",
                key,
                entry.id,
                size_label,
                escape_label(&entry.title),
            );
            count += 1;
        }
    }
    if count == 0 {
        let _ = writeln!(s, "item --gap -- (no {label} images uploaded yet)");
    }
    let _ = writeln!(s, "item --gap");
    let _ = writeln!(s, "item --key b back   < Back to main menu");
    let _ = writeln!(s, "choose target || goto menu");
    let _ = writeln!(s, "iseq ${{target}} back && chain {base}/boot.ipxe || goto menu");
    let _ = writeln!(s, "chain {base}/boot/${{target}}.ipxe || goto menu");
    s
}

/// Format a byte count as `NNNN MB` (iVentoy-style — MB not MiB, to match
/// operator expectations from the original tool).
fn fmt_size_mib(bytes: u64) -> String {
    let mib = bytes / (1024 * 1024);
    format!("{} MB", mib)
}

/// Assign `--key N <id>` hotkeys 1..9, then nothing for positions >=9.
/// iPXE's menu needs the --key prefix as a separate token before the id.
fn hotkey_for_index(i: usize) -> String {
    if i < 9 {
        format!("--key {} ", i + 1)
    } else {
        String::new()
    }
}

/// Tools submenu — Utilities, Shell, NIC Info, Reboot, Exit to firmware.
#[must_use]
pub fn render_tools_menu(base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "set base-url {base}");
    let _ = writeln!(s, ":menu");
    let _ = writeln!(s, "menu PXEForge - Tools");
    let _ = writeln!(s, "item --key u util     Utilities  (memtest, ...)");
    let _ = writeln!(s, "item --key s shell    PXEForge Shell");
    let _ = writeln!(s, "item --key n nic      Network Card Info");
    let _ = writeln!(s, "item --gap");
    let _ = writeln!(s, "item --key r reboot   Reboot Computer");
    let _ = writeln!(s, "item --key e firmware Exit and continue BIOS boot");
    let _ = writeln!(s, "item --gap");
    let _ = writeln!(s, "item --key b back     < Back to main menu");
    let _ = writeln!(s, "choose target || goto menu");
    let _ = writeln!(s, "iseq ${{target}} util     && chain {base}/boot/_util.ipxe  || goto menu");
    let _ = writeln!(s, "iseq ${{target}} shell    && chain {base}/boot/_shell.ipxe || goto menu");
    let _ = writeln!(s, "iseq ${{target}} nic      && chain {base}/boot/_nic.ipxe   || goto menu");
    let _ = writeln!(s, "iseq ${{target}} reboot   && reboot                        || goto menu");
    let _ = writeln!(s, "iseq ${{target}} firmware && exit 0                        || goto menu");
    let _ = writeln!(s, "iseq ${{target}} back     && chain {base}/boot.ipxe        || goto menu");
    let _ = writeln!(s, "goto menu");
    s
}

/// "Boot from Local HDD". On BIOS, we sanboot the first local drive; on
/// UEFI we `exit` so the firmware moves to the next boot entry (normally
/// the internal disk).
#[must_use]
pub fn render_local_hdd(base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "# Boot from Local HDD - platform-sensitive");
    let _ = writeln!(s, "iseq ${{platform}} pcbios && sanboot --no-describe --drive 0x80 || ");
    let _ = writeln!(s, "# UEFI path: fall through to the firmware's next boot entry");
    let _ = writeln!(s, "exit 0");
    let _ = writeln!(s, "# If the above exit returns, loop back to the main menu");
    let _ = writeln!(s, "chain {base}/boot.ipxe");
    s
}

/// Utilities submenu. For Phase 2 we bundle memtest86+ as an optional
/// asset (if absent, the item is listed but errors gracefully). No third-
/// party tools are fetched at runtime.
#[must_use]
pub fn render_util(base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, ":menu");
    let _ = writeln!(s, "menu PXEForge - Utilities");
    let _ = writeln!(s, "item memtest      MemTest86+ (RAM diagnostic)");
    let _ = writeln!(s, "item --gap");
    let _ = writeln!(s, "item back         < Back");
    let _ = writeln!(s, "choose target || goto menu");
    let _ = writeln!(s, "iseq ${{target}} memtest && chain {base}/ipxe/memtest.bin || ");
    let _ = writeln!(s, "iseq ${{target}} back && chain {base}/boot/_tools_menu.ipxe || ");
    let _ = writeln!(s, "goto menu");
    s
}

/// "PXEForge Shell" — iPXE shell, branded.
#[must_use]
pub fn render_shell(base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "echo ==========================================");
    let _ = writeln!(s, "echo   PXEForge Shell");
    let _ = writeln!(s, "echo   'exit' returns to the main menu");
    let _ = writeln!(s, "echo ==========================================");
    let _ = writeln!(s, "shell");
    let _ = writeln!(s, "chain {base}/boot.ipxe");
    s
}

/// "Network Card Info" — print ifstat + route + config.
#[must_use]
pub fn render_nic_info(base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "echo ==========================================");
    let _ = writeln!(s, "echo   Network Card Info");
    let _ = writeln!(s, "echo ==========================================");
    let _ = writeln!(s, "ifstat");
    let _ = writeln!(s, "echo");
    let _ = writeln!(s, "route");
    let _ = writeln!(s, "echo");
    let _ = writeln!(s, "echo 'Press any key to return to menu'");
    let _ = writeln!(s, "prompt --timeout 30000");
    let _ = writeln!(s, "chain {base}/boot.ipxe");
    s
}

/// Gated Deployment entry point. Joins the queue, then enters a long-poll
/// loop (iPXE repeats the chain on 3xx redirects / HTTP errors until a
/// real script comes back).
#[must_use]
pub fn render_gate_entry(base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "# Gated Deployment - join the queue and wait for operator");
    let _ = writeln!(s, "echo Joining gate queue...");
    // imgfetch writes the body to a file in iPXE's transient FS; we read
    // the gate id out of the Location-style header by asking the server
    // to put it in the response body as a single token.
    let _ = writeln!(s, "chain --replace {base}/api/gate/join?mac=${{mac}}");
    s
}

/// Per-entry boot script (same as Phase 1, with extra_kernel_args appended).
#[must_use]
pub fn render_entry(entry: &BootEntry, settings: &Settings, base_url: &str) -> String {
    let mut s = String::new();
    let base = base_url.trim_end_matches('/');
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "set base-url {base}");
    match &entry.kind {
        BootKind::LinuxKernel { kernel_url, initrd_urls, args } => {
            let mut cmdline = args.cmdline.replace("${base-url}", base);
            if !settings.extra_kernel_args.trim().is_empty() {
                cmdline.push(' ');
                cmdline.push_str(settings.extra_kernel_args.trim());
            }
            let _ = writeln!(s, "kernel {base}/{kernel_url} {cmdline}");
            for u in initrd_urls {
                let _ = writeln!(s, "initrd {base}/{u}");
            }
            let _ = writeln!(s, "boot || goto failed");
        }
        BootKind::Wimboot { wimboot_url, files } => {
            let _ = writeln!(s, "kernel {base}/{wimboot_url}");
            for (tag, url) in files {
                let _ = writeln!(s, "initrd --name {tag} {base}/{url} {tag}");
            }
            let _ = writeln!(s, "boot || goto failed");
        }
        BootKind::SanBootIso { iso_url } => {
            let _ = writeln!(s, "sanboot --no-describe {base}/{iso_url} || goto failed");
        }
    }
    let _ = writeln!(s, ":failed");
    let _ = writeln!(s, "echo Boot failed - returning to menu in 5s");
    let _ = writeln!(s, "sleep 5");
    let _ = writeln!(s, "chain {base}/boot.ipxe");
    s
}

fn is_linux_family(f: DistroFamily) -> bool {
    matches!(
        f,
        DistroFamily::DebianUbuntu
            | DistroFamily::RhelFedora
            | DistroFamily::OpenSuse
            | DistroFamily::Arch
            | DistroFamily::Alpine
            | DistroFamily::Unknown
    )
}

fn is_windows_family(f: DistroFamily) -> bool {
    matches!(f, DistroFamily::WindowsPe)
}

fn has_family(isos: &[IsoMeta], pred: fn(DistroFamily) -> bool) -> bool {
    isos.iter().any(|i| pred(i.introspection.family))
}

fn escape_label(s: &str) -> String {
    s.chars().map(|c| match c { '\n' | '\r' => ' ', c => c }).collect()
}
