use anyhow::{bail, Context, Result};
use camino::Utf8PathBuf;
use cap_std::ambient_authority;
use cap_std::fs::Dir;
use cap_std_ext::dirext::CapStdExtDirExt;
use std::io::prelude::*;
use std::path::Path;
use std::process::Command;

use bootc_internal_blockdev::Device;

use crate::bootupd::RootContext;
use crate::component::*;
use crate::freezethaw::fsfreeze_thaw_cycle;
use crate::grubconfigs;
use crate::model::*;
use crate::packagesystem;

// grub2-install file path
pub(crate) const GRUB_BIN: &str = "usr/sbin/grub2-install";

#[cfg(target_arch = "powerpc64")]
fn target_device(device: &Device) -> Result<String> {
    const PREPBOOT_GUID: &str = "9E1A2D38-C612-4316-AA26-8B49521E5A8B";
    /// We make a best-effort to support MBR partitioning too.
    const PREPBOOT_MBR_TYPE: &str = "41";

    if !device.has_children() {
        return Ok(device.path());
    }

    let prepdev = device
        .find_partition_of_type(PREPBOOT_GUID)
        .or_else(|| device.find_partition_of_type(PREPBOOT_MBR_TYPE))
        .ok_or_else(|| {
            anyhow::anyhow!("Failed to find PReP partition with GUID {PREPBOOT_GUID}")
        })?;
    Ok(prepdev.path())
}

#[derive(Default)]
pub(crate) struct Bios {}

impl Bios {
    // Return `true` if grub2-modules installed
    fn check_grub_modules(&self) -> Result<bool> {
        let usr_path = Path::new("/usr/lib/grub");
        #[cfg(target_arch = "x86_64")]
        {
            usr_path.join("i386-pc").try_exists().map_err(Into::into)
        }
        #[cfg(target_arch = "powerpc64")]
        {
            usr_path
                .join("powerpc-ieee1275")
                .try_exists()
                .map_err(Into::into)
        }
    }

    // Run grub2-install
    fn run_grub_install(&self, dest_root: &str, device: &Device) -> Result<()> {
        if !self.check_grub_modules()? {
            bail!("Failed to find grub2-modules");
        }
        let grub_install = Path::new("/").join(GRUB_BIN);
        if !grub_install.exists() {
            bail!("Failed to find {:?}", grub_install);
        }

        let mut cmd = Command::new(grub_install);
        let boot_dir = Path::new(dest_root).join("boot");
        // We forcibly inject mdraid1x because it's needed by CoreOS's default of "install raw disk image"
        // We also add part_gpt because in some cases probing of the partition map can fail such
        // as in a container, but we always use GPT.
        #[cfg(target_arch = "x86_64")]
        cmd.args(["--target", "i386-pc"])
            .args(["--boot-directory", boot_dir.to_str().unwrap()])
            .args(["--modules", "mdraid1x part_gpt"])
            .arg(&device.path());

        #[cfg(target_arch = "powerpc64")]
        {
            let device = target_device(device)?;
            cmd.args(&["--target", "powerpc-ieee1275"])
                .args(&["--boot-directory", boot_dir.to_str().unwrap()])
                .arg("--no-nvram")
                .arg(&device);
        }

        let cmdout = cmd.output()?;
        if !cmdout.status.success() {
            std::io::stderr().write_all(&cmdout.stderr)?;
            bail!("Failed to run {:?}", cmd);
        }
        Ok(())
    }
}

