//! Installation steps execution module
//!
//! This module handles the execution of installation steps including pre/post scriptlets,
//! trigger processing, and hook execution for package installations and upgrades.
//!
//! Structure (3-level hierarchy inspired by RPM transaction model):
//!
//! ## Level 0: Transaction (rpmtsRun style)
//! Executes the entire transaction by processing all package operations in order.
//! Functions:
//! - `run_transaction_batch()` - Main transaction handler, processes all operations in `ordered_operations`
//! - `run_ldconfig_if_needed()` - Updates library cache after transaction completion
//!
//! ## Level 1: Package pair operation (rpmtsProcess/rpmteProcess style)
//! Processes a single package operation (install/upgrade/remove) by executing a sequence of actions.
//! Functions:
//! - `process_package_operation()` - Handles a single `PackageOperation`, calls `run_action()` for each step
//!
//! ## Level 2: Package actions entrypoint (runGoal style)
//! Executes a single action for a package operation.
//! Functions:
//! - `run_action()` - Executes one `PackageAction` (PreInstall, LinkFiles, PostInstall, etc.)
//!
//! ## Level 3a: Install/remove actions (rpmPackageInstall/rpmPackageErase style)
//! Performs actual file operations and system state changes.
//! Actions (handled within `run_action()`):
//! - `LinkFiles` - Links package files to environment (handles upgrade diff unlinking)
//! - `UnlinkFiles` - Unlinks package files from environment
//! - `ExposeExecutables` - Exposes executables (ebin exposure)
//! - `UnexposeExecutables` - Unexposes executables (ebin unexposure)
//!
//! ## Level 3b: Scriptlet/triggers actions
//! Executes scriptlets and triggers at various stages of package operations.
//! Actions (handled within `run_action()`):
//! - `PreInstall` - Runs pre-install scriptlets, RPM triggerprein, DEB activate triggers
//! - `PostInstall` - Runs post-install scriptlets, RPM triggerin/filetriggerin, DEB trigger processing
//! - `PreRemove` - Runs pre-remove scriptlets, RPM triggerun/filetriggerun (for upgrades)
//! - `PostRemove` - Runs post-remove scriptlets, RPM triggerpostun/filetriggerpostun (for upgrades)
//! - `PreUpgrade` - Runs pre-upgrade scriptlets, RPM triggerprein
//! - `PostUpgrade` - Runs post-upgrade scriptlets, RPM triggerin, DEB trigger processing
//!
//! ## Helper/Utility Functions
//! - Operation maps are cached in `InstallationPlan` (fresh_installs, upgrades_new, upgrades_old, old_removes)
//!
//! ## Obsolete Functions (kept for reference, marked with `#[allow(dead_code)]`)
//! - `process_upgrades()` - Replaced by `process_package_operation()` with integrated triggers
//! - `process_single_package_upgrade()` - Replaced by `run_action()` with integrated triggers
//! - `process_fresh_installs()` - Replaced by `process_package_operation()` with integrated triggers

#[cfg(unix)]
use std::path::Path;
use std::path::PathBuf;
#[cfg(unix)]
use std::time::SystemTime;
use color_eyre::Result;
use std::sync::Arc;
#[cfg(unix)]
use color_eyre::eyre::WrapErr;
#[cfg(unix)]
use crate::lfs;
use crate::models::{PackageFormat, InstalledPackageInfo};
use crate::models::PACKAGE_CACHE;
use crate::plan::{InstallationPlan, PackageOperation, OperationType, remove_package_from_cache};
use crate::hooks;
use crate::hooks::{run_hooks, run_pkgkey_hooks_pair, HookWhen};
#[cfg(unix)]
use crate::utils;
use crate::scriptlets::{run_scriptlet, run_trans_scriptlets, ScriptletType};
#[cfg(unix)]
use crate::run;
use crate::remove::unlink_package;
use log;

