#[cfg(any(target_arch = "x86_64", target_arch = "powerpc64"))]
use crate::bios;
use crate::component;
use crate::component::{Component, ValidationResult};
use crate::coreos;
#[cfg(any(
    target_arch = "x86_64",
    target_arch = "aarch64",
    target_arch = "riscv64"
))]
use crate::efi;
use crate::freezethaw::fsfreeze_thaw_cycle;
#[cfg(any(
    target_arch = "x86_64",
    target_arch = "aarch64",
    target_arch = "powerpc64",
    target_arch = "riscv64"
))]
use crate::grubconfigs::{ensure_grub_permissions, GRUB2DIR};
use crate::model::{ComponentStatus, ComponentUpdatable, ContentMetadata, SavedState, Status};
use crate::{ostreeutil, util};
use anyhow::{anyhow, Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use cap_std_ext::dirext::CapStdExtDirExt;
use clap::crate_version;
use fn_error_context::context;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};

use bootc_internal_blockdev::Device;
use cap_std::ambient_authority;
use cap_std::fs::{Dir, Permissions, PermissionsExt};
use cap_std_ext::cap_std;

pub(crate) enum ConfigMode {
    None,
    Static,
    WithUUID,
}

impl ConfigMode {
    pub(crate) fn enabled_with_uuid(&self) -> Option<bool> {
        match self {
            ConfigMode::None => None,
            ConfigMode::Static => Some(false),
            ConfigMode::WithUUID => Some(true),
        }
    }
}

pub(crate) fn install(
    source_root: &str,
    dest_root: &str,
    devices: &[Device],
    configs: ConfigMode,
    update_firmware: bool,
    target_components: Option<&[String]>,
    auto_components: bool,
) -> Result<()> {
    let source_root_dir =
        Dir::open_ambient_dir(source_root, ambient_authority()).context("Opening source root")?;
    SavedState::ensure_not_present(dest_root)
        .context("failed to install, invalid re-install attempted")?;

    let all_components = get_components_impl(auto_components);
    if all_components.is_empty() {
        println!("No components available for this platform.");
        return Ok(());
    }
    let target_components = if let Some(target_components) = target_components {
        // Checked by CLI parser
        assert!(!auto_components);
        target_components
            .iter()
            .map(|name| {
                all_components
                    .get(name.as_str())
                    .ok_or_else(|| anyhow!("Unknown component: {name}"))
            })
            .collect::<Result<Vec<_>>>()?
    } else {
        all_components.values().collect()
    };

    if target_components.is_empty() && !auto_components {
        anyhow::bail!("No components specified");
    }

    let mut state = SavedState::default();
    let mut installed_efi_vendor = None;
    for &component in target_components.iter() {
        // skip for BIOS if no devices specified
        if component.name() == "BIOS" && devices.is_empty() {
            println!(
                "Skip installing component {} without target device",
                component.name()
            );
            continue;
        }

        // skip components that don't have an update metadata
        if component.query_update(&source_root_dir)?.is_none() {
            println!(
                "Skip installing component {} without update metadata",
                component.name()
            );
            continue;
        }

        // Determine which devices to install to. For EFI, filter to only
        // devices that have an ESP partition.
        let devices_to_install: Vec<Option<&Device>> = if devices.is_empty() {
            // No devices specified: install once with auto-detection (None).
            vec![None]
        } else if component.name() == "EFI" {
            // For EFI, only install to devices that have an ESP partition.
            let esp_devices: Vec<&Device> = devices
                .iter()
                .filter(|dev| match dev.find_partition_of_esp() {
                    Ok(_) => true,
                    Err(e) => {
                        log::warn!("Skipping device {} for EFI: {:#}", dev.path(), e);
                        false
                    }
                })
                .collect();
            if esp_devices.is_empty() {
                // No devices had a detectable ESP partition. This can happen with
                // loopback devices or other setups where partition scanning fails.
                // Fall back to auto-detection — the EFI component will look for
                // an already-mounted ESP at the destination.
                log::info!(
                    "No ESP partitions detected on specified devices; \
                     falling back to mounted ESP detection"
                );
                vec![None]
            } else {
                esp_devices.into_iter().map(Some).collect()
            }
        } else {
            devices.iter().map(|dev| Some(dev)).collect()
        };

        for device in &devices_to_install {
            let device_desc = device.map_or("(auto)".to_string(), |d| d.path());
            let meta = component
                .install(source_root, dest_root, *device, update_firmware)
                .with_context(|| {
                    format!(
                        "installing component {} to device {}",
                        component.name(),
                        device_desc,
                    )
                })?;
            log::info!(
                "Installed {} {} to {}",
                component.name(),
                meta.meta.version,
                device_desc,
            );
            // State is tracked per-component, not per-device. The metadata
            // (version, filetree, etc.) is identical across all devices, so we
            // only need to record it once. Updates and validation discover all
            // colocated devices at runtime (e.g. via find_colocated_esps()).
            if !state.installed.contains_key(component.name()) {
                state.installed.insert(component.name().into(), meta);
            }
            // Yes this is a hack...the Component thing just turns out to be too generic.
            if installed_efi_vendor.is_none() {
                if let Some(vendor) = component.get_efi_vendor(Path::new(source_root))? {
                    installed_efi_vendor = Some(vendor);
                }
            }
        }
    }
    let sysroot = &Dir::open_ambient_dir(dest_root, ambient_authority())?;

    #[cfg(any(
        target_arch = "x86_64",
        target_arch = "aarch64",
        target_arch = "powerpc64",
        target_arch = "riscv64"
    ))]
    match configs.enabled_with_uuid() {
        Some(uuid) => {
            let meta = get_static_config_meta()?;
            state.static_configs = Some(meta);
            crate::grubconfigs::install(
                sysroot,
                Some(&source_root_dir),
                installed_efi_vendor.as_deref(),
                uuid,
            )?;
            // On other architectures, assume that there's nothing to do.
        }
        None => {}
    }

    // Unmount the ESP, etc.
    drop(target_components);

    let mut state_guard =
        SavedState::unlocked(sysroot.try_clone()?).context("failed to acquire write lock")?;
    state_guard
        .update_state(&state)
        .context("failed to update state")?;

    Ok(())
}

