use std::fmt::Write;
use std::io::Read;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};

use anyhow::{anyhow, Context, Result};
use bootc_internal_utils::CommandRunExt;
use cap_std::ambient_authority;
use cap_std::fs::{Dir, DirBuilder, DirBuilderExt, MetadataExt, Permissions, PermissionsExt};
use cap_std_ext::dirext::CapStdExtDirExt;
use fn_error_context::context;

use crate::freezethaw::fsfreeze_thaw_cycle;

/// The subdirectory of /boot we use
pub(crate) const GRUB2DIR: &str = "grub2";
const CONFIGDIR: &str = "/usr/lib/bootupd/grub2-static";
const DROPINDIR: &str = "configs.d";
// The related grub files
const GRUBENV: &str = "grubenv";
pub(crate) const GRUBCONFIG: &str = "grub.cfg";
pub(crate) const GRUBCONFIG_BACKUP: &str = "grub.cfg.backup";
// The grub files that created by bootupd
const GRUB_FILES: [&str; 3] = ["bootuuid.cfg", GRUBCONFIG, GRUBENV];

// File mode for /boot/grub2/grub.config
// https://github.com/coreos/bootupd/issues/952
pub(crate) const GRUBCONFIG_FILE_MODE: u32 = 0o600;

/// Install the static GRUB config files.
#[context("Installing static GRUB configs")]
pub(crate) fn install(
    target_root: &Dir,
    src_root: Option<&Dir>,
    installed_efi_vendor: Option<&str>,
    write_uuid: bool,
) -> Result<()> {
    let bootdir = &target_root.open_dir("boot").context("Opening /boot")?;
    let boot_is_mount = {
        let root_dev = target_root.metadata(".")?.dev();
        let boot_dev = bootdir.metadata(".")?.dev();
        log::debug!("root_dev={root_dev} boot_dev={boot_dev}");
        root_dev != boot_dev
    };

    if !bootdir.exists(GRUB2DIR) {
        let mut dir_options = DirBuilder::new();
        dir_options.recursive(true).mode(0o700);
        bootdir.create_dir_with(GRUB2DIR, &dir_options)?;
    }

    let configdir = if let Some(src_root) = src_root {
        // strip the leading `/` to make the path relative to the given
        // source root
        src_root.open_dir(Path::new(CONFIGDIR).strip_prefix("/")?)?
    } else {
        Dir::open_ambient_dir(Path::new(CONFIGDIR), ambient_authority())?
    };

    let mut config = String::from("# Generated by bootupd / do not edit\n\n");

    let mut pre = String::new();
    configdir
        .open("grub-static-pre.cfg")?
        .read_to_string(&mut pre)?;
    config.push_str(pre.as_str());

    let dropindir = configdir.open_dir(Path::new(DROPINDIR))?;
    // Sort the files for reproducibility
    let mut entries = dropindir
        .entries()?
        .map(|e| e.map_err(anyhow::Error::msg))
        .collect::<Result<Vec<_>>>()?;
    entries.sort_by_key(|a| a.file_name());
    for ent in entries {
        let name = ent.file_name();
        let name = name
            .to_str()
            .ok_or_else(|| anyhow!("Invalid UTF-8: {name:?}"))?;
        if !name.ends_with(".cfg") {
            log::debug!("Ignoring {name}");
            continue;
        }
        writeln!(config, "\n### BEGIN {name} ###")?;
        let mut dropin = String::new();
        dropindir.open(name)?.read_to_string(&mut dropin)?;

        config.push_str(dropin.as_str());
        writeln!(config, "### END {name} ###")?;
        println!("Added {name}");
    }

    let grub2dir = bootdir.open_dir(GRUB2DIR)?;
    grub2dir
        .atomic_write_with_perms(
            "grub.cfg",
            config.as_bytes(),
            Permissions::from_mode(GRUBCONFIG_FILE_MODE),
        )
        .context("Copying grub-static.cfg")?;
    println!("Installed: grub.cfg");

    write_grubenv(&grub2dir).context("Create grubenv")?;
    ensure_file_permissions(&grub2dir, GRUBENV)?;

    let uuid_path = if write_uuid {
        let target_fs = if boot_is_mount { bootdir } else { target_root };
        let bootfs_meta = crate::filesystem::inspect_filesystem(target_fs, ".")?;
        let bootfs_uuid = bootfs_meta
            .uuid
            .ok_or_else(|| anyhow::anyhow!("Failed to find UUID for boot"))?;
        let grub2_uuid_contents = format!("set BOOT_UUID=\"{bootfs_uuid}\"\n");
        let uuid_path = "bootuuid.cfg";

        grub2dir
            .atomic_write_with_perms(
                uuid_path,
                grub2_uuid_contents,
                Permissions::from_mode(GRUBCONFIG_FILE_MODE),
            )
            .context("Writing bootuuid.cfg")?;
        println!("Installed: bootuuid.cfg");

        Some(uuid_path)
    } else {
        None
    };

    fsfreeze_thaw_cycle(grub2dir.reopen_as_ownedfd()?)?;

    if let Some(vendordir) = installed_efi_vendor {
        log::debug!("vendordir={:?}", &vendordir);
        let vendor = PathBuf::from(vendordir);
        let target = &vendor.join("grub.cfg");
        let dest_efidir = target_root
            .open_dir_optional("boot/efi/EFI")
            .context("Opening /boot/efi/EFI")?;
        if let Some(efidir) = dest_efidir {
            configdir
                .copy("grub-static-efi.cfg", &efidir, target)
                .context("Copying static EFI")?;
            println!("Installed: {target:?}");
            if let Some(uuid_path) = uuid_path {
                let target = &vendor.join(uuid_path);
                grub2dir
                    .copy(uuid_path, &efidir, target)
                    .context("Writing bootuuid.cfg to efi dir")?;
                println!("Installed: {target:?}");
            }
            fsfreeze_thaw_cycle(efidir.reopen_as_ownedfd()?)?;
        } else {
            println!("Could not find /boot/efi/EFI when installing {target:?}");
        }
    }

    Ok(())
}