/// Package actions (L3: Package Actions)
/// Represents individual actions that compose a package operation
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PackageAction {
    /// Pre-install scriptlet (before files are linked)
    PreInstall,
    /// Link files to environment
    LinkFiles,
    /// Post-install scriptlet (after files are linked)
    PostInstall,
    /// Pre-remove scriptlet (before files are unlinked)
    PreRemove,
    /// Unlink files from environment
    UnlinkFiles,
    /// Post-remove scriptlet (after files are unlinked)
    PostRemove,
    /// Pre-upgrade scriptlet (for new package, before old is removed)
    PreUpgrade,
    /// Post-upgrade scriptlet (for new package, after old is removed)
    PostUpgrade,
    /// Expose executables (ebin exposure)
    ExposeExecutables,
    /// Unexpose executables (ebin unexposure)
    UnexposeExecutables,
}

/// Level 2: Package actions entrypoint (runGoal style)
/// Executes a single action for a package
/// For upgrades, old_pkgkey and old_pkg_info are provided
fn run_action(
    plan: &mut InstallationPlan,
    action: PackageAction,
    pkgkey: &str,
    pkg_info: &Arc<InstalledPackageInfo>,
    old_pkgkey: Option<&str>,
    old_pkg_info: Option<&Arc<InstalledPackageInfo>>,
) -> Result<()> {
    let store_root = plan.store_root.clone();
    let env_root = plan.env_root.clone();

    match action {
        PackageAction::PreInstall => {
            run_pkgkey_hooks_pair(plan, HookWhen::PreInstall, pkgkey)?;
            run_scriptlet(plan,    ScriptletType::PreInstall, pkgkey, pkg_info.as_ref(), old_pkgkey)?;
            run_pkgkey_hooks_pair(plan, HookWhen::PreInstall2, pkgkey)?;
        }

        PackageAction::LinkFiles => {
            // Files are already linked during download/unpack phase, but we may need to handle diff linking for upgrades
            let new_files_union = &plan.batch.new_files;
            crate::link::unlink_package_diff(old_pkgkey, old_pkg_info, pkg_info, &store_root, &env_root, new_files_union)?;
            // Note: Actual linking happens earlier in the download/unpack phase

            PACKAGE_CACHE.installed_packages.write().unwrap().insert(pkgkey.to_string(), Arc::clone(pkg_info));
            PACKAGE_CACHE.pkgline2installed.write().unwrap().insert(pkg_info.pkgline.clone(), Arc::clone(pkg_info));
        }

        PackageAction::PostInstall => {
            run_scriptlet(plan, ScriptletType::PostInstall, pkgkey, pkg_info.as_ref(), old_pkgkey)?;
            run_pkgkey_hooks_pair(plan, HookWhen::PostInstall, pkgkey)?;
            run_pkgkey_hooks_pair(plan, HookWhen::PostInstall2, pkgkey)?;

            // DEB trigger processing: noawait triggers from Unincorp (immediate, per-package processing)
            crate::deb_triggers::run_debian_unincorp_triggers(plan, HookWhen::PostInstall)?;
        }

        PackageAction::PreRemove => {
            run_pkgkey_hooks_pair(plan, HookWhen::PreRemove, pkgkey)?;
            run_scriptlet(plan,    ScriptletType::PreRemove, pkgkey, pkg_info.as_ref(), old_pkgkey)?;
            run_pkgkey_hooks_pair(plan, HookWhen::PreRemove2, pkgkey)?;
        }

        PackageAction::UnlinkFiles => {
            if old_pkgkey.is_none() {   // Upgrade is handled by unlink_package_diff()
                unlink_package(pkgkey, &pkg_info.pkgline, &store_root, &env_root)?;
            }
            remove_package_from_cache(pkgkey, pkg_info);
        }

        PackageAction::PostRemove => {
            run_scriptlet(plan,    ScriptletType::PostRemove, pkgkey, pkg_info.as_ref(), old_pkgkey)?;
            run_pkgkey_hooks_pair(plan, HookWhen::PostRemove, pkgkey)?;
            run_pkgkey_hooks_pair(plan, HookWhen::PostRemove2, pkgkey)?;
        }

        PackageAction::PreUpgrade => {
            run_pkgkey_hooks_pair(plan, HookWhen::PreUpgrade, pkgkey)?;
            run_scriptlet(plan,    ScriptletType::PreUpgrade, pkgkey, pkg_info.as_ref(), old_pkgkey)?;
        }

        PackageAction::PostUpgrade => {
            run_scriptlet(plan,    ScriptletType::PostUpgrade, pkgkey, pkg_info.as_ref(), old_pkgkey)?;
            run_pkgkey_hooks_pair(plan, HookWhen::PostUpgrade, pkgkey)?;
        }

        PackageAction::ExposeExecutables => {
            let store_fs_dir = plan.store_root.join(&pkg_info.pkgline).join("fs");
            crate::expose::expose_package(plan, &store_fs_dir, &pkgkey)?;
        }

        PackageAction::UnexposeExecutables => {
            crate::expose::unexpose_package(plan, &env_root, pkgkey)?;
        }
    }
    Ok(())
}