#[context("Get static config metadata")]
fn get_static_config_meta() -> Result<ContentMetadata> {
    let self_bin_meta = std::fs::metadata("/proc/self/exe").context("Querying self meta")?;
    let self_meta = ContentMetadata {
        timestamp: self_bin_meta.modified()?.into(),
        version: crate_version!().into(),
        versions: None,
    };
    Ok(self_meta)
}

type Components = BTreeMap<&'static str, Box<dyn Component>>;

#[allow(clippy::box_default)]
/// Return the set of known components; if `auto` is specified then the system
/// filters to the target booted state.
pub(crate) fn get_components_impl(_auto: bool) -> Components {
    #[cfg(not(target_arch = "s390x"))]
    let mut components = BTreeMap::new();

    #[cfg(target_arch = "s390x")]
    let components = BTreeMap::new();

    fn insert_component(components: &mut Components, component: Box<dyn Component>) {
        components.insert(component.name(), component);
    }

    #[cfg(target_arch = "x86_64")]
    {
        if _auto {
            let is_efi_booted = crate::efi::is_efi_booted().unwrap();
            log::info!(
                "System boot method: {}",
                if is_efi_booted { "EFI" } else { "BIOS" }
            );
            if is_efi_booted {
                insert_component(&mut components, Box::new(efi::Efi::default()));
            } else {
                insert_component(&mut components, Box::new(bios::Bios::default()));
            }
        } else {
            insert_component(&mut components, Box::new(bios::Bios::default()));
            insert_component(&mut components, Box::new(efi::Efi::default()));
        }
    }
    #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
    insert_component(&mut components, Box::new(efi::Efi::default()));

    #[cfg(target_arch = "powerpc64")]
    insert_component(&mut components, Box::new(bios::Bios::default()));

    components
}

pub(crate) fn get_components() -> Components {
    get_components_impl(false)
}

