mod activate;
mod config;
mod env_generator;
mod etc_overlay;
mod find_etc;
mod fs;
mod init;
mod initrd_init;
mod path;
mod proc_mounts;
mod switch_root;

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

use anyhow::{Context, Result, bail};

pub use crate::{
    activate::activate,
    env_generator::env_generator,
    etc_overlay::clear_etc_opaque,
    find_etc::find_etc,
    init::init,
    initrd_init::initrd_init,
    path::{resolve_in_prefix, resolve_in_root},
    switch_root::switch_root,
};

pub const SYSROOT_PATH: &str = "/sysroot";

/// Verify that an init path is inside a `NixOS` toplevel directory.
///
/// If the path is verified, returns the path to the toplevel.
///
/// Check for the file `nixos-version` inside the toplevel to verify.
///
/// # Errors
///
/// If the init is not inside a `NixOS` toplevel return an error.
pub fn verify_init_is_nixos(prefix: &str, path: impl AsRef<Path>) -> Result<PathBuf> {
    let toplevel = path
        .as_ref()
        .parent()
        .map(Path::to_path_buf)
        .context("Provided init= is not in a directory")?;

    let stripped_toplevel = toplevel
        .strip_prefix("/")
        .with_context(|| format!("Failed to strip / from {}", toplevel.display()))?;

    let nixos_version_in_prefix = Path::new(prefix)
        .join(stripped_toplevel)
        .join("nixos-version");

    if !nixos_version_in_prefix
        .try_exists()
        .context("Failed to check whether nixos-version exists in toplevel")?
    {
        bail!(
            "Failed to verify init {} is inside a NixOS toplevel",
            path.as_ref().display()
        )
    }
    Ok(toplevel)
}

/// Find the canonical path of the init in a prefix.
///
/// Uses the `init=` parameter on the kernel command-line.
///
/// Returns the relative path of the init to the prefix, e.g. without the `/sysroot` prefix.
pub fn find_init_in_prefix(prefix: &str) -> Result<PathBuf> {
    let cmdline = std::fs::read_to_string("/proc/cmdline")?;
    let init = extract_init(&cmdline)?;
    let canonicalized_init = resolve_in_prefix(prefix, &init)?;
    log::info!("Found init: {}.", canonicalized_init.display());
    Ok(canonicalized_init)
}

/// Extract the value of the `init` parameter from the given kernel `cmdline`.
///
/// If `init=` appears multiple times the last one wins, matching the kernel.
/// This is what makes appending `init=/bin/sh` at the boot prompt work even
/// though the boot entry already has an `init=`.
fn extract_init(cmdline: &str) -> Result<PathBuf> {
    let init = cmdline
        .split_ascii_whitespace()
        .filter_map(|p| p.strip_prefix("init="))
        .next_back()
        .with_context(|| format!("No init= parameter on kernel cmdline: {cmdline}"))?;

    Ok(PathBuf::from(init))
}

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

    use std::fs;

    use tempfile::tempdir;

    #[test]
    fn test_verify_init_is_nixos() -> Result<()> {
        let prefix = tempdir()?;
        let toplevel = prefix.path().join("toplevel");
        fs::create_dir(&toplevel)?;

        let init = &toplevel.join("init");
        fs::write(init, "init")?;

        let nixos_version = toplevel.join("nixos-version");
        fs::write(&nixos_version, "25.11")?;

        verify_init_is_nixos(prefix.path().to_str().unwrap(), "/toplevel/init")?;

        Ok(())
    }

    #[test]
    fn test_extract_init_single() {
        assert_eq!(
            extract_init("root=fstab init=/nix/store/xxx-nixos/init quiet").unwrap(),
            PathBuf::from("/nix/store/xxx-nixos/init")
        );
    }

    #[test]
    fn test_extract_init_last_wins() {
        assert_eq!(
            extract_init("init=/nix/store/xxx-nixos/init init=/bin/sh").unwrap(),
            PathBuf::from("/bin/sh")
        );
    }

    #[test]
    fn test_extract_init_missing() {
        assert!(extract_init("root=fstab quiet").is_err());
    }
}