/// Execute transaction scriptlets and triggers before file operations.
/// Runs %pretrans, %preuntrans, and transfiletriggerun scriptlets/triggers.
fn begin_transaction(
    plan: &InstallationPlan,
) -> Result<()> {
    let package_format = plan.package_format;
    // Execute transaction scriptlets at transaction boundaries (RPM behavior)
    // Order: %pretrans of new, then %preuntrans of old (before any file operations)
    if package_format == PackageFormat::Rpm {
        // %pretrans of packages being installed/upgraded
        run_trans_scriptlets(plan, ScriptletType::PreTrans)?;

        // %preuntrans of packages being removed (runs after %pretrans, before removals)
        run_trans_scriptlets(plan, ScriptletType::PreUnTrans)?;
    }

    // Hook: PreTransaction
    run_hooks(plan, HookWhen::PreTransaction)?;

    Ok(())
}

/// Execute transaction scriptlets and triggers after file operations.
/// Runs %posttrans, %postuntrans, transfiletriggerpostun, and transfiletriggerin scriptlets/triggers.
fn end_transaction(
    plan: &mut InstallationPlan,
) -> Result<()> {
    let package_format = plan.package_format;

    // Execute transaction scriptlets: %posttrans of packages being installed/upgraded
    // This runs AFTER all file operations complete (RPM behavior)
    if package_format == PackageFormat::Rpm {
        // %posttrans of packages being installed/upgraded
        run_trans_scriptlets(plan, ScriptletType::PostTrans)?;

        // Execute transaction scriptlets: %postuntrans of packages being removed
        // This runs AFTER %posttrans, AFTER uninstall transaction completes (RPM behavior)
        // Order: %posttrans → %postuntrans → %transfiletriggerpostun → %transfiletriggerin
        run_trans_scriptlets(plan, ScriptletType::PostUnTrans)?;
    }

    // Hooks: PostUnTrans then PostTransaction
    run_hooks(plan, HookWhen::PostUnTrans)?;
    run_hooks(plan, HookWhen::PostTransaction)?;

    // DEB trigger processing: await triggers from Unincorp (batched, after all packages are processed)
    crate::deb_triggers::run_debian_unincorp_triggers(plan, HookWhen::PostTransaction)?;

    Ok(())
}