pub(crate) fn generate_update_metadata(sysroot_path: &str) -> Result<()> {
    // create bootupd update dir which will save component metadata files for both components
    let updates_dir = Path::new(sysroot_path).join(crate::model::BOOTUPD_UPDATES_DIR);
    std::fs::create_dir_all(&updates_dir)
        .with_context(|| format!("Failed to create updates dir {:?}", &updates_dir))?;
    for component in get_components().values() {
        if let Some(v) = component.generate_update_metadata(sysroot_path)? {
            println!(
                "Generated update layout for {}: {}",
                component.name(),
                v.version,
            );
        } else {
            println!(
                "Generating update layout for {} was not possible, skipping.",
                component.name(),
            );
        }
    }

    Ok(())
}

/// Return value from daemon → client for component update
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum ComponentUpdateResult {
    AtLatestVersion,
    Updated {
        previous: ContentMetadata,
        interrupted: Option<ContentMetadata>,
        new: ContentMetadata,
    },
}

fn ensure_writable_boot() -> Result<()> {
    util::ensure_writable_mount("/boot")
}

/// daemon implementation of component update
pub(crate) fn update(name: &str, rootcxt: &RootContext) -> Result<ComponentUpdateResult> {
    let mut state = SavedState::load_from_disk("/")?.unwrap_or_default();
    let component = component::new_from_name(name)?;
    let inst = if let Some(inst) = state.installed.get(name) {
        inst.clone()
    } else {
        anyhow::bail!("Component {} is not installed", name);
    };
    let sysroot = &rootcxt.sysroot;
    let update = component.query_update(sysroot)?;
    let update = match update.as_ref() {
        Some(p) => match inst.meta.can_upgrade_to(p) {
            std::cmp::Ordering::Less => p, // current < available -> upgrade
            _ => return Ok(ComponentUpdateResult::AtLatestVersion),
        },
        None => {
            component.query_requires_update(sysroot)?;
            return Ok(ComponentUpdateResult::AtLatestVersion);
        }
    };

    ensure_writable_boot()?;
    // Verify the permissions of grub files are 0600
    #[cfg(any(
        target_arch = "x86_64",
        target_arch = "aarch64",
        target_arch = "powerpc64",
        target_arch = "riscv64"
    ))]
    {
        let grub2dir = &sysroot
            .open_dir(format!("boot/{GRUB2DIR}"))
            .context("Opening /boot/grub2")?;
        ensure_grub_permissions(grub2dir)?;
    }

    let mut pending_container = state.pending.take().unwrap_or_default();
    let interrupted = pending_container.get(component.name()).cloned();
    pending_container.insert(component.name().into(), update.clone());
    let sysroot = sysroot.try_clone()?;
    let mut state_guard =
        SavedState::acquire_write_lock(sysroot).context("Failed to acquire write lock")?;
    state_guard
        .update_state(&state)
        .context("Failed to update state")?;

    let newinst = component
        .run_update(rootcxt, &inst)
        .with_context(|| format!("Failed to update {}", component.name()))?;
    state.installed.insert(component.name().into(), newinst);
    pending_container.remove(component.name());
    state_guard.update_state(&state)?;

    Ok(ComponentUpdateResult::Updated {
        previous: inst.meta,
        interrupted,
        new: update.clone(),
    })
}

/// daemon implementation of component adoption
pub(crate) fn adopt_and_update(
    name: &str,
    rootcxt: &RootContext,
    with_static_config: bool,
) -> Result<Option<ContentMetadata>> {
    let sysroot = &rootcxt.sysroot;
    let mut state = SavedState::load_from_disk("/")?.unwrap_or_default();
    let component = component::new_from_name(name)?;
    if state.installed.contains_key(name) {
        anyhow::bail!("Component {} is already installed", name);
    };

    ensure_writable_boot()?;
    // Verify the permissions of grub files are 0600
    #[cfg(any(
        target_arch = "x86_64",
        target_arch = "aarch64",
        target_arch = "powerpc64",
        target_arch = "riscv64"
    ))]
    {
        let grub2dir = &sysroot
            .open_dir(format!("boot/{GRUB2DIR}"))
            .context("Opening /boot/grub2")?;
        ensure_grub_permissions(grub2dir)?;
    }

    let Some(update) = component.query_update(sysroot)? else {
        anyhow::bail!("Component {} has no available update", name);
    };

    let sysroot = sysroot.try_clone()?;
    let mut state_guard =
        SavedState::acquire_write_lock(sysroot).context("Failed to acquire write lock")?;

    let inst = component
        .adopt_update(&rootcxt, &update, with_static_config)
        .context("Failed adopt and update")?;
    if let Some(inst) = inst {
        state.installed.insert(component.name().into(), inst);
        // Set static_configs metadata and save
        if with_static_config && state.static_configs.is_none() {
            let meta = get_static_config_meta()?;
            state.static_configs = Some(meta);
            // Set bootloader to none
            ostreeutil::set_ostree_bootloader("none")?;

            println!("Static GRUB configuration has been adopted successfully.");
        }
        state_guard.update_state(&state)?;
        return Ok(Some(update));
    } else {
        // Nothing adopted, skip
        log::info!("Component '{}' skipped adoption", component.name());
        return Ok(None);
    }
}