#[context("Create file boot/grub2/grubenv")]
fn write_grubenv(grubdir: &Dir) -> Result<()> {
    if grubdir.exists(GRUBENV) {
        return Ok(());
    }
    let editenv = Path::new("/usr/bin/grub2-editenv");
    if !editenv.exists() {
        anyhow::bail!("Failed to find {:?}", editenv);
    }

    std::process::Command::new(editenv)
        .args([GRUBENV, "create"])
        .current_dir(format!("/proc/self/fd/{}", grubdir.as_raw_fd()))
        .run_inherited_with_cmd_context()
}

#[context("Ensure file permissions are 0600")]
fn ensure_file_permissions(target_dir: &Dir, target_file: &str) -> Result<()> {
    let metadata = match target_dir.metadata(target_file) {
        Ok(metadata) => metadata,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            // Silently return if file doesn't exist
            return Ok(());
        }
        Err(e) => return Err(e).context(format!("Reading {} metadata", target_file)),
    };

    let mode = metadata.permissions().mode() & 0o777;
    if mode != GRUBCONFIG_FILE_MODE {
        target_dir
            .set_permissions(target_file, Permissions::from_mode(GRUBCONFIG_FILE_MODE))
            .with_context(|| format!("Setting {} permissions to 0600", target_file))?;
    }
    Ok(())
}

#[context("Ensure grub files permissions are 0600")]
pub(crate) fn ensure_grub_permissions(grub_dir: &Dir) -> Result<()> {
    for file_name in GRUB_FILES.iter() {
        ensure_file_permissions(&grub_dir, file_name)
            .with_context(|| format!("Failed to ensure permissions for {}", file_name))?;
    }

    Ok(())
}

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

    #[test]
    #[ignore]
    fn test_install() -> Result<()> {
        env_logger::init();
        let td = tempfile::tempdir()?;
        let tdp = td.path();
        let td = Dir::open_ambient_dir(tdp, ambient_authority())?;
        std::fs::create_dir_all(tdp.join("boot/grub2"))?;
        std::fs::create_dir_all(tdp.join("boot/efi/EFI/BOOT"))?;
        std::fs::create_dir_all(tdp.join("boot/efi/EFI/fedora"))?;
        install(&td, None, Some("fedora"), false).unwrap();

        assert!(td.exists("boot/grub2/grub.cfg"));
        assert!(td.exists("boot/efi/EFI/fedora/grub.cfg"));
        Ok(())
    }
    #[test]
    fn test_write_grubenv() -> Result<()> {
        // Skip this test if grub2-editenv is not installed
        let editenv = Path::new("/usr/bin/grub2-editenv");
        if !editenv.try_exists()? {
            return Ok(());
        }
        let td = tempfile::tempdir()?;
        let tdp = td.path();
        let grub = tdp.join("boot/grub2");
        std::fs::create_dir_all(&grub)?;
        let td = Dir::open_ambient_dir(&grub, ambient_authority())?;
        write_grubenv(&td)?;

        assert!(td.exists("grubenv"));
        ensure_file_permissions(&td, "grubenv")?;
        // Verify permissions are now 0600
        {
            let metadata = td.metadata("grubenv")?;
            let mode = metadata.permissions().mode() & 0o777;
            assert_eq!(mode, 0o600);
        }
        Ok(())
    }
    #[test]
    fn test_ensure_grub_permissions() -> Result<()> {
        let td = tempfile::tempdir()?;
        let tdp = td.path();
        let grub = tdp.join("boot/grub2");
        std::fs::create_dir_all(&grub)?;
        let grub_dir = Dir::open_ambient_dir(&grub, ambient_authority())?;

        // Create grubenv, grub.cfg with permissions (0o644), no bootuuid.cfg
        {
            grub_dir.atomic_write_with_perms(
                "grubenv",
                "grubenv content".as_bytes(),
                Permissions::from_mode(0o644),
            )?;
            grub_dir.atomic_write_with_perms(
                "grub.cfg",
                "grub.cfg content".as_bytes(),
                Permissions::from_mode(0o644),
            )?;

            // bootuuid.cfg doesn't exist
            assert!(grub_dir.metadata("bootuuid.cfg").is_err());
        }

        // Call the function to fix permissions
        ensure_grub_permissions(&grub_dir)?;

        // Verify grubenv was fixed to 0o600
        let metadata = grub_dir.metadata("grubenv")?;
        assert_eq!(metadata.permissions().mode() & 0o777, 0o600);

        // Verify grub.cfg was fixed 0o600
        let metadata = grub_dir.metadata("grub.cfg")?;
        assert_eq!(metadata.permissions().mode() & 0o777, 0o600);
        Ok(())
    }
}