/// Build maps for hooks from ordered_operations
/// Stores the maps in plan.batch members
fn build_batch_maps(plan: &mut InstallationPlan) {
    plan.batch.fresh_installs.clear();
    plan.batch.upgrades_new.clear();
    plan.batch.upgrades_old.clear();
    plan.batch.old_removes.clear();

    for op in &plan.ordered_operations {
        match op.op_type {
            OperationType::FreshInstall => {
                if let Some(pkgkey) = &op.new_pkgkey {
                    if plan.batch.new_pkgkeys.contains(pkgkey) {
                        plan.batch.fresh_installs.insert(pkgkey.clone());
                    }
                }
            }
            OperationType::Upgrade => {
                if let (Some(new_pkgkey), Some(old_pkgkey)) = (&op.new_pkgkey, &op.old_pkgkey) {
                    if plan.batch.new_pkgkeys.contains(new_pkgkey) {
                        plan.batch.upgrades_new.insert(new_pkgkey.clone());
                        plan.batch.upgrades_old.insert(old_pkgkey.clone());
                    }
                }
            }
            OperationType::Removal => {
                if let Some(pkgkey) = &op.old_pkgkey {
                    plan.batch.old_removes.insert(pkgkey.clone());
                }
            }
        }
    }
}

/// Build union of all files from new packages in batch for diff calculation during upgrades
fn build_batch_file_union(plan: &mut InstallationPlan) -> Result<()> {
    plan.batch.new_files.clear();
    for pkgkey in &plan.batch.new_pkgkeys {
        if let Some(package_info) = crate::plan::pkgkey2new_pkg_info(plan, pkgkey) {
            match crate::package_cache::map_pkgline2filelist(&plan.store_root, &package_info.pkgline) {
                Ok(files) => {
                    for file in files {
                        plan.batch.new_files.insert(PathBuf::from(file));
                    }
                }
                Err(e) => {
                    log::warn!("Failed to get file list for {} (pkgline {}): {}", pkgkey, package_info.pkgline, e);
                }
            }
        }
    }
    log::debug!("Batch file union contains {} files from {} packages", plan.batch.new_files.len(), plan.batch.new_pkgkeys.len());
    Ok(())
}

/// Level 0: Transaction handler (rpmtsRun style)
/// Executes the entire transaction by processing all package operations in order
/// https://rpm-software-management.github.io/rpm/man/rpm-scriptlets.7#EXECUTION_ORDER
pub fn run_transaction_batch(
    plan: &mut InstallationPlan,
) -> Result<()> {
    // Build maps for hooks from ordered_operations
    build_batch_maps(plan);
    // Build union of all files from new packages in batch for diff calculation during upgrades
    build_batch_file_union(plan)?;

    // Setup tool wrappers for newly installed tools (after new_files is populated)
    crate::tool_wrapper::setup_tool_wrappers(plan)?;

    // Execute transaction scriptlets at transaction boundaries (RPM behavior)
    begin_transaction(&plan)?;

    // Load hooks for batch packages (incremental loading)
    hooks::load_batch_hooks(plan)?;

    // Load Debian triggers for batch packages (incremental loading)
    crate::deb_triggers::load_batch_deb_triggers(plan)?;

    // Run PreTransaction hooks
    run_hooks(plan, HookWhen::PreTransaction)?;

    // Process each package operation in order (rpmtsProcess style)
    log::debug!("run_transaction_batch: starting process_package_operations");
    process_package_operations(plan)?;
    log::debug!("run_transaction_batch: process_package_operations completed");

    // Run ldconfig if needed (after all package operations complete) - Unix only
    #[cfg(unix)]
    run_ldconfig_if_needed(&plan.env_root)?;

    // Execute transaction scriptlets and hooks: %posttrans, %postuntrans, PostUnTrans, PostTransaction
    // This runs AFTER all file operations complete (RPM behavior)
    // end_transaction() handles PostTransaction hooks - don't duplicate here
    end_transaction(plan)?;

    #[cfg(feature = "libkrun")]
    {
        crate::libkrun::shutdown_vm_reuse_session_if_active()?;
    }

    // Follow-up batches will see is_first=false
    plan.batch.is_first = false;

    log::debug!("run_transaction_batch: completed successfully");
    Ok(())
}