/// Get the block device backing the current root by trying `/boot` first,
/// then falling back to `/sysroot`. This avoids issues with virtual
/// filesystems like composefs that are mounted on `/`.
#[context("Finding block device from boot or sysroot")]
fn list_dev_current_root() -> Result<Device> {
    let auth = cap_std::ambient_authority();
    for path in ["/boot", "/sysroot"] {
        if let Ok(dir) = Dir::open_ambient_dir(path, auth) {
            if let Ok(dev) = bootc_internal_blockdev::list_dev_by_dir(&dir) {
                return Ok(dev);
            }
        }
    }
    anyhow::bail!("Failed to find block device from /boot or /sysroot")
}

/// daemon implementation of component validate
pub(crate) fn validate(name: &str) -> Result<ValidationResult> {
    let state = SavedState::load_from_disk("/")?.unwrap_or_default();
    let component = component::new_from_name(name)?;
    let Some(inst) = state.installed.get(name) else {
        anyhow::bail!("Component {} is not installed", name);
    };
    let device = list_dev_current_root()?;
    component.validate(inst, &device)
}

pub(crate) fn status() -> Result<Status> {
    let mut ret: Status = Default::default();
    let mut known_components = get_components();
    let sysroot = Dir::open_ambient_dir("/", ambient_authority())?;
    let state = SavedState::load_from_disk("/")?;
    if let Some(state) = state {
        for (name, ic) in state.installed.iter() {
            log::trace!("Gathering status for installed component: {}", name);
            let component = known_components
                .remove(name.as_str())
                .ok_or_else(|| anyhow!("Unknown component installed: {}", name))?;
            let component = component.as_ref();
            let interrupted = state.pending.as_ref().and_then(|p| p.get(name.as_str()));
            let update = component.query_update(&sysroot)?;
            let updatable = ComponentUpdatable::from_metadata(&ic.meta, update.as_ref());
            let adopted_from = ic.adopted_from.clone();
            ret.components.insert(
                name.to_string(),
                ComponentStatus {
                    installed: ic.meta.clone(),
                    interrupted: interrupted.cloned(),
                    update,
                    updatable,
                    adopted_from,
                },
            );
        }
    } else {
        log::trace!("No saved state");
    }

    // Process the remaining components not installed
    log::trace!("Remaining known components: {}", known_components.len());
    for (name, _) in known_components {
        // To determine if not-installed components can be adopted:
        //
        // `query_adopt_state()` checks for existing installation state,
        // such as a `version` in `/sysroot/.coreos-aleph-version.json`,
        // or the presence of `/ostree/deploy`.
        //
        // `component.query_adopt()` performs additional checks,
        // including hardware/device requirements.
        // For example, it will skip BIOS adoption if the system is booted via EFI
        // and lacks a BIOS_BOOT partition.
        //
        // Once a component is determined to be adoptable, it is added to the
        // adoptable list, and adoption proceeds automatically.
        //
        // Therefore, calling `query_adopt_state()` alone is sufficient.
        if let Some(adopt_ver) = component::query_adopt_state()? {
            let component = component::new_from_name(&name)?;
            // Skip if the update metadata could not be found
            if component.query_update(&sysroot)?.is_none() {
                continue;
            };
            ret.adoptable.insert(name.to_string(), adopt_ver);
        } else {
            log::trace!("Not adoptable: {}", name);
        }
    }

    Ok(ret)
}

