/*
 * Copyright (C) 2020 Red Hat, Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

use std::cell::RefCell;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::process::Command;

use anyhow::{bail, Context, Result};
use bootc_internal_utils::CommandRunExt;
use camino::{Utf8Path, Utf8PathBuf};
use cap_std::ambient_authority;
use cap_std::fs::Dir;
use cap_std_ext::cap_std;
use cap_std_ext::dirext::CapStdExtDirExt;
use fn_error_context::context;
use os_release::OsRelease;
use rustix::{fd::AsFd, fd::BorrowedFd, fs::StatVfsMountFlags};
use walkdir::WalkDir;
use widestring::U16CString;

use bootc_internal_blockdev::Device;

use crate::bootupd::RootContext;
use crate::freezethaw::fsfreeze_thaw_cycle;
use crate::model::*;
use crate::ostreeutil;
use crate::util::{self, get_metadata_timestamp};
use crate::{component::*, packagesystem::*};
use crate::{filetree, grubconfigs};

/// Well-known paths to the ESP that may have been mounted external to us.
pub(crate) const ESP_MOUNTS: &[&str] = &["boot/efi", "efi", "boot"];

/// New efi dir under usr/lib
const EFILIB: &str = "usr/lib/efi";

/// The binary to change EFI boot ordering
const EFIBOOTMGR: &str = "efibootmgr";
#[cfg(target_arch = "aarch64")]
pub(crate) const SHIM: &str = "shimaa64.efi";

#[cfg(target_arch = "x86_64")]
pub(crate) const SHIM: &str = "shimx64.efi";

#[cfg(target_arch = "riscv64")]
pub(crate) const SHIM: &str = "shimriscv64.efi";

/// The mount path for uefi
const EFIVARFS: &str = "/sys/firmware/efi/efivars";

/// Systemd boot loader info EFI variable names
const LOADER_INFO_VAR_STR: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
const STUB_INFO_VAR_STR: &str = "StubInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";

/// The options of cp command for installation
const OPTIONS: &[&str] = &["-rp", "--reflink=auto"];

/// Check if the given path is a mount point via statx(MOUNT_ROOT).
fn is_mount_point(path: &Path) -> Result<bool> {
    use rustix::fs::{AtFlags, StatxAttributes, StatxFlags};
    // See https://github.com/coreos/cap-std-ext/blob/5493d689/src/dirext.rs#L514
    // CWD is unused here because callers always pass absolute paths.
    let r = rustix::fs::statx(
        rustix::fs::CWD,
        path,
        AtFlags::NO_AUTOMOUNT | AtFlags::SYMLINK_NOFOLLOW,
        StatxFlags::empty(),
    )?;
    if r.stx_attributes_mask.contains(StatxAttributes::MOUNT_ROOT) {
        Ok(r.stx_attributes.contains(StatxAttributes::MOUNT_ROOT))
    } else {
        anyhow::bail!(
            "could not determine if {path:?} is a mount point (kernel too old for MOUNT_ROOT)"
        )
    }
}

/// Return `true` if the system is booted via EFI
pub(crate) fn is_efi_booted() -> Result<bool> {
    Path::new("/sys/firmware/efi")
        .try_exists()
        .map_err(Into::into)
}

#[derive(Default)]
pub(crate) struct Efi {
    mountpoint: RefCell<Option<PathBuf>>,
}

impl Efi {
    // Get mounted point for esp
    pub(crate) fn get_mounted_esp(&self, root: &Path) -> Result<Option<PathBuf>> {
        // First check all potential mount points without holding the borrow
        let mut found_mount = None;
        for &mnt in ESP_MOUNTS.iter() {
            let path = root.join(mnt);
            if !path.exists() {
                continue;
            }

            let st = rustix::fs::statfs(&path)?;
            if st.f_type == libc::MSDOS_SUPER_MAGIC {
                // Verify this is actually a mount point, not just a subdirectory
                // on a vfat filesystem. Compare device IDs: a mount point has a
                // different device than its parent. Without this check, a vfat
                // subdirectory (e.g. /boot/efi on a vfat /boot) could be
                // misidentified as a mounted ESP.
                if !is_mount_point(&path)? {
                    // Same device as parent - this is a subdirectory, not a mount point
                    log::debug!("Skipping {path:?}: vfat but not a mount point");
                    continue;
                }
                util::ensure_writable_mount(&path)?;
                found_mount = Some(path);
                break;
            }
        }

        // Only borrow mutably if we found a mount point
        if let Some(mnt) = found_mount {
            log::debug!("Reusing existing mount point {mnt:?}");
            *self.mountpoint.borrow_mut() = Some(mnt.clone());
            Ok(Some(mnt))
        } else {
            Ok(None)
        }
    }

    // Mount the passed esp_device, return mount point
    pub(crate) fn mount_esp_device(&self, root: &Path, esp_device: &Path) -> Result<PathBuf> {
        let mut mountpoint = None;

        for &mnt in ESP_MOUNTS.iter() {
            let mnt = root.join(mnt);
            if !mnt.exists() {
                continue;
            }

            // Check if the target is already a mounted ESP (e.g. the host
            // already has the ESP mounted when running install-to-filesystem).
            let st = rustix::fs::statfs(&mnt)?;
            if st.f_type == libc::MSDOS_SUPER_MAGIC {
                if is_mount_point(&mnt)? {
                    log::debug!("ESP already mounted at {mnt:?}, reusing");
                    mountpoint = Some(mnt);
                    break;
                }
            }

            std::process::Command::new("mount")
                .arg(&esp_device)
                .arg(&mnt)
                .run_inherited()
                .with_context(|| format!("Failed to mount {:?}", esp_device))?;
            log::debug!("Mounted at {mnt:?}");
            mountpoint = Some(mnt);
            break;
        }
        let mnt = mountpoint.ok_or_else(|| anyhow::anyhow!("No mount point found"))?;
        *self.mountpoint.borrow_mut() = Some(mnt.clone());
        Ok(mnt)
    }

    // Firstly check if esp is already mounted, then mount the passed esp device
    pub(crate) fn ensure_mounted_esp(&self, root: &Path, esp_device: &Path) -> Result<PathBuf> {
        if let Some(mountpoint) = self.mountpoint.borrow().as_deref() {
            return Ok(mountpoint.to_owned());
        }
        let destdir = if let Some(destdir) = self.get_mounted_esp(Path::new(root))? {
            destdir
        } else {
            self.mount_esp_device(root, esp_device)?
        };
        Ok(destdir)
    }

    fn unmount(&self) -> Result<()> {
        if let Some(mount) = self.mountpoint.borrow_mut().take() {
            Command::new("umount")
                .arg(&mount)
                .run_inherited()
                .with_context(|| format!("Failed to unmount {mount:?}"))?;
            log::trace!("Unmounted");
        }
        Ok(())
    }

    #[context("Updating EFI firmware variables")]
    fn update_firmware(&self, device: &Device, espdir: &Dir, vendordir: &str) -> Result<()> {
        if !is_efi_booted()? {
            log::debug!("Not booted via EFI, skipping firmware update");
            return Ok(());
        }
        let sysroot = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
        let efi = sysroot
            .open_dir(EFIVARFS.strip_prefix("/").unwrap())
            .context("Opening efivars dir")?;
        let st = rustix::fs::fstatvfs(efi.as_fd())?;
        // Do nothing if efivars is readonly or empty
        // See https://github.com/coreos/bootupd/issues/972
        if st.f_flag.contains(StatVfsMountFlags::RDONLY)
            || std::fs::read_dir(EFIVARFS)?.next().is_none()
        {
            log::info!("Skipped EFI variables update: efivars not writable or empty");
            return Ok(());
        }

        // Check shim exists and return earlier if not
        let shim_path = format!("EFI/{vendordir}/{SHIM}");
        if !espdir.exists(&shim_path) {
            anyhow::bail!("Failed to find {shim_path}");
        }
        let loader = format!("\\EFI\\{vendordir}\\{SHIM}");

        let product_name = get_product_name(&sysroot)?;
        log::debug!("Get product name: '{product_name}'");
        assert!(product_name.len() > 0);

        // Multi-device is handled by the caller: Efi::install() is called once
        // per device in the loop at bootupd.rs install(), so each device's ESP
        // gets its firmware updated individually.
        let esp_part_num = device.get_esp_partition_number()?;

        // clear all the boot entries that match the target name
        clear_efi_target(&product_name)?;
        let device_path = device.path();
        create_efi_boot_entry(&device_path, esp_part_num.trim(), &loader, &product_name)
    }
}

#[context("Get product name")]
fn get_product_name(sysroot: &Dir) -> Result<String> {
    let release_path = "etc/system-release";
    if sysroot.exists(release_path) {
        let content = sysroot.read_to_string(release_path)?;
        let re = regex::Regex::new(r" *release.*").unwrap();
        let name = re.replace_all(&content, "").trim().to_string();
        return Ok(name);
    }
    // Read /etc/os-release
    let release: OsRelease = OsRelease::new()?;
    Ok(release.name)
}

/// Convert a nul-terminated UTF-16 byte array to a String.
fn string_from_utf16_bytes(slice: &[u8]) -> String {
    // For some reason, systemd appends 3 nul bytes after the string.
    // Drop the last byte if there's an odd number.
    let size = slice.len() / 2;
    let v: Vec<u16> = (0..size)
        .map(|i| u16::from_ne_bytes([slice[2 * i], slice[2 * i + 1]]))
        .collect();
    U16CString::from_vec(v).unwrap().to_string_lossy()
}

/// Read a nul-terminated UTF-16 string from an EFI variable.
fn read_efi_var_utf16_string(name: &str) -> Option<String> {
    let efivars = Path::new(EFIVARFS);
    if !efivars.exists() {
        log::trace!("No efivars mount at {:?}", efivars);
        return None;
    }
    let path = efivars.join(name);
    if !path.exists() {
        log::trace!("No EFI variable {name}");
        return None;
    }
    match std::fs::read(&path) {
        Ok(buf) => {
            // Skip the first 4 bytes, those are the EFI variable attributes.
            if buf.len() < 4 {
                log::warn!("Read less than 4 bytes from {:?}", path);
                return None;
            }
            Some(string_from_utf16_bytes(&buf[4..]))
        }
        Err(reason) => {
            log::warn!("Failed reading {:?}: {reason}", path);
            None
        }
    }
}

/// Read the LoaderInfo EFI variable if it exists.
fn get_loader_info() -> Option<String> {
    read_efi_var_utf16_string(LOADER_INFO_VAR_STR)
}

/// Read the StubInfo EFI variable if it exists.
fn get_stub_info() -> Option<String> {
    read_efi_var_utf16_string(STUB_INFO_VAR_STR)
}

/// Whether to skip adoption if a systemd bootloader is found.
fn skip_systemd_bootloaders() -> bool {
    if let Some(loader_info) = get_loader_info() {
        if loader_info.starts_with("systemd") {
            log::trace!("Skipping adoption for {:?}", loader_info);
            return true;
        }
    }
    if let Some(stub_info) = get_stub_info() {
        log::trace!("Skipping adoption for {:?}", stub_info);
        return true;
    }
    false
}

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

    fn query_adopt(&self, devices: &Option<Vec<Device>>) -> Result<Option<Adoptable>> {
        if devices.is_none() {
            log::trace!("No ESP detected");
            return Ok(None);
        };

        // Don't adopt if the system is booted with systemd-boot or
        // systemd-stub since those will be managed with bootctl.
        if skip_systemd_bootloaders() {
            return Ok(None);
        }
        crate::component::query_adopt_state()
    }

    // Backup "/boot/efi/EFI/{vendor}/grub.cfg" to "/boot/efi/EFI/{vendor}/grub.cfg.bak"
    // Replace "/boot/efi/EFI/{vendor}/grub.cfg" with new static "grub.cfg"
    fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &Dir) -> Result<()> {
        let sysroot = Dir::open_ambient_dir(sysroot_path, ambient_authority())
            .with_context(|| format!("Opening {sysroot_path}"))?;
        let Some(vendor) = self.get_efi_vendor(&Path::new(sysroot_path))? else {
            anyhow::bail!("Failed to find efi vendor");
        };

        // destdir is /boot/efi/EFI
        let efidir = destdir
            .open_dir(&vendor)
            .with_context(|| format!("Opening EFI/{}", vendor))?;

        if !efidir.exists(grubconfigs::GRUBCONFIG_BACKUP) {
            println!("Creating a backup of the current GRUB config on EFI");
            efidir
                .copy(
                    grubconfigs::GRUBCONFIG,
                    &efidir,
                    grubconfigs::GRUBCONFIG_BACKUP,
                )
                .context("Failed to backup GRUB config")?;
        }

        grubconfigs::install(&sysroot, None, Some(&vendor), true)?;
        // Synchronize the filesystem containing /boot/efi/EFI/{vendor} to disk.
        fsfreeze_thaw_cycle(efidir.reopen_as_ownedfd()?)?;

        Ok(())
    }

    /// Given an adoptable system and an update, perform the update.
    fn adopt_update(
        &self,
        rootcxt: &RootContext,
        updatemeta: &ContentMetadata,
        with_static_config: bool,
    ) -> Result<Option<InstalledContent>> {
        let esp_devices = rootcxt.device.find_colocated_esps()?;
        let Some(meta) = self.query_adopt(&esp_devices)? else {
            return Ok(None);
        };

        let updated_path = {
            let efilib_path = rootcxt.path.join(EFILIB);
            if efilib_path.exists() && get_efi_component_from_usr(&rootcxt.path, EFILIB)?.is_some()
            {
                PathBuf::from(EFILIB)
            } else {
                component_updatedirname(self)
            }
        };
        let updated = rootcxt
            .sysroot
            .open_dir(&updated_path)
            .with_context(|| format!("opening update dir {}", updated_path.display()))?;
        let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?;

        let esp_devices = esp_devices.unwrap_or_default();
        for esp in esp_devices {
            let destpath =
                &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp.path()))?;

            let efidir = Dir::open_ambient_dir(&destpath.join("EFI"), ambient_authority())
                .context("opening EFI dir")?;
            validate_esp_fstype(&efidir)?;

            // For adoption, we should only touch files that we know about.
            let diff = updatef.relative_diff_to(&efidir)?;
            log::trace!("applying adoption diff: {}", &diff);
            filetree::apply_diff(&updated, &efidir, &diff, None)
                .context("applying filesystem changes")?;

            // Backup current config and install static config
            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(), &efidir)?;
                };
            }

            // Do the sync before unmount
            fsfreeze_thaw_cycle(efidir.reopen_as_ownedfd()?)?;
            drop(efidir);
            self.unmount().context("unmount after adopt")?;
        }
        Ok(Some(InstalledContent {
            meta: updatemeta.clone(),
            filetree: Some(updatef),
            adopted_from: Some(meta.version),
        }))
    }

    fn install(
        &self,
        src_root: &str,
        dest_root: &str,
        device: Option<&Device>,
        update_firmware: bool,
    ) -> Result<InstalledContent> {
        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());
        };
        log::debug!("Found metadata {}", meta.version);

        // Determine the destination path for the ESP.
        // If a device is provided, find and mount its ESP partition (unmounting
        // any previously mounted ESP first to target the correct device).
        // If no device is provided, fall back to an already-mounted ESP.
        let destpath = if let Some(dev) = device {
            // Unmount any previously mounted ESP to ensure we install to the
            // correct device. This is important for multi-device installs where
            // each device has its own ESP.
            self.unmount()?;

            let esp_device = dev
                .find_partition_of_esp()
                .with_context(|| format!("Failed to find ESP device on {}", dev.path()))?;
            self.mount_esp_device(Path::new(dest_root), Path::new(&esp_device.path()))?
        } else if let Some(destdir) = self.get_mounted_esp(Path::new(dest_root))? {
            destdir
        } else {
            anyhow::bail!("No device specified and no mounted ESP found");
        };

        let destd = &Dir::open_ambient_dir(&destpath, ambient_authority())
            .with_context(|| format!("opening dest dir {}", destpath.display()))?;
        validate_esp_fstype(destd)?;

        let src_path = Utf8Path::new(src_root);
        let efi_comps = if src_path.join(EFILIB).exists() {
            get_efi_component_from_usr(&src_path, EFILIB)?
        } else {
            None
        };
        let dest = destpath.to_str().with_context(|| {
            format!(
                "Include invalid UTF-8 characters in dest {}",
                destpath.display()
            )
        })?;

        let efi_path = if let Some(efi_components) = efi_comps {
            for efi in efi_components {
                filetree::copy_dir_with_args(&src_dir, efi.path.as_str(), dest, OPTIONS)?;
            }
            EFILIB
        } else {
            let updates = component_updatedirname(self);
            let src = updates
                .to_str()
                .context("Include invalid UTF-8 characters in path")?;
            filetree::copy_dir_with_args(&src_dir, src, dest, OPTIONS)?;
            &src.to_owned()
        };

        // Get filetree from efi path
        let ft = crate::filetree::FileTree::new_from_dir(&src_dir.open_dir(efi_path)?)?;
        if update_firmware {
            if let Some(dev) = device {
                if let Some(vendordir) =
                    self.get_efi_vendor(src_path.join(efi_path).as_std_path())?
                {
                    self.update_firmware(dev, destd, &vendordir)?
                }
            }
        }
        Ok(InstalledContent {
            meta,
            filetree: Some(ft),
            adopted_from: None,
        })
    }

    fn run_update(
        &self,
        rootcxt: &RootContext,
        current: &InstalledContent,
    ) -> Result<InstalledContent> {
        let currentf = current
            .filetree
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("No filetree for installed EFI found!"))?;
        let sysroot_dir = &rootcxt.sysroot;
        let updatemeta = self.query_update(sysroot_dir)?.expect("update available");
        let updated_path = {
            let efilib_path = rootcxt.path.join(EFILIB);
            if efilib_path.exists() && get_efi_component_from_usr(&rootcxt.path, EFILIB)?.is_some()
            {
                PathBuf::from(EFILIB)
            } else {
                component_updatedirname(self)
            }
        };

        let updated = rootcxt
            .sysroot
            .open_dir(&updated_path)
            .with_context(|| format!("opening update dir {}", updated_path.display()))?;
        let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?;
        let diff = currentf.diff(&updatef)?;

        let Some(esp_devices) = rootcxt.device.find_colocated_esps()? else {
            anyhow::bail!("Failed to find all esp devices");
        };

        for esp in esp_devices {
            let destpath =
                &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp.path()))?;
            let destdir = Dir::open_ambient_dir(&destpath.join("EFI"), ambient_authority())
                .context("opening EFI dir")?;
            validate_esp_fstype(&destdir)?;
            log::trace!("applying diff: {}", &diff);
            filetree::apply_diff(&updated, &destdir, &diff, None)
                .context("applying filesystem changes")?;

            // Do the sync before unmount
            fsfreeze_thaw_cycle(destdir.reopen_as_ownedfd()?)?;
            drop(destdir);
            self.unmount().context("unmount after update")?;
        }

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

    fn generate_update_metadata(&self, sysroot: &str) -> Result<Option<ContentMetadata>> {
        let sysroot_path = Utf8Path::new(sysroot);
        let sysroot_dir = Dir::open_ambient_dir(sysroot_path, cap_std::ambient_authority())?;

        let ostreeboot = sysroot_dir
            .open_dir_optional(ostreeutil::BOOT_PREFIX)
            .context("Opening usr/lib/ostree-boot")?;

        // Newer images >= F44 have boot entries in /usr/lib/efi, but older images
        // have them in /usr/lib/ostree-boot, which we move to /usr/lib/bootupd/updates/EFI
        let metadata = if sysroot_path.join(EFILIB).exists() {
            println!("Generating metadata from {EFILIB}");
            generate_meta_from_usr_efi(sysroot_path)?
        } else {
            match &ostreeboot {
                Some(..) => {
                    println!(
                        "Moving {}/efi/EFI to {BOOTUPD_UPDATES_DIR}/EFI",
                        ostreeutil::BOOT_PREFIX
                    );
                    // Transfer usr/lib/ostree-boot/EFI files to usr/lib/bootupd/updates/EFI
                    transfer_ostree_boot_to_bootupd_updates(sysroot_path, self)?
                }

                // If /usr/lib/ostree-boot doesn't exist, assume /usr/lib/efi will
                None => anyhow::bail!("/usr/lib/ostree-boot and /usr/lib/efi not found"),
            }
        };

        if let Some(ostreeboot) = ostreeboot {
            let cruft = ["loader", "grub2"];
            for p in cruft.iter() {
                println!("Removing {p} from {}", ostreeutil::BOOT_PREFIX);
                ostreeboot.remove_all_optional(p)?;
            }

            println!("Removing efi/EFI from {}", ostreeutil::BOOT_PREFIX);
            // Remove usr/lib/ostree-boot/efi/EFI dir (after transfer) or if it is empty
            ostreeboot.remove_all_optional("efi/EFI")?;
        };

        write_update_metadata(sysroot_path.as_str(), self, &metadata)?;

        Ok(Some(metadata))
    }

    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 EFI and no update metadata
        if is_efi_booted()? {
            anyhow::bail!("Failed to find EFI update metadata");
        }
        Ok(())
    }

    fn validate(&self, current: &InstalledContent, device: &Device) -> Result<ValidationResult> {
        let esp_devices = device.find_colocated_esps()?;
        if !is_efi_booted()? && esp_devices.is_none() {
            return Ok(ValidationResult::Skip);
        }
        let currentf = current
            .filetree
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("No filetree for installed EFI found!"))?;

        let mut errs = Vec::new();
        let esp_devices = esp_devices.unwrap_or_default();
        for esp in esp_devices.iter() {
            let destpath = &self.ensure_mounted_esp(Path::new("/"), Path::new(&esp.path()))?;

            let efidir = Dir::open_ambient_dir(&destpath.join("EFI"), ambient_authority())
                .with_context(|| format!("opening EFI dir {}", destpath.display()))?;
            let diff = currentf.relative_diff_to(&efidir)?;

            for f in diff.changes.iter() {
                errs.push(format!("Changed: {}", f));
            }
            for f in diff.removals.iter() {
                errs.push(format!("Removed: {}", f));
            }
            assert_eq!(diff.additions.len(), 0);
            drop(efidir);
            self.unmount().context("unmount after validate")?;
        }

        if !errs.is_empty() {
            Ok(ValidationResult::Errors(errs))
        } else {
            Ok(ValidationResult::Valid)
        }
    }

    fn get_efi_vendor(&self, sysroot: &Path) -> Result<Option<String>> {
        let efi_lib = sysroot.join(EFILIB);
        let updates = sysroot.join(component_updatedirname(self));

        let paths: [&Path; 3] = [&efi_lib, &updates, sysroot];
        let target = paths
            .into_iter()
            .find(|p| p.exists())
            .ok_or_else(|| anyhow::anyhow!("Failed to find valid target path"))?;
        let shim_files = find_file_recursive(target, SHIM)?;

        // Does not support multiple shim for efi
        if shim_files.len() > 1 {
            anyhow::bail!("Found multiple {SHIM} in the image");
        }
        if let Some(p) = shim_files.first() {
            let p = p
                .parent()
                .unwrap()
                .file_name()
                .ok_or_else(|| anyhow::anyhow!("No file name found"))?;
            Ok(Some(p.to_string_lossy().into_owned()))
        } else {
            anyhow::bail!("Failed to find {SHIM} in the image")
        }
    }
}

impl Drop for Efi {
    fn drop(&mut self) {
        log::debug!("Unmounting");
        let _ = self.unmount();
    }
}

fn validate_esp_fstype(dir: &Dir) -> Result<()> {
    let dir = unsafe { BorrowedFd::borrow_raw(dir.as_raw_fd()) };
    let stat = rustix::fs::fstatfs(&dir)?;
    if stat.f_type != libc::MSDOS_SUPER_MAGIC {
        bail!(
            "EFI mount is not a msdos filesystem, but is {:?}",
            stat.f_type
        );
    };
    Ok(())
}

#[derive(Debug, PartialEq)]
struct BootEntry {
    id: String,
    name: String,
}

/// Parse boot entries from efibootmgr output
fn parse_boot_entries(output: &str) -> Vec<BootEntry> {
    let mut entries = Vec::new();

    for line in output.lines().filter_map(|line| line.strip_prefix("Boot")) {
        // Need to consider if output only has "Boot0000* UiApp", without additional info
        if line.starts_with('0') {
            let parts = if let Some((parts, _)) = line.split_once('\t') {
                parts
            } else {
                line
            };
            if let Some((id, name)) = parts.split_once(' ') {
                let id = id.trim_end_matches('*').to_string();
                let name = name.trim().to_string();
                entries.push(BootEntry { id, name });
            }
        }
    }
    entries
}

#[context("Clearing EFI boot entries that match target {target}")]
pub(crate) fn clear_efi_target(target: &str) -> Result<()> {
    let target = target.to_lowercase();
    let output = Command::new(EFIBOOTMGR).output()?;
    if !output.status.success() {
        anyhow::bail!("Failed to invoke {EFIBOOTMGR}")
    }

    let output = String::from_utf8(output.stdout)?;
    let boot_entries = parse_boot_entries(&output);
    for entry in boot_entries {
        if entry.name.to_lowercase() == target {
            log::debug!("Deleting matched target {:?}", entry);
            let mut cmd = Command::new(EFIBOOTMGR);
            cmd.args(["-b", entry.id.as_str(), "-B"]);
            println!("Executing: {cmd:?}");
            cmd.run_inherited_with_cmd_context()?;
        }
    }

    anyhow::Ok(())
}

#[context("Adding new EFI boot entry")]
pub(crate) fn create_efi_boot_entry(
    device: &str,
    esp_partition_number: &str,
    loader: &str,
    target: &str,
) -> Result<()> {
    log::debug!("Creating new EFI boot entry using '{target}'");
    let mut cmd = Command::new(EFIBOOTMGR);
    cmd.args([
        "--create",
        "--disk",
        device,
        "--part",
        esp_partition_number,
        "--loader",
        loader,
        "--label",
        target,
    ]);
    println!("Executing: {cmd:?}");
    cmd.run_inherited_with_cmd_context()
}

#[context("Find target file recursively")]
fn find_file_recursive<P: AsRef<Path>>(dir: P, target_file: &str) -> Result<Vec<PathBuf>> {
    let mut result = Vec::new();

    for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
        if entry.file_type().is_file() {
            if let Some(file_name) = entry.file_name().to_str() {
                if file_name == target_file {
                    if let Some(path) = entry.path().to_str() {
                        result.push(path.into());
                    }
                }
            }
        }
    }

    Ok(result)
}

#[context("Generating metadata from usr/lib/efi")]
fn generate_meta_from_usr_efi(sysroot_path: &Utf8Path) -> Result<ContentMetadata> {
    let Some(efi_components) = get_efi_component_from_usr(sysroot_path, EFILIB)? else {
        anyhow::bail!("Failed to find EFI components");
    };

    let mut packages = Vec::new();
    let mut modules_vec: Vec<Module> = vec![];
    for efi in efi_components {
        packages.push(format!("{}-{}", efi.name, efi.version));
        modules_vec.push(Module {
            name: efi.name,
            rpm_evr: efi.version,
        });
    }
    modules_vec.sort_unstable();

    let meta = ContentMetadata {
        timestamp: get_metadata_timestamp()?,
        version: packages.join(","),
        versions: Some(modules_vec),
    };

    Ok(meta)
}

#[derive(Debug, PartialEq, Eq)]
pub struct EFIComponent {
    pub name: String,
    pub version: String,
    path: Utf8PathBuf,
}

/// Get EFIComponents from e.g. usr/lib/efi, like "usr/lib/efi/<name>/<version>/EFI"
fn get_efi_component_from_usr<'a>(
    sysroot: &'a Utf8Path,
    usr_path: &'a str,
) -> Result<Option<Vec<EFIComponent>>> {
    let efilib_path = sysroot.join(usr_path);
    let skip_count = Utf8Path::new(usr_path).components().count();

    let mut components: Vec<EFIComponent> = WalkDir::new(&efilib_path)
        .min_depth(3) // <name>/<version>/EFI: so 3 levels down
        .max_depth(3)
        .into_iter()
        .filter_map(|entry| {
            let entry = entry.ok()?;
            if !entry.file_type().is_dir() || entry.file_name() != "EFI" {
                return None;
            }

            let abs_path = entry.path();
            let rel_path = abs_path.strip_prefix(sysroot).ok()?;
            let utf8_rel_path = Utf8PathBuf::from_path_buf(rel_path.to_path_buf()).ok()?;

            let mut components = utf8_rel_path.components();

            let name = components.nth(skip_count)?.to_string();
            let version = components.next()?.to_string();

            Some(EFIComponent {
                name,
                version,
                path: utf8_rel_path,
            })
        })
        .collect();

    if components.len() == 0 {
        return Ok(None);
    }
    components.sort_by(|a, b| a.name.cmp(&b.name));

    Ok(Some(components))
}

/// Copies usr/lib/ostree-boot/EFI to usr/lib/bootupd/updates
fn transfer_ostree_boot_to_bootupd_updates(
    sysroot: &Utf8Path,
    component: &Efi,
) -> Result<ContentMetadata> {
    let ostreebootdir = sysroot.join(ostreeutil::BOOT_PREFIX);

    // move EFI files to updates dir from /usr/lib/ostree-boot
    if !ostreebootdir.exists() {
        anyhow::bail!("Failed to find {ostreebootdir}");
    }

    let efisrc = ostreebootdir.join("efi/EFI");
    if !efisrc.exists() {
        bail!("Failed to find {:?}", &efisrc);
    }

    let dest_efidir = component_updatedir(sysroot.as_str(), component);
    let dest_efidir = Utf8PathBuf::from_path_buf(dest_efidir).expect("Path is invalid UTF-8");

    // Fork off mv() because on overlayfs one can't rename() a lower level
    // directory today, and this will handle the copy fallback.
    Command::new("mv")
        .args([&efisrc, &dest_efidir])
        .run_capture_stderr()?;

    let efidir = Dir::open_ambient_dir(dest_efidir.as_std_path(), ambient_authority())
        .with_context(|| format!("Opening {}", dest_efidir))?;

    let files = crate::util::filenames(&efidir)?.into_iter().map(|mut f| {
        f.insert_str(0, "/boot/efi/EFI");
        f
    });

    let files = files.collect::<Vec<_>>();

    query_files(sysroot.as_str(), files)
}

#[cfg(test)]
mod tests {
    use cap_std_ext::dirext::CapStdExtDirExt;

    use super::*;

    #[test]
    fn test_parse_boot_entries() -> Result<()> {
        let output = r"
BootCurrent: 0003
Timeout: 0 seconds
BootOrder: 0003,0001,0000,0002
Boot0000* UiApp	FvVol(7cb8bdc9-f8eb-4f34-aaea-3ee4af6516a1)/FvFile(462caa21-7614-4503-836e-8ab6f4662331)
Boot0001* UEFI Misc Device	PciRoot(0x0)/Pci(0x3,0x0){auto_created_boot_option}
Boot0002* EFI Internal Shell	FvVol(7cb8bdc9-f8eb-4f34-aaea-3ee4af6516a1)/FvFile(7c04a583-9e3e-4f1c-ad65-e05268d0b4d1)
Boot0003* Fedora	HD(2,GPT,94ff4025-5276-4bec-adea-e98da271b64c,0x1000,0x3f800)/\EFI\fedora\shimx64.efi";
        let entries = parse_boot_entries(output);
        assert_eq!(
            entries,
            [
                BootEntry {
                    id: "0000".to_string(),
                    name: "UiApp".to_string()
                },
                BootEntry {
                    id: "0001".to_string(),
                    name: "UEFI Misc Device".to_string()
                },
                BootEntry {
                    id: "0002".to_string(),
                    name: "EFI Internal Shell".to_string()
                },
                BootEntry {
                    id: "0003".to_string(),
                    name: "Fedora".to_string()
                }
            ]
        );
        let output = r"
BootCurrent: 0003
Timeout: 0 seconds
BootOrder: 0003,0001,0000,0002";
        let entries = parse_boot_entries(output);
        assert_eq!(entries, []);

        let output = r"
BootCurrent: 0003
Timeout: 0 seconds
BootOrder: 0003,0001,0000,0002
Boot0000* UiApp
Boot0001* UEFI Misc Device
Boot0002* EFI Internal Shell
Boot0003* test";
        let entries = parse_boot_entries(output);
        assert_eq!(
            entries,
            [
                BootEntry {
                    id: "0000".to_string(),
                    name: "UiApp".to_string()
                },
                BootEntry {
                    id: "0001".to_string(),
                    name: "UEFI Misc Device".to_string()
                },
                BootEntry {
                    id: "0002".to_string(),
                    name: "EFI Internal Shell".to_string()
                },
                BootEntry {
                    id: "0003".to_string(),
                    name: "test".to_string()
                }
            ]
        );
        Ok(())
    }
    #[cfg(test)]
    fn fixture() -> Result<cap_std_ext::cap_tempfile::TempDir> {
        let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
        tempdir.create_dir("etc")?;
        Ok(tempdir)
    }
    #[test]
    fn test_get_product_name() -> Result<()> {
        let tmpd = fixture()?;
        {
            tmpd.atomic_write("etc/system-release", "Fedora release 40 (Forty)")?;
            let name = get_product_name(&tmpd)?;
            assert_eq!("Fedora", name);
        }
        {
            tmpd.atomic_write("etc/system-release", "CentOS Stream release 9")?;
            let name = get_product_name(&tmpd)?;
            assert_eq!("CentOS Stream", name);
        }
        {
            tmpd.atomic_write(
                "etc/system-release",
                "Red Hat Enterprise Linux CoreOS release 4",
            )?;
            let name = get_product_name(&tmpd)?;
            assert_eq!("Red Hat Enterprise Linux CoreOS", name);
        }
        {
            tmpd.atomic_write(
                "etc/system-release",
                "Red Hat Enterprise Linux CoreOS release 4
                ",
            )?;
            let name = get_product_name(&tmpd)?;
            assert_eq!("Red Hat Enterprise Linux CoreOS", name);
        }
        {
            tmpd.remove_file("etc/system-release")?;
            let name = get_product_name(&tmpd)?;
            assert!(name.len() > 0);
        }
        Ok(())
    }

    #[test]
    fn test_get_efi_component_from_usr() -> Result<()> {
        let tmpdir: &tempfile::TempDir = &tempfile::tempdir()?;
        let tpath = tmpdir.path();
        let efi_path = tpath.join("usr/lib/efi");
        std::fs::create_dir_all(efi_path.join("BAR/1.1/EFI"))?;
        std::fs::create_dir_all(efi_path.join("FOO/1.1/EFI"))?;
        std::fs::create_dir_all(efi_path.join("FOOBAR/1.1/test"))?;
        let utf8_tpath =
            Utf8Path::from_path(tpath).ok_or_else(|| anyhow::anyhow!("Path is not valid UTF-8"))?;
        let efi_comps = get_efi_component_from_usr(utf8_tpath, EFILIB)?;
        assert_eq!(
            efi_comps,
            Some(vec![
                EFIComponent {
                    name: "BAR".to_string(),
                    version: "1.1".to_string(),
                    path: Utf8PathBuf::from("usr/lib/efi/BAR/1.1/EFI"),
                },
                EFIComponent {
                    name: "FOO".to_string(),
                    version: "1.1".to_string(),
                    path: Utf8PathBuf::from("usr/lib/efi/FOO/1.1/EFI"),
                },
            ])
        );
        std::fs::remove_dir_all(efi_path.join("BAR/1.1/EFI"))?;
        std::fs::remove_dir_all(efi_path.join("FOO/1.1/EFI"))?;
        let efi_comps = get_efi_component_from_usr(utf8_tpath, EFILIB)?;
        assert_eq!(efi_comps, None);
        Ok(())
    }
}