/// Process each package operation in order (rpmtsProcess style)
fn process_package_operations(
    plan: &mut InstallationPlan,
) -> Result<()> {
    // Clone operations to avoid borrow checker issues
    let operations: Vec<_> = plan.ordered_operations.clone();
    for op in &operations {
        // Skip operations that don't have completed packages yet (for installs/upgrades)
        if let Some(new_pkgkey) = &op.new_pkgkey {
            if !plan.batch.new_pkgkeys.contains(new_pkgkey) {
                // Package not yet completed (e.g., AUR packages being built)
                continue;
            }
        }

        // Level 1: Process package pair operation
        process_package_operation(plan, op)?;
    }
    log::debug!("process_package_operations: completed {} operations", operations.len());
    Ok(())
}

/// Level 1: Package pair operation handler (rpmtsProcess/rpmteProcess style)
/// Processes a single package operation by executing a sequence of actions
pub fn process_package_operation(
    plan: &mut InstallationPlan,
    op: &PackageOperation,
) -> Result<()> {
    let package_format = plan.package_format;
    match op.op_type {
        OperationType::FreshInstall => {
            if let Some(pkgkey) = &op.new_pkgkey {
                if plan.batch.new_pkgkeys.contains(pkgkey) {
                    if let Some(completed_info) = crate::plan::pkgkey2new_pkg_info(plan, pkgkey) {
                        // Execute actions for fresh install
                        run_action(plan, PackageAction::PreInstall,   pkgkey, &completed_info, None, None)?;
                        run_action(plan, PackageAction::LinkFiles,    pkgkey, &completed_info, None, None)?;
                        run_action(plan, PackageAction::PostInstall,  pkgkey, &completed_info, None, None)?;
                        if op.should_expose() {
                            run_action(plan, PackageAction::ExposeExecutables, pkgkey, &completed_info, None, None)?;
                        }
                    }
                }
            }
        }
        OperationType::Upgrade => {
            if let (Some(new_pkgkey), Some(old_pkgkey)) = (&op.new_pkgkey, &op.old_pkgkey) {
                if plan.batch.new_pkgkeys.contains(new_pkgkey) {
                    if let Some(new_info) = crate::plan::pkgkey2new_pkg_info(plan, new_pkgkey) {
                        if let Some(old_info) = crate::plan::pkgkey2installed_pkg_info(old_pkgkey) {
                            if op.should_unexpose() {
                                run_action(plan, PackageAction::UnexposeExecutables, old_pkgkey, &old_info, None, None)?;
                            }
                            match package_format {
                                PackageFormat::Rpm | PackageFormat::Conda => {
                                    // Order matches RPM scriptlet execution sequence https://rpm-software-management.github.io/rpm/man/rpm-scriptlets.7
                                    // Also matches conda's execution order (conda/core/link.py _execute method)
                                    // 1. %pre of _new_     | pre-link of _new_     (PreInstall)
                                    // 2. (unpack _new_ files)                      (LinkFiles)
                                    // 3. %post of _new_    | post-link of _new_    (PostInstall)
                                    //
                                    // 4. %preun of _old_   | pre-unlink of _old_   (PreRemove)
                                    // 5. (erase _old_ files)                       (UnlinkFiles)
                                    // 6. %postun of _old_  | post-unlink of _old_  (PostRemove)
                                    run_action(plan, PackageAction::PreInstall,   new_pkgkey, &new_info, Some(old_pkgkey), Some(&old_info))?;
                                    run_action(plan, PackageAction::LinkFiles,    new_pkgkey, &new_info, Some(old_pkgkey), Some(&old_info))?;
                                    run_action(plan, PackageAction::PostInstall,  new_pkgkey, &new_info, Some(old_pkgkey), Some(&old_info))?;

                                    run_action(plan, PackageAction::PreRemove,    old_pkgkey, &old_info, Some(new_pkgkey), Some(&new_info))?;
                                    run_action(plan, PackageAction::UnlinkFiles,  old_pkgkey, &old_info, Some(new_pkgkey), Some(&new_info))?;
                                    run_action(plan, PackageAction::PostRemove,   old_pkgkey, &old_info, Some(new_pkgkey), Some(&new_info))?;
                                },
                                PackageFormat::Deb => {
                                    // Order matches Debian maintainer script execution sequence https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html
                                    // Based on dpkg/src/main/unpack.c process_archive() and configure.c:
                                    // 1. prerm of _old_         (PreRemove) with "upgrade new-version"
                                    //    - Called at line 1459: maintscript_run_old_or_new(..., PRERMFILE, "upgrade", ...)
                                    //    - Args: $1="upgrade", $2=new_version (from script.c:347-348)
                                    // 2. preinst of _new_       (PreInstall) with "upgrade old-version new-version"
                                    //    - Called at line 1520: maintscript_run_new(..., PREINSTFILE, "upgrade", old_ver, new_ver, NULL)
                                    //    - Args: $1="upgrade", $2=old_version, $3=new_version
                                    // 3. (unpack _new_ files)   (LinkFiles) - extracts new files, also calls unlink_package_diff()
                                    //    - Unpacking happens at lines 1614-1650
                                    //    - unlink_package_diff() called here (equivalent to pkg_remove_old_files() at line 1686)
                                    // 4. postrm of _old_        (PostRemove) with "upgrade new-version"
                                    //    - Called at line 1663: maintscript_run_old_or_new(..., POSTRMFILE, "upgrade", ...)
                                    //    - Args: $1="upgrade", $2=new_version (only if old was HALFINSTALLED/UNPACKED, but we always call it)
                                    // 5. (unlink _old_ files)   (UnlinkFiles) - completely unlink old package from environment
                                    //    - Needed for store-based system, not in dpkg (dpkg replaces files during unpack)
                                    // 6. postinst of _new_      (PostInstall) with "configure old-version"
                                    //    - Called in configure.c:679: maintscript_postinst(..., "configure", old_version, NULL)
                                    //    - Args: $1="configure", $2=old_version
                                    run_action(plan, PackageAction::PreRemove,    old_pkgkey, &old_info, Some(new_pkgkey), Some(&new_info))?;
                                    run_action(plan, PackageAction::PreInstall,   new_pkgkey, &new_info, Some(old_pkgkey), Some(&old_info))?;
                                    run_action(plan, PackageAction::LinkFiles,    new_pkgkey, &new_info, Some(old_pkgkey), Some(&old_info))?;
                                    run_action(plan, PackageAction::PostRemove,   old_pkgkey, &old_info, Some(new_pkgkey), Some(&new_info))?;
                                    run_action(plan, PackageAction::UnlinkFiles,  old_pkgkey, &old_info, Some(new_pkgkey), Some(&new_info))?;
                                    run_action(plan, PackageAction::PostInstall,  new_pkgkey, &new_info, Some(old_pkgkey), Some(&old_info))?;
                                },
                                PackageFormat::Pacman | PackageFormat::Apk => {
                                    // Archlinux https://man.archlinux.org/man/PKGBUILD.5#INSTALL/UPGRADE/REMOVE_SCRIPTING
                                    // Alpine https://wiki.alpinelinux.org/wiki/APKBUILD_Reference
                                    run_action(plan, PackageAction::PreUpgrade,   new_pkgkey, &new_info, Some(old_pkgkey), Some(&old_info))?;
                                    run_action(plan, PackageAction::LinkFiles,    new_pkgkey, &new_info, Some(old_pkgkey), Some(&old_info))?;
                                    run_action(plan, PackageAction::UnlinkFiles,  old_pkgkey, &old_info, Some(new_pkgkey), Some(&new_info))?;
                                    run_action(plan, PackageAction::PostUpgrade,  new_pkgkey, &new_info, Some(old_pkgkey), Some(&old_info))?;
                                },
                                PackageFormat::Epkg | PackageFormat::Python | PackageFormat::Brew => { todo!() },
                            }

                            if op.should_expose() {
                                run_action(plan, PackageAction::ExposeExecutables, new_pkgkey, &new_info, None, None)?;
                            }
                        }
                    }
                }
            }
        }
        OperationType::Removal => {
            if let Some(pkgkey) = &op.old_pkgkey {
                if let Some(pkg_info) = crate::plan::pkgkey2installed_pkg_info(pkgkey) {
                    if op.should_unexpose() {
                        run_action(plan, PackageAction::UnexposeExecutables, pkgkey, &pkg_info, None, None)?;
                    }
                    // Execute actions for removal
                    run_action(plan, PackageAction::PreRemove,        pkgkey, &pkg_info, None, None)?;
                    run_action(plan, PackageAction::UnlinkFiles,      pkgkey, &pkg_info, None, None)?;
                    run_action(plan, PackageAction::PostRemove,       pkgkey, &pkg_info, None, None)?;
                    crate::deb_triggers::run_debian_unincorp_triggers(plan, HookWhen::PostInstall)?;
                }
            }
        }
    }
    Ok(())
}