pub(crate) fn print_status_avail(status: &Status) -> Result<()> {
    let mut avail = Vec::new();
    for (name, component) in status.components.iter() {
        if let ComponentUpdatable::Upgradable = component.updatable {
            avail.push(name.as_str());
        }
    }
    for (name, adoptable) in status.adoptable.iter() {
        if adoptable.confident {
            avail.push(name.as_str());
        }
    }
    if !avail.is_empty() {
        println!("Updates available: {}", avail.join(" "));
    }
    Ok(())
}

pub(crate) fn print_status(status: &Status) -> Result<()> {
    if status.components.is_empty() {
        println!("No components installed.");
    }
    for (name, component) in status.components.iter() {
        println!("Component {}", name);
        println!("  Installed: {}", component.installed.version);

        if let Some(i) = component.interrupted.as_ref() {
            println!(
                "  WARNING: Previous update to {} was interrupted",
                i.version
            );
        }
        let msg = match component.updatable {
            ComponentUpdatable::NoUpdateAvailable => Cow::Borrowed("No update found"),
            ComponentUpdatable::AtLatestVersion => Cow::Borrowed("At latest version"),
            ComponentUpdatable::WouldDowngrade => Cow::Borrowed("Ignoring downgrade"),
            ComponentUpdatable::Upgradable => Cow::Owned(format!(
                "Available: {}",
                component.update.as_ref().expect("update").version
            )),
        };
        println!("  Update: {}", msg);
    }

    if status.adoptable.is_empty() {
        println!("No components are adoptable.");
    }
    for (name, adopt) in status.adoptable.iter() {
        let ver = &adopt.version.version;
        if adopt.confident {
            println!("Detected: {}: {}", name, ver);
        } else {
            println!("Adoptable: {}: {}", name, ver);
        }
    }

    if let Some(coreos_aleph) = coreos::get_aleph_version(Path::new("/"))? {
        println!("CoreOS aleph version: {}", coreos_aleph.aleph.version);
    }

    #[cfg(any(
        target_arch = "x86_64",
        target_arch = "aarch64",
        target_arch = "riscv64"
    ))]
    {
        let boot_method = if efi::is_efi_booted()? { "EFI" } else { "BIOS" };
        println!("Boot method: {}", boot_method);
    }

    Ok(())
}

pub struct RootContext {
    pub sysroot: Dir,
    pub path: Utf8PathBuf,

    /// The block device backing the root filesystem.
    ///
    /// Used to determine the device to install to for components that need
    /// it, and passed to component update/adoption logic for validation.
    pub device: Device,
}

impl RootContext {
    fn new(sysroot: Dir, path: &str, device: Device) -> Self {
        Self {
            sysroot,
            path: Utf8Path::new(path).into(),
            device,
        }
    }
}

/// Initialize parent devices to prepare the update
fn prep_before_update() -> Result<RootContext> {
    let path = "/";
    let sysroot = Dir::open_ambient_dir(path, ambient_authority()).context("Opening root dir")?;
    let device = list_dev_current_root()?;
    Ok(RootContext::new(sysroot, path, device))
}

pub(crate) fn client_run_update() -> Result<()> {
    crate::try_fail_point!("update");
    let rootcxt = prep_before_update()?;
    let status: Status = status()?;
    if status.components.is_empty() && status.adoptable.is_empty() {
        println!("No components installed.");
        return Ok(());
    }
    let mut updated = false;
    for (name, cstatus) in status.components.iter() {
        match cstatus.updatable {
            ComponentUpdatable::Upgradable => {}
            _ => continue,
        };
        match update(name, &rootcxt)? {
            ComponentUpdateResult::AtLatestVersion => {
                // Shouldn't happen unless we raced with another client
                eprintln!(
                    "warning: Expected update for {}, raced with a different client?",
                    name
                );
                continue;
            }
            ComponentUpdateResult::Updated {
                previous,
                interrupted,
                new,
            } => {
                if let Some(i) = interrupted {
                    eprintln!(
                        "warning: Continued from previous interrupted update: {}",
                        i.version,
                    );
                }
                println!("Previous {}: {}", name, previous.version);
                println!("Updated {}: {}", name, new.version);
            }
        }
        updated = true;
    }
    for (name, adoptable) in status.adoptable.iter() {
        if adoptable.confident {
            if let Some(r) = adopt_and_update(name, &rootcxt, false)? {
                println!("Adopted and updated: {}: {}", name, r.version);
                updated = true;
            }
        } else {
            println!("Component {} requires explicit adopt-and-update", name);
        }
    }
    if !updated {
        println!("No update available for any component.");
    }
    Ok(())
}

