//! Windows ISO post-processing. Patches `boot.wim` (image index 2, WinPE)
//! with two plain-text files so the client hits our SMB share and runs
//! Windows Setup from there.
//!
//! Credit: the *technique* (not the code) is adapted from Bootimus
//! (Apache-2.0, https://github.com/garybowers/bootimus). We reimplement in
//! Rust and shell out to `wimlib-imagex` at container runtime because
//! there is no maintained pure-Rust wimlib binding.
//!
//! What we inject — and why these are safe:
//!
//!   * `Windows/System32/winpeshl.ini`: a plain INI that WinPE reads at
//!     startup and uses to launch `startnet.cmd` instead of the default
//!     interactive shell. No driver, no executable, no signed code.
//!
//!   * `Windows/System32/startnet.cmd`: a batch file that runs `wpeinit`,
//!     waits for a DHCP lease, then `net use Z: \\<server>\<share> /user:guest`
//!     and invokes `Z:\setup.exe`. Everything the client executes is stock
//!     Microsoft-signed WinPE + `setup.exe`. We add zero native code to
//!     the client's boot path. The trust store is untouched.
//!
//! What we *do not* inject:
//!   * No `.sys` drivers, signed or otherwise.
//!   * No `.cer`, no registry hive edits, no `bcdedit` changes.
//!   * No `bypass*` Windows 11 tweaks (operators who want those can use an
//!     unattend.xml; they will never be injected silently by us).

use std::path::{Path, PathBuf};
use std::process::Command;

/// Public identifier of whether/how Windows patching ran for an ISO.
/// Stored on `IsoMeta` so the UI can show a clear "SMB ready" indicator.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum WinPatchState {
    /// Not a Windows ISO, nothing to do.
    NotApplicable,
    /// Windows ISO detected but Windows support is disabled in settings.
    DisabledBySettings,
    /// wimlib-imagex isn't on PATH — operator needs to install the runtime
    /// dependency before Windows ISOs can be patched.
    WimlibMissing,
    /// Patching succeeded; the ISO's boot.wim was rewritten in-place.
    Patched { smb_host: String, smb_share: String },
    /// wimlib returned an error.
    Failed { reason: String },
}

pub struct WimPatcher {
    pub smb_host: String,
    pub smb_share: String,
}

impl WimPatcher {
    #[must_use]
    pub fn new(smb_host: String, smb_share: String) -> Self {
        Self {
            smb_host,
            smb_share,
        }
    }

    /// Apply WinPE patches to `boot.wim` inside `extracted_iso_dir`. Returns
    /// a state enum — never panics. Designed to be safely re-runnable; each
    /// call rebuilds image 2 from scratch via `wimlib-imagex update`.
    pub fn patch(&self, extracted_iso_dir: &Path) -> WinPatchState {
        if !wimlib_present() {
            return WinPatchState::WimlibMissing;
        }
        let boot_wim = extracted_iso_dir.join("sources").join("boot.wim");
        if !boot_wim.exists() {
            // Not a standard Windows install ISO layout.
            return WinPatchState::NotApplicable;
        }

        let work = match tempfile::tempdir() {
            Ok(d) => d,
            Err(e) => {
                return WinPatchState::Failed {
                    reason: format!("tempdir: {e}"),
                }
            }
        };

        // Stage the two files we want present at /Windows/System32/.
        let staging = work.path().join("stage/Windows/System32");
        if let Err(e) = std::fs::create_dir_all(&staging) {
            return WinPatchState::Failed {
                reason: format!("staging mkdir: {e}"),
            };
        }
        if let Err(e) = std::fs::write(staging.join("winpeshl.ini"), WINPESHL_INI) {
            return WinPatchState::Failed {
                reason: format!("write winpeshl.ini: {e}"),
            };
        }
        let startnet = render_startnet(&self.smb_host, &self.smb_share);
        if let Err(e) = std::fs::write(staging.join("startnet.cmd"), startnet) {
            return WinPatchState::Failed {
                reason: format!("write startnet.cmd: {e}"),
            };
        }

        // Build a wimlib update command file:
        //   add <stage>/Windows/System32 /Windows/System32
        let update_file = work.path().join("update.cmd");
        let update_cmd = format!("add \"{}\" \"/Windows/System32\"\n", staging.display());
        if let Err(e) = std::fs::write(&update_file, update_cmd) {
            return WinPatchState::Failed {
                reason: format!("write update.cmd: {e}"),
            };
        }

        // Run wimlib-imagex update against image index 2 (WinPE).
        let output = Command::new("wimlib-imagex")
            .arg("update")
            .arg(&boot_wim)
            .arg("2")
            .arg("--rebuild")
            .arg("--command-file")
            .arg(&update_file)
            .output();
        match output {
            Ok(o) if o.status.success() => WinPatchState::Patched {
                smb_host: self.smb_host.clone(),
                smb_share: self.smb_share.clone(),
            },
            Ok(o) => WinPatchState::Failed {
                reason: format!(
                    "wimlib-imagex update failed (exit {:?}): {}",
                    o.status.code(),
                    String::from_utf8_lossy(&o.stderr)
                ),
            },
            Err(e) => WinPatchState::Failed {
                reason: format!("spawn wimlib-imagex: {e}"),
            },
        }
    }
}