/// Run ldconfig if the library cache needs updating
/// Called after all package operations complete
#[cfg(unix)]
fn run_ldconfig_if_needed(env_root: &Path) -> Result<()> {
    let ld_so_cache = crate::dirs::path_join(env_root, &["etc", "ld.so.cache"]);
    let lib_dirs = [
        crate::dirs::path_join(env_root, &["etc", "ld.so.conf.d"]),
        env_root.join("lib"),
        env_root.join("lib64"),
        crate::dirs::path_join(env_root, &["usr", "lib"]),
        crate::dirs::path_join(env_root, &["usr", "lib64"]),
    ];

    // Get mtime of ld.so.cache if it exists
    let cache_mtime = if lfs::exists_or_any_symlink(&ld_so_cache) {
        lfs::symlink_metadata(&ld_so_cache)
            .with_context(|| format!("Failed to get metadata for {}", ld_so_cache.display()))?
            .modified()
            .with_context(|| format!("Failed to get modification time for {}", ld_so_cache.display()))?
    } else {
        // If cache doesn't exist, we need to run ldconfig
        SystemTime::UNIX_EPOCH
    };

    // Check if any lib directory has been modified more recently than the cache
    let needs_update = lib_dirs.iter().any(|dir| {
        if !lfs::exists_or_any_symlink(dir) {
            return false;
        }
        match lfs::symlink_metadata(dir) {
            Ok(metadata) => {
                match metadata.modified() {
                    Ok(dir_mtime) => dir_mtime > cache_mtime,
                    Err(_) => true, // If we can't get mtime, assume update needed
                }
            }
            Err(_) => false, // If we can't get metadata, skip this directory
        }
    });

    if needs_update {
        log::info!("Library cache needs updating, running ldconfig");

        // Check if ldconfig exists in the environment before trying to run it
        match run::find_command_in_env_path("ldconfig", env_root) {
            Ok(ldconfig_path) => {
                // Inherit VM settings from active VM reuse session during install/upgrade.
                // This allows ldconfig to reuse the same VM that was created for the main command.
                // The VM reuse logic is handled in fork_and_execute() -> prepare_run_options_for_command().
                let mut run_options = run::RunOptions {
                    command: ldconfig_path.to_string_lossy().to_string(),
                    no_exit: true,
                    chdir_to_env_root: true, // ldconfig should run relative to environment root
                    ..Default::default()
                };

                // Nested under `epkg run --isolate=vm` (e.g. e2e): libc::clone with namespace flags
                // can return EPERM; ldconfig only needs the env tree, not an extra namespace.
                if utils::e2e_backend_is_vm() {
                    run_options.skip_namespace_isolation = true;
                }

                // Execute ldconfig
                run::fork_and_execute(env_root, &run_options)?;
            }
            Err(_) => {
                log::info!("ldconfig command not found in environment, skipping library cache update");
            }
        }
    } else {
        log::debug!("Library cache is up to date, skipping ldconfig");
    }

    Ok(())
}