pub(crate) fn client_run_adopt_and_update(with_static_config: bool) -> Result<()> {
    let rootcxt = prep_before_update()?;
    let status: Status = status()?;
    if status.adoptable.is_empty() {
        println!("No components are adoptable.");
    } else {
        for (name, _) in status.adoptable.iter() {
            if let Some(r) = adopt_and_update(name, &rootcxt, with_static_config)? {
                println!("Adopted and updated: {}: {}", name, r.version);
            }
        }
    }
    Ok(())
}

pub(crate) fn client_run_validate() -> Result<()> {
    let status: Status = status()?;
    if status.components.is_empty() {
        println!("No components installed.");
        return Ok(());
    }
    let mut caught_validation_error = false;
    for (name, _) in status.components.iter() {
        match validate(name)? {
            ValidationResult::Valid => {
                println!("Validated: {}", name);
            }
            ValidationResult::Skip => {
                println!("Skipped: {}", name);
            }
            ValidationResult::Errors(errs) => {
                for err in errs {
                    eprintln!("{}", err);
                }
                caught_validation_error = true;
            }
        }
    }
    if caught_validation_error {
        anyhow::bail!("Caught validation errors");
    }
    Ok(())
}

#[context("Migrating to a static GRUB config")]
pub(crate) fn client_run_migrate_static_grub_config() -> Result<()> {
    // Did we already complete the migration?
    // We need to migrate if bootloader is not none (or not set)
    if let Some(bootloader) = ostreeutil::get_ostree_bootloader()? {
        if bootloader == "none" {
            println!("Already using a static GRUB config");
            return Ok(());
        }
        println!(
            "ostree repo 'sysroot.bootloader' config option is currently set to: '{}'",
            bootloader
        );
    } else {
        println!("ostree repo 'sysroot.bootloader' config option is not set yet");
    }

    // Remount /boot read write just for this unit (we are called in a slave mount namespace by systemd)
    ensure_writable_boot()?;

    let grub_config_dir = PathBuf::from("/boot/grub2");
    let dirfd = Dir::open_ambient_dir(&grub_config_dir, ambient_authority())
        .context("Opening /boot/grub2")?;

    // We mark the bootloader as BLS capable to disable the ostree-grub2 logic.
    // We can do that as we know that we are run after the bootloader has been
    // updated and all recent GRUB2 versions support reading BLS configs.
    // Ignore errors as this is not critical. This is a safety net if a user
    // manually overwrites the (soon) static GRUB config by calling `grub2-mkconfig`.
    // We need this until we can rely on ostree-grub2 being removed from the image.
    println!("Marking bootloader as BLS capable...");
    _ = File::create("/boot/grub2/.grub2-blscfg-supported");

    // Migrate /boot/grub2/grub.cfg to a static GRUB config if it is a symlink
    let grub_config_filename = PathBuf::from("/boot/grub2/grub.cfg");
    match dirfd.read_link("grub.cfg") {
        Err(_) => {
            println!(
                "'{}' is not a symlink, nothing to migrate",
                grub_config_filename.display()
            );
        }
        Ok(path) => {
            println!("Migrating to a static GRUB config...");

            // Resolve symlink location
            let mut current_config = grub_config_dir.clone();
            current_config.push(path);

            // Backup the current GRUB config which is hopefully working right now
            let backup_config = PathBuf::from("/boot/grub2/grub.cfg.backup");
            println!(
                "Creating a backup of the current GRUB config '{}' in '{}'...",
                current_config.display(),
                backup_config.display()
            );
            fs::copy(&current_config, &backup_config).context("Failed to backup GRUB config")?;

            // Read the current config, strip the ostree generated GRUB entries and
            // write the result to a temporary file
            println!("Stripping ostree generated entries from GRUB config...");
            let stripped_config = "grub.cfg.stripped";
            let current_config_file =
                File::open(current_config).context("Could not open current GRUB config")?;
            let content = BufReader::new(current_config_file);

            strip_grub_config_file(content, &dirfd, stripped_config)?;

            // Atomically replace the symlink
            dirfd
                .rename(stripped_config, &dirfd, "grub.cfg")
                .context("Failed to replace symlink with current GRUB config")?;

            fsfreeze_thaw_cycle(dirfd.reopen_as_ownedfd()?)?;

            println!("GRUB config symlink successfully replaced with the current config");
        }
    };

    println!("Setting 'sysroot.bootloader' to 'none' in ostree repo config...");
    ostreeutil::set_ostree_bootloader("none")?;

    println!("Static GRUB config migration completed successfully");
    Ok(())
}