impl Component for Bios {
    fn name(&self) -> &'static str {
        "BIOS"
    }

    fn install(
        &self,
        src_root: &str,
        dest_root: &str,
        device: Option<&Device>,
        _update_firmware: bool,
    ) -> Result<InstalledContent> {
        let device =
            device.ok_or_else(|| anyhow::anyhow!("BIOS component requires a target device"))?;
        let src_dir = Dir::open_ambient_dir(src_root, ambient_authority())
            .with_context(|| format!("opening source directory {src_root}"))?;
        let Some(meta) = get_component_update(&src_dir, self)? else {
            anyhow::bail!("No update metadata for component {} found", self.name());
        };

        self.run_grub_install(dest_root, device)
            .with_context(|| format!("installing GRUB on {}", device.path()))?;

        Ok(InstalledContent {
            meta,
            filetree: None,
            adopted_from: None,
        })
    }

    fn generate_update_metadata(&self, sysroot_path: &str) -> Result<Option<ContentMetadata>> {
        let grub_install = Path::new(sysroot_path).join(GRUB_BIN);
        if !grub_install.exists() {
            println!("Failed to find {:?}", grub_install);
            return Ok(None);
        }

        // Query the rpm database and list the package and build times for /usr/sbin/grub2-install
        let meta = packagesystem::query_files(sysroot_path, [&grub_install])?;
        write_update_metadata(sysroot_path, self, &meta)?;
        Ok(Some(meta))
    }

    fn query_adopt(&self, devices: &Option<Vec<Device>>) -> Result<Option<Adoptable>> {
        #[cfg(target_arch = "x86_64")]
        if crate::efi::is_efi_booted()? && devices.is_none() {
            log::debug!("Skip BIOS adopt");
            return Ok(None);
        }
        crate::component::query_adopt_state()
    }

    // Backup the current grub.cfg and replace with new static config
    // - Backup "/boot/loader/grub.cfg" to "/boot/grub2/grub.cfg.bak"
    // - Remove symlink "/boot/grub2/grub.cfg"
    // - Replace "/boot/grub2/grub.cfg" symlink with new static "grub.cfg"
    fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &Dir) -> Result<()> {
        let grub = "boot/grub2";
        // sysroot_path is /, destdir is Dir of /
        let grub_config_path = Utf8PathBuf::from(sysroot_path).join(grub);
        let grub_config_dir = destdir.open_dir(grub).context("Opening boot/grub2")?;

        let grub_config = grub_config_path.join(grubconfigs::GRUBCONFIG);

        if !grub_config.exists() {
            anyhow::bail!("Could not find '{}'", grub_config);
        }

        let mut current_config;
        // If /boot/grub2/grub.cfg is not symlink, we need to keep going
        if !grub_config.is_symlink() {
            println!("'{}' is not a symlink", grub_config);
            current_config = grub_config.clone();
        } else {
            // If /boot/grub2/grub.cfg is symlink to /boot/loader/grub.cfg,
            // backup it to /boot/grub2/grub.cfg.bak
            // Get real file for symlink /boot/grub2/grub.cfg
            let real_config = grub_config_dir.read_link(grubconfigs::GRUBCONFIG)?;
            let real_config =
                Utf8PathBuf::from_path_buf(real_config).expect("Path should be valid UTF-8");
            // Resolve symlink location
            current_config = grub_config_path.clone();
            current_config.push(real_config);
        }

        let backup_config = grub_config_path.join(grubconfigs::GRUBCONFIG_BACKUP);
        if !backup_config.exists() {
            // Backup the current GRUB config which is hopefully working right now
            println!(
                "Creating a backup of the current GRUB config '{}' in '{}'...",
                current_config, backup_config
            );
            std::fs::copy(&current_config, &backup_config)
                .context("Failed to backup GRUB config")?;
        }

        crate::grubconfigs::install(&destdir, None, None, true)?;

        // Remove the real config if it is symlink and will not
        // if /boot/grub2/grub.cfg is file
        if current_config != grub_config {
            println!("Removing {}", current_config);
            grub_config_dir.remove_file_optional(current_config.as_std_path())?;
        }

        // Synchronize the filesystem containing /boot/grub2 to disk.
        fsfreeze_thaw_cycle(grub_config_dir.reopen_as_ownedfd()?)?;

        Ok(())
    }

    fn adopt_update(
        &self,
        rootcxt: &RootContext,
        update: &ContentMetadata,
        with_static_config: bool,
    ) -> Result<Option<InstalledContent>> {
        let bios_devices = rootcxt.device.find_colocated_bios_boot()?;
        let Some(meta) = self.query_adopt(&bios_devices)? else {
            return Ok(None);
        };

        // Install grub onto each parent (whole-disk) device. For BIOS boot on
        // multi-disk setups (e.g. RAID), grub must be written to every backing
        // disk's MBR/BIOS-boot partition so the system can boot from any one.
        for parent in rootcxt.device.find_all_roots()? {
            self.run_grub_install(rootcxt.path.as_str(), &parent)?;
            log::debug!("Installed grub modules on {}", parent.path());
        }

        if with_static_config {
            // Install the static config if the OSTree bootloader is not set.
            if let Some(bootloader) = crate::ostreeutil::get_ostree_bootloader()? {
                println!(
                    "ostree repo 'sysroot.bootloader' config option is currently set to: '{bootloader}'",
                );
            } else {
                println!("ostree repo 'sysroot.bootloader' config option is not set yet");
                self.migrate_static_grub_config(rootcxt.path.as_str(), &rootcxt.sysroot)?;
            };
        }
        Ok(Some(InstalledContent {
            meta: update.clone(),
            filetree: None,
            adopted_from: Some(meta.version),
        }))
    }

    fn query_update(&self, sysroot: &Dir) -> Result<Option<ContentMetadata>> {
        get_component_update(sysroot, self)
    }

    fn query_requires_update(&self, sysroot: &Dir) -> Result<()> {
        // Failed as expected if booted with BIOS and no update metadata
        if !sysroot.exists("sys/firmware/efi") {
            anyhow::bail!("Failed to find BIOS update metadata");
        }
        Ok(())
    }

    fn run_update(&self, rootcxt: &RootContext, _: &InstalledContent) -> Result<InstalledContent> {
        let updatemeta = self
            .query_update(&rootcxt.sysroot)?
            .expect("update available");

        for parent in rootcxt.device.find_all_roots()? {
            self.run_grub_install(rootcxt.path.as_str(), &parent)?;
            log::debug!("Installed grub modules on {}", parent.path());
        }

        let adopted_from = None;
        Ok(InstalledContent {
            meta: updatemeta,
            filetree: None,
            adopted_from,
        })
    }

    fn validate(&self, _: &InstalledContent, _device: &Device) -> Result<ValidationResult> {
        Ok(ValidationResult::Skip)
    }

    fn get_efi_vendor(&self, _: &Path) -> Result<Option<String>> {
        Ok(None)
    }
}