fn wimlib_present() -> bool {
    which("wimlib-imagex").is_some()
}

fn which(cmd: &str) -> Option<PathBuf> {
    let paths = std::env::var_os("PATH")?;
    for dir in std::env::split_paths(&paths) {
        let p = dir.join(cmd);
        if p.is_file() {
            return Some(p);
        }
    }
    None
}

/// The winpeshl.ini contents. This file tells WinPE "don't run cmd.exe
/// interactively; run startnet.cmd and exit when it returns".
const WINPESHL_INI: &str = "[LaunchApps]\r\n\
\"%SYSTEMROOT%\\system32\\startnet.cmd\"\r\n";

/// Render startnet.cmd. The script:
///   1. Loads WinPE networking (`wpeinit`) and renews DHCP.
///   2. Waits until the SMB server is reachable.
///   3. Maps the install share to Z: as guest.
///   4. Runs setup.exe from the share.
///
/// Uses CRLF line endings because WinPE cmd.exe requires them for .cmd files
/// created on unix hosts.
fn render_startnet(host: &str, share: &str) -> String {
    use std::fmt::Write as _;
    let host = host.trim();
    let share = share.trim_matches('/');
    let mut s = String::new();
    // Windows-style CRLF; consumed verbatim by cmd.exe inside WinPE.
    // Bootimus v0.1.58 lesson: surface `net use` errors instead of
    // tight-looping on a blind retry. We retry but log every miss.
    s.push_str("@echo off\r\n");
    s.push_str("echo OpenPXE WinPE bootstrap\r\n");
    s.push_str("wpeinit\r\n");
    // v0.1.58: explicitly start Workstation before mapping the share —
    // `net use` otherwise lazily inits SMB-client and races wpeinit.
    s.push_str("net start Workstation >nul 2>&1\r\n");
    s.push_str("ipconfig /renew\r\n");
    writeln!(s, "echo Waiting for SMB server {host} to be reachable...\r").unwrap();
    writeln!(
        s,
        ":waitsmb\r\nping -n 1 -w 500 {host} >nul && goto havenet\r\n\
         timeout /t 2 /nobreak >nul\r\ngoto waitsmb\r"
    )
    .unwrap();
    s.push_str(":havenet\r\n");
    writeln!(
        s,
        "echo Mapping install media from \\\\{host}\\{share}...\r"
    )
    .unwrap();
    writeln!(
        s,
        ":mapshare\r\nnet use Z: \\\\{host}\\{share} /user:guest \"\" /persistent:no && goto mapped\r\n\
         timeout /t 3 /nobreak >nul\r\ngoto mapshare\r"
    )
    .unwrap();
    s.push_str(":mapped\r\n");
    s.push_str("echo Starting Windows Setup\r\n");
    s.push_str("Z:\\setup.exe\r\n");
    s.push_str("echo Setup exited; dropping to cmd for diagnosis\r\n");
    s.push_str("cmd\r\n");
    s
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn startnet_has_crlf_and_no_testsigning() {
        let s = render_startnet("10.0.0.5", "win11");
        assert!(s.contains("\r\n"));
        // Hard guard: must never include trust-store or driver-policy mutations.
        assert!(!s.to_lowercase().contains("bcdedit"));
        assert!(!s.to_lowercase().contains("testsigning"));
        assert!(!s.to_lowercase().contains("certutil"));
        assert!(s.contains("net use Z:"));
        assert!(s.contains("setup.exe"));
    }

    #[test]
    fn startnet_primes_workstation_and_surfaces_mapping_errors() {
        let s = render_startnet("10.0.0.5", "win11");
        assert!(
            s.contains("net start Workstation"),
            "WinPE should explicitly start the SMB client before net use:\n{s}"
        );
        let net_use_line = s
            .lines()
            .find(|line| line.contains("net use Z:"))
            .expect("net use line");
        assert!(
            !net_use_line.contains(">nul"),
            "net use errors must remain visible in WinPE console: {net_use_line}"
        );
    }

    #[test]
    fn patcher_reports_wimlib_missing_gracefully() {
        // We don't assume wimlib is present in CI; this checks the missing
        // branch is the noisy-but-survivable one we expect.
        let patcher = WimPatcher::new("10.0.0.5".into(), "win11".into());
        let dir = tempfile::tempdir().unwrap();
        // Construct a fake "sources/boot.wim".
        std::fs::create_dir_all(dir.path().join("sources")).unwrap();
        std::fs::write(dir.path().join("sources/boot.wim"), b"placeholder").unwrap();
        let result = patcher.patch(dir.path());
        // Depending on whether wimlib is installed on the runner, we get
        // either WimlibMissing or Failed(...). Both mean "no silent
        // success with trust-store mutation" — that's the invariant.
        assert!(matches!(
            result,
            WinPatchState::WimlibMissing | WinPatchState::Failed { .. }
        ));
    }
}