/// Writes a stripped GRUB config to `stripped_config_name`, removing lines between
/// `### BEGIN /etc/grub.d/15_ostree ###` and `### END /etc/grub.d/15_ostree ###`.
fn strip_grub_config_file(
    current_config_content: impl BufRead,
    dirfd: &Dir,
    stripped_config_name: &str,
) -> Result<()> {
    let mut writer = vec![];

    let mut skip = false;
    for line in current_config_content.lines() {
        let line = line.context("Failed to read line from GRUB config")?;
        if line == "### END /etc/grub.d/15_ostree ###" {
            skip = false;
            continue;
        }
        if skip {
            continue;
        }
        if line == "### BEGIN /etc/grub.d/15_ostree ###" {
            skip = true;
            continue;
        }
        writer
            .write_all(line.as_bytes())
            .context("Failed to write stripped GRUB config")?;
        writer
            .write_all(b"\n")
            .context("Failed to write stripped GRUB config")?;
    }

    // mode = -rw------- (600)
    dirfd
        .atomic_write_with_perms(stripped_config_name, writer, Permissions::from_mode(0o600))
        .with_context(|| format!("Writing to {stripped_config_name}"))?;

    Ok(())
}

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

    #[test]
    fn test_failpoint_update() {
        let guard = fail::FailScenario::setup();
        fail::cfg("update", "return").unwrap();
        let r = client_run_update();
        assert_eq!(r.is_err(), true);
        guard.teardown();
    }

    #[test]
    fn test_strip_grub_config_file() -> Result<()> {
        let root: &tempfile::TempDir = &tempfile::tempdir()?;
        let root_path = root.path();
        let rootd = Dir::open_ambient_dir(root_path, ambient_authority())?;
        let stripped_config = "stripped";
        let content = r"
### BEGIN /etc/grub.d/10_linux ###

### END /etc/grub.d/10_linux ###

### BEGIN /etc/grub.d/15_ostree ###
menuentry 'Red Hat Enterprise Linux CoreOS 4 (ostree)' --class gnu-linux --class gnu --class os --unrestricted 'ostree-0-a92522f9-74dc-456a-ae0c-05ba22bca976' {
load_video
set gfxpayload=keep
insmod gzio
insmod part_gpt
insmod ext2
if [ x$feature_platform_search_hint = xy ]; then
  search --no-floppy --fs-uuid --set=root  a92522f9-74dc-456a-ae0c-05ba22bca976
else
  search --no-floppy --fs-uuid --set=root a92522f9-74dc-456a-ae0c-05ba22bca976
fi
linuxefi /ostree/rhcos-bf3b382/vmlinuz console=tty0 console=ttyS0,115200n8 rootflags=defaults,prjquota rw $ignition_firstboot root=UUID=cbac8cdc
initrdefi /ostree/rhcos-bf3b382/initramfs.img
}
### END /etc/grub.d/15_ostree ###

### BEGIN /etc/grub.d/20_linux_xen ###
### END /etc/grub.d/20_linux_xen ###";

        strip_grub_config_file(
            BufReader::new(std::io::Cursor::new(content)),
            &rootd,
            stripped_config,
        )?;
        let stripped_content = rootd.read_to_string(stripped_config)?;
        let expected = r"
### BEGIN /etc/grub.d/10_linux ###

### END /etc/grub.d/10_linux ###


### BEGIN /etc/grub.d/20_linux_xen ###
### END /etc/grub.d/20_linux_xen ###
";
        assert_eq!(expected, stripped_content);
        Ok(())
    }
}
