//! # Path layout (`dirs`) at startup
//!
//! How `EPKGDirs` is filled before the rest of epkg runs:
//!
//! 1. **Load YAML** — `load_config_from_matches` deserializes `options.yaml` (or `--config`) into
//!    `EPKGConfig`. The `dirs` field may be empty or only partially set.
//! 2. **Common options** — `parse_options_common` applies global CLI flags, arch, and
//!    `init.shared_store` (except `self install`, which sets store mode from its own flags).
//! 3. **Subcommand** — `parse_options_subcommand` runs the subcommand parser, then
//!    `determine_environment_final` and `validate_env_name`.
//! 4. **Finalize dirs** — [`init_config_dirs`] runs `EPKGDirs::build_dirs` (OS + `shared_store`),
//!    merges into `config.dirs`, then moves that merged value into a process-global [`OnceLock`]
//!    (YAML overrides preserved). The `dirs` field on the stored [`crate::models::EPKGConfig`] is
//!    left empty after the move.
//! 5. **Readers** — [`crate::models::dirs`] and [`dirs_ref`] return `&'static EPKGDirs` pointing at
//!    that single `OnceLock` allocation (no per-call clone).

use std::env;
use std::sync::OnceLock;
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

use crate::models::*;
use crate::repo::RepoRevise;
#[cfg(unix)]
use crate::utils;
#[cfg(unix)]
use crate::userdb;
use color_eyre::eyre::{self};
use color_eyre::Result;

/// Holds the merged [`EPKGDirs`] after [`init_config_dirs`] (exactly one `set`; read-only thereafter).
static DIRS: OnceLock<EPKGDirs> = OnceLock::new();

/// Read-only path layout for this process (same backing store as [`crate::models::dirs`]).
///
/// Prefer [`crate::models::dirs`] at call sites so [`crate::models::config`] is initialized first;
/// use `dirs_ref` only when you know parsing has already run.
#[inline]
pub fn dirs_ref() -> &'static EPKGDirs {
    DIRS.get()
        .expect("dirs not initialized (init_config_dirs must run before dirs_ref)")
}

/// Compute `user_envs` path without relying on `dirs_ref()`.
/// This can be called during config initialization.
///
/// - If shared_store: /opt/epkg/envs/$USER
/// - If !shared_store: $HOME/.epkg/envs
pub fn compute_user_envs(shared_store: bool) -> Result<PathBuf> {
    if shared_store {
        let username = get_username()?;
        Ok(PathBuf::from("/opt/epkg/envs").join(&username))
    } else {
        let home = get_home()?;
        Ok(PathBuf::from(&home).join(".epkg").join("envs"))
    }
}

/// Join `parts` onto `base` with one `Path::join` per component (avoids `.join("a/b")` mixing
/// separators on Windows.)
#[inline]
pub fn path_join<P: AsRef<Path> + ?Sized>(base: &P, parts: &[&str]) -> PathBuf {
    let mut p = base.as_ref().to_path_buf();
    for s in parts {
        p = p.join(s);
    }
    p
}

/// Basename of the epkg binary under each environment's `usr/bin/` (matches `init` copy target).
#[cfg(windows)]
pub const EPKG_USR_BIN_NAME: &str = "epkg.exe";
#[cfg(not(windows))]
pub const EPKG_USR_BIN_NAME: &str = "epkg";

#[cfg(unix)]
impl EPKGDirs {
    /// Computes the full default path layout for `options` (shared vs private store, home, username).
    ///
    /// **Steps:**
    ///
    /// 1. Called from `init_config_dirs` before `merge_from`.
    /// 2. [`init_config_dirs`] merges this into `config.dirs`, then moves the result into the
    ///    global `OnceLock` (see that function).
    pub fn build_dirs(options: &EPKGConfig) -> Result<Self> {
        let opt_epkg = PathBuf::from("/opt/epkg");
        let home = get_home()?;
        let home_epkg = PathBuf::from(&home).join(".epkg");

        // EPKG_CACHE: explicit cache directory for VM guest path consistency
        // When set, guest uses the same cache path as host virtiofs mount.
        #[cfg(unix)]
        let home_cache = if let Ok(cache) = env::var("EPKG_CACHE") {
            if !cache.is_empty() {
                log::debug!("build_dirs: using EPKG_CACHE='{}'", cache);
                PathBuf::from(cache)
            } else {
                // macOS uses ~/Library/Caches/epkg, Linux uses ~/.cache/epkg
                #[cfg(target_os = "macos")]
                { path_join(&PathBuf::from(&home), &["Library", "Caches", "epkg"]) }
                #[cfg(not(target_os = "macos"))]
                { path_join(&PathBuf::from(&home), &[".cache", "epkg"]) }
            }
        } else {
            // macOS uses ~/Library/Caches/epkg, Linux uses ~/.cache/epkg
            #[cfg(target_os = "macos")]
            { path_join(&PathBuf::from(&home), &["Library", "Caches", "epkg"]) }
            #[cfg(not(target_os = "macos"))]
            { path_join(&PathBuf::from(&home), &[".cache", "epkg"]) }
        };

        let (epkg_store, epkg_cache) = if options.init.shared_store {
            // Shared store/cache live under /opt/epkg
            (opt_epkg.join("store"), opt_epkg.join("cache"))
        } else {
            // Non-shared store uses user home, cache uses home_cache
            (home_epkg.join("store"), home_cache.clone())
        };

        // Get username - if it fails, the error will propagate upward
        let username = get_username()?;

        let user_envs = if options.init.shared_store {
            // /opt/epkg/envs/$USER
            opt_epkg.join("envs").join(&username)
        } else {
            // $HOME/.epkg/envs
            home_epkg.join("envs")
        };

        let user_aur_builds = if options.init.shared_store {
            // /opt/epkg/cache/aur_builds/$USER
            epkg_cache.join("aur_builds").join(&username)
        } else {
            // $HOME/.cache/epkg/aur_builds
            home_cache.join("aur_builds")
        };

        // Runtime dir for VM sockets and other ephemeral files
        // MUST use ~/.epkg/run (under home_epkg) instead of XDG_RUNTIME_DIR.
        // Reason: home_epkg is bind-mounted in VM namespace, ensuring cross-namespace visibility.
        // - Child creates session file at env_root/home/wfg/.epkg/run/vm-sessions/
        // - Parent sees it at ~/.epkg/run/vm-sessions/ via bind mount
        // - If using XDG_RUNTIME_DIR (/run/user/1000/epkg), files are invisible to parent
        let epkg_run = PathBuf::from(&home).join(".epkg").join("run");

        Ok(Self {
            opt_epkg,
            home_epkg,
            home_cache,
            user_envs,
            user_aur_builds,
            epkg_downloads_cache: epkg_cache.join("downloads"),
            epkg_channels_cache: epkg_cache.join("channels"),
            epkg_store,
            epkg_cache,
            epkg_run,
        })
    }
}

/// Windows shared-install root: `D:\epkg` when drive `D:` exists, otherwise `C:\epkg`.
///
/// This only applies when you use a shared store install on Windows; the D: drive check avoids
/// putting a large store on a small system C: when a second disk is available.
#[cfg(windows)]
fn windows_global_epkg_root() -> PathBuf {
    let d = Path::new("D:\\");
    if d.exists() {
        PathBuf::from(r"D:\epkg")
    } else {
        PathBuf::from(r"C:\epkg")
    }
}

#[cfg(windows)]
impl EPKGDirs {
    /// Computes the full default path layout for `options` (shared vs private store, `%USERPROFILE%`, etc.).
    ///
    /// **Steps:**
    ///
    /// 1. Called from `init_config_dirs` before `merge_from`.
    /// 2. [`init_config_dirs`] merges this into `config.dirs`, then moves the result into the
    ///    global `OnceLock` (see that function).
    pub fn build_dirs(options: &EPKGConfig) -> Result<Self> {
        let opt_epkg = windows_global_epkg_root();

        // Per-user (private) layout: %USERPROFILE%\.epkg\ — store, envs, and cache live under
        // .epkg (no separate %USERPROFILE%\.cache\epkg).
        // Use get_home() which handles MSYS2/Cygwin HOME paths correctly.
        let user_profile = PathBuf::from(get_home()?);

        let home_epkg = user_profile.join(".epkg");
        let home_cache = home_epkg.join("cache");

        let (epkg_store, epkg_cache) = if options.init.shared_store {
            // Shared store/cache under C:\epkg\
            (opt_epkg.join("store"), opt_epkg.join("cache"))
        } else {
            // User-private: under %USERPROFILE%\.epkg\
            (home_epkg.join("store"), home_cache.clone())
        };

        // Get username
        let username = env::var("USERNAME")
            .or_else(|_| env::var("USER"))
            .unwrap_or_else(|_| "user".to_string());

        let user_envs = if options.init.shared_store {
            // C:\epkg\envs\<username>
            opt_epkg.join("envs").join(&username)
        } else {
            // %USERPROFILE%\.epkg\envs
            home_epkg.join("envs")
        };

        // AUR builds not applicable on Windows, but keep for struct completeness
        let user_aur_builds = epkg_cache.join("aur_builds").join(&username);

        // Runtime dir for VM sockets and named pipes on Windows
        let epkg_run = home_epkg.join("run");

        Ok(Self {
            opt_epkg,
            home_epkg,
            home_cache,
            user_envs,
            user_aur_builds,
            epkg_downloads_cache: epkg_cache.join("downloads"),
            epkg_channels_cache: epkg_cache.join("channels"),
            epkg_store,
            epkg_cache,
            epkg_run,
        })
    }
}

/// Finishes your `dirs` configuration after CLI and `options.yaml` are loaded.
///
/// You may have set only some paths under `dirs:`; this fills every remaining empty field
/// using the same rules as a fresh install ([`EPKGDirs::build_dirs`]), then installs the merged
/// snapshot as the process-wide read-only path roots (see module docs).
///
/// **Steps:**
///
/// 1. `computed = EPKGDirs::build_dirs(config)?` — full default layout for current `EPKGConfig`.
/// 2. `config.dirs.merge_from(&computed)` — YAML paths win; empty slots filled from `computed`.
/// 3. `mem::take(&mut config.dirs)` into the global `OnceLock` — `config.dirs` becomes default-empty.
pub fn init_config_dirs(config: &mut EPKGConfig) -> Result<()> {
    if DIRS.get().is_some() {
        return Err(eyre::eyre!("init_config_dirs: already initialized"));
    }
    let computed = EPKGDirs::build_dirs(config)?;
    config.dirs.merge_from(&computed);
    let merged = std::mem::take(&mut config.dirs);
    DIRS.set(merged)
        .map_err(|_| eyre::eyre!("init_config_dirs: DIRS.set failed (duplicate init)"))?;
    Ok(())
}

impl EPKGDirs {
    /// Keeps paths you already set in `options.yaml` and copies defaults from `computed` only into empty slots.
    ///
    /// So your explicit overrides win; anything you left out gets the usual defaults for your OS
    /// and shared/private store mode.
    ///
    /// **Steps:**
    ///
    /// 1. For each path field, if `self` is empty, copy from the same field in `computed` (built by `build_dirs`).
    /// 2. Non-empty `self` values (from YAML) are left unchanged.
    ///
    /// [`init_config_dirs`] then moves `self` out into the global `OnceLock`; callers read paths via
    /// [`crate::models::dirs`] / [`dirs_ref`], not via the `dirs` field on [`EPKGConfig`] after init.
    pub fn merge_from(&mut self, computed: &EPKGDirs) {
        let m = |p: &mut PathBuf, q: &PathBuf| {
            if p.as_os_str().is_empty() {
                *p = q.clone();
            }
        };
        m(&mut self.opt_epkg, &computed.opt_epkg);
        m(&mut self.home_epkg, &computed.home_epkg);
        m(&mut self.home_cache, &computed.home_cache);
        m(&mut self.user_envs, &computed.user_envs);
        m(&mut self.user_aur_builds, &computed.user_aur_builds);
        m(&mut self.epkg_store, &computed.epkg_store);
        m(&mut self.epkg_cache, &computed.epkg_cache);
        m(&mut self.epkg_downloads_cache, &computed.epkg_downloads_cache);
        m(&mut self.epkg_channels_cache, &computed.epkg_channels_cache);
        m(&mut self.epkg_run, &computed.epkg_run);
    }
}

/// Get the base path to unpack package temporarily
pub fn unpack_basedir() -> PathBuf {
    dirs_ref().epkg_store.join("unpack")
}

pub fn get_env_root(env_name: String) -> Result<PathBuf> {
    let current_env = config().common.env_name.clone();
    // Only use the cached env config if we're asking for the current environment
    // AND env_root is already set (e.g., from --root flag or main.rs initialization)
    if !current_env.is_empty() && current_env == env_name {
        let current_env_root = config().common.env_root.clone();
        if !current_env_root.is_empty() {
            Ok(PathBuf::from(&current_env_root))
        } else {
            // env_name matches but env_root not set - load from env_name
            // This handles -e flag where only env_name was set
            let env_config = crate::io::deserialize_env_config_for(env_name)?;
            Ok(PathBuf::from(&env_config.env_root))
        }
    } else {
        // Load from config file
        // With path-consistency mount, stored paths are valid in both host and guest
        let env_config = crate::io::deserialize_env_config_for(env_name)?;
        Ok(PathBuf::from(&env_config.env_root))
    }
}

pub fn get_default_env_root() -> Result<PathBuf> {
    get_env_root(config().common.env_name.clone())
}

pub fn get_generations_root(env_name: &str) -> Result<PathBuf> {
    let env_root = get_env_root(env_name.to_string())?;
    Ok(env_root.join("generations"))
}

pub fn get_default_generations_root() -> Result<PathBuf> {
    get_generations_root(&config().common.env_name)
}

/// Decode an auto-generated environment name back to its filesystem path.
/// Auto-generated names start with '__' and encode the path by replacing '/' with '__'.
/// For example: `__root__.epkg__tmp__myenv` -> `/root/.epkg/tmp/myenv`
/// Returns None if the name is not an auto-generated path-encoded name.
fn env_path_from_name(env_name: &str) -> Option<PathBuf> {
    // Auto-generated names must start with '__'
    if !env_name.starts_with("__") {
        return None;
    }

    // Check if this looks like a path-encoded name (contains '__' after the initial '__')
    // Names like "__main" are special, not path-encoded
    let after_prefix = &env_name[2..];
    if !after_prefix.contains("__") {
        return None;
    }

    // Decode: replace '__' with '/' and add leading '/'
    let decoded = format!("/{}", after_prefix.replace("__", "/"));
    Some(PathBuf::from(decoded))
}

/// Get the base path for an environment
/// Location is determined by InitOptions.shared_store:
///   - shared_store=false: $HOME/.epkg/envs/$env_name
///   - shared_store=true:
///     - self:   /opt/epkg/envs/root/self (special env for package manager files only)
///     - others: /opt/epkg/envs/$USER/$env_name
/// Supports both 'env_name' and 'owner/env_name' formats
/// Special handling for path-encoded names like '__root__.epkg__tmp__myenv'
/// Note: EnvConfig.public only controls visibility/permissions, not location
pub fn get_env_base_path(env_name: &str) -> PathBuf {
    if env_name.is_empty() {
        panic!("env_name is empty in get_env_base_path");
    }

    // Check if this is a path-encoded name (auto-generated from --root)
    if let Some(path) = env_path_from_name(env_name) {
        return path;
    }

    // Visit other's /opt/epkg/envs/$owner/$name (public envs)
    if let Some(slash_pos) = env_name.find('/') {
        if !matches!(config().subcommand,
              EpkgCommand::Run
            | EpkgCommand::Info
            | EpkgCommand::List
            | EpkgCommand::EnvList
            | EpkgCommand::Search
        ) {
            use std::process::exit;
            eprintln!("Can only read-only visit others public env via `epkg run|info|search|list|env list`");
            exit(1);
        }
        let owner = &env_name[..slash_pos];
        let name = &env_name[slash_pos + 1..];
        return public_envs_path().join(owner).join(name);
    }

    // No slash: own environment
    if config().init.shared_store {
        if env_name == SELF_ENV {
            return public_envs_path().join("root").join(SELF_ENV);
        }
    }

    // Visit my own env
    dirs_ref().user_envs.join(env_name)
}

/// Get the relative path to epkg environment config (etc/epkg/env.yaml)
fn env_config_relative_path() -> PathBuf {
    path_join(Path::new(""), &["etc", "epkg", "env.yaml"])
}

/// `$env_root/etc/epkg` built with per-component joins (consistent separators on Windows).
#[inline]
pub fn env_root_etc_epkg(env_root: &Path) -> PathBuf {
    path_join(env_root, &["etc", "epkg"])
}

#[inline]
pub fn env_root_channel_yaml(env_root: &Path) -> PathBuf {
    env_root_etc_epkg(env_root).join("channel.yaml")
}

#[inline]
pub fn env_root_repos_d(env_root: &Path) -> PathBuf {
    env_root_etc_epkg(env_root).join("repos.d")
}

#[inline]
pub fn env_root_env_yaml(env_root: &Path) -> PathBuf {
    env_root_etc_epkg(env_root).join("env.yaml")
}

/// Get the path to an environment's configuration file
pub fn get_env_config_path(env_name: &str) -> PathBuf {
    let cfg = crate::config();
    // When env_root is set (e.g. create with --root, install --root), config lives at env_root/etc/epkg/env.yaml.
    // Check before in_env_root so that create with --root writes to the target path, not /etc.
    if !cfg.common.env_root.is_empty() && env_name == cfg.common.env_name {
        return env_root_env_yaml(Path::new(&cfg.common.env_root));
    }
    // If we're running inside an environment root (chroot/bind mount), the config is at /etc/epkg/env.yaml
    if cfg.common.in_env_root && env_name == cfg.common.env_name {
        return env_root_env_yaml(Path::new("/"));
    }
    get_env_base_path(env_name).join(env_config_relative_path())
}

pub fn find_env_base(env_name: &str) -> Option<PathBuf> {
    // Find environment based on current shared_store setting
    let base = get_env_base_path(env_name);
    if base.join(env_config_relative_path()).exists() {
        return Some(base);
    }

    None
}

/// Find the root directory for an environment by searching both private and public locations
/// Returns None if the environment is not found
pub fn find_env_root(env_name: &str) -> Option<PathBuf> {
    if let Some(env_base) = find_env_base(env_name) {
        // Canonicalize the base to resolve symlinks and get the real path
        return std::fs::canonicalize(env_base).ok();
    }
    None
}

/// Find the first existing dir:
/// - $HOME/.epkg/envs/self/usr/src/epkg
/// - /opt/epkg/envs/root/self/usr/src/epkg
/// Get the epkg source path for accessing assets (repos, mirrors, etc.)
///
/// This function is safe to call during config parsing because it does not
/// call `config()` which could cause a deadlock during initialization.
/// It uses `determine_shared_store()` and `compute_user_envs()` directly.
pub fn get_epkg_src_path() -> PathBuf {
    // Use determine_shared_store() to check store mode without calling config()
    let shared_store = crate::utils::determine_shared_store().unwrap_or(false);

    // Compute user_envs without calling get_env_base_path() which calls config()
    let user_envs = compute_user_envs(shared_store).unwrap_or_else(|_| {
        // Fallback: use home-based path
        let home = std::env::var("HOME").unwrap_or_default();
        PathBuf::from(home).join(".epkg").join("envs")
    });

    let user_path = path_join(&user_envs, &[SELF_ENV, "usr", "src", "epkg"]);
    if user_path.exists() {
        log::debug!("Using user's epkg source path: {:?}", user_path);
        return user_path;
    }

    // Compute public_envs path directly without calling public_envs_path() which uses dirs_ref()
    let public_envs = if shared_store {
        #[cfg(unix)]
        { PathBuf::from("/opt/epkg/envs") }
        #[cfg(windows)]
        { windows_global_epkg_root().join("envs") }
    } else {
        #[cfg(unix)]
        { PathBuf::from("/opt/epkg/envs") }
        #[cfg(windows)]
        { windows_global_epkg_root().join("envs") }
    };

    let root_path = path_join(&public_envs, &["root", "self", "usr", "src", "epkg"]);
    if root_path.exists() {
        log::debug!("Using root's epkg source path: {:?}", root_path);
        return root_path;
    }

    log::debug!("Neither user nor root epkg source path exists, returning user path anyway: {:?}", user_path);
    user_path
}

/// Retrieves the home directory path, trying multiple methods.
/// When running as setuid, validates environment variables against real UID for security.
pub fn get_home() -> Result<String> {
    // EPKG_HOME: explicit host home directory for VM guest path consistency
    // When set, guest uses the same paths as host (mount to same path, not /root/.epkg)
    if let Ok(home) = env::var("EPKG_HOME") {
        if !home.is_empty() {
            log::debug!("get_home: using EPKG_HOME='{}'", home);
            return Ok(home);
        }
    }

    // EPKG_USER: host username for VM guest path computation (fallback when EPKG_HOME not set)
    // Build host-style home path: /home/$EPKG_USER (Linux) or /Users/$EPKG_USER (macOS)
    #[cfg(unix)]
    if let Ok(user) = env::var("EPKG_USER") {
        if !user.is_empty() {
            // Check if host is macOS by looking at EPKG_HOST_OS
            if let Ok(host_os) = env::var("EPKG_HOST_OS") {
                if host_os == "Darwin" {
                    let host_home = format!("/Users/{}", user);
                    log::debug!("get_home: using macOS host path /Users/{} from EPKG_USER", user);
                    return Ok(host_home);
                }
            }
            // Default Linux host path
            let host_home = format!("/home/{}", user);
            log::debug!("get_home: using Linux host path /home/{} from EPKG_USER", user);
            return Ok(host_home);
        }
    }

    // On Windows, prioritize USERPROFILE over HOME since HOME may be set by
    // MSYS2/Cygwin and point to their internal directories instead of the
    // actual Windows user profile.
    #[cfg(windows)]
    {
        // First try USERPROFILE (Windows native)
        if let Ok(userprofile) = env::var("USERPROFILE") {
            return Ok(userprofile);
        }

        // MSYS2/Cygwin don't pass USERPROFILE to MinGW binaries, so we need to
        // detect and convert MSYS2/Cygwin home paths to Windows user paths.
        // MSYS2 home: C:\msys64\home\username or C:\cygwin64\home\username
        // Windows home: C:\Users\username
        if let Ok(home) = env::var("HOME") {
            let home_lower = home.to_lowercase();
            // Check if this is an MSYS2 or Cygwin home path
            if home_lower.contains("msys") || home_lower.contains("cygwin") {
                // Extract username from the path (last component)
                if let Some(username) = home.rsplit('\\').next() {
                    if !username.is_empty() && username != "home" {
                        let win_home = format!("C:\\Users\\{}", username);
                        return Ok(win_home);
                    }
                }
            }
            // Not an MSYS2/Cygwin path, use HOME as-is
            return Ok(home);
        }

        // Last resort: try USERNAME to construct Windows home
        if let Ok(username) = env::var("USERNAME") {
            return Ok(format!("C:\\Users\\{}", username));
        }
    }

    // Security check: if running as setuid, get home from real UID and validate env vars
    #[cfg(unix)]
    if utils::is_suid() {
        let real_home = utils::get_home_from_uid()?;

        // Validate HOME environment variable if set
        if let Ok(env_home) = env::var("HOME") {
            if env_home != real_home {
                return Err(eyre::eyre!(
                    "Security violation: HOME environment variable ('{}') does not match real UID home ('{}'). \
                    Environment variables cannot be trusted when running as setuid.",
                    env_home, real_home
                ));
            }
        }

        // fixup bare docker HOME=/
        if real_home == "/" && utils::is_running_as_root() {
            return Ok("/root".to_string());
        }

        return Ok(real_home);
    }

    // Try HOME environment variable first (only when not setuid)
    if let Ok(home) = env::var("HOME") {
        // fixup bare docker HOME=/
        #[cfg(unix)]
        if home == "/" && utils::is_running_as_root() {
            return Ok("/root".to_string());
        }

        return Ok(home);
    }

    // bare docker may have HOME=/
    #[cfg(unix)]
    if utils::is_running_as_root() {
        return Ok("/root".to_string());
    }

    // Try using /etc/passwd directly (works in statically linked binaries)
    // getpwuid() doesn't work reliably in static builds due to NSS limitations
    #[cfg(unix)]
    {
        let uid = unsafe { libc::getuid() };
        if let Ok(home) = userdb::get_home_by_uid(uid, None) {
            return Ok(home);
        }
    }

    // Try matching path patterns to find home directory
    if let Ok(current_dir) = std::env::current_dir() {
        let path_str = current_dir.to_string_lossy();

        // Check if path starts with /home/username
        if let Some(captures) = regex::Regex::new(r"^(/home/[^/]+)").ok().and_then(|re| re.captures(&path_str)) {
            if let Some(home_match) = captures.get(1) {
                return Ok(home_match.as_str().to_string());
            }
        }

        // Check if path is in /root
        if path_str.starts_with("/root") {
            return Ok("/root".to_string());
        }
    }

    Err(eyre::eyre!("Could not determine home directory"))
}

pub fn get_repo_dir(repo: &RepoRevise) -> PathBuf {
    let repodata_name = repo.repodata_name.clone();
    #[cfg(windows)]
    let repodata_name = repodata_name.replace('/', "\\");
    dirs_ref()
        .epkg_channels_cache
        .join(&repo.channel)
        .join(repodata_name)
        .join(repo.arch.clone())
}

/// Check if a shell binary is installed and executable
#[cfg(unix)]
fn is_shell_installed(shell_name: &str) -> bool {
    // Common locations where shell binaries are typically installed
    let shell_paths = [
        format!("/bin/{}", shell_name),
        format!("/usr/bin/{}", shell_name),
        format!("/usr/local/bin/{}", shell_name),
    ];

    for path in &shell_paths {
        if let Ok(_metadata) = fs::metadata(path) {
            // Check if the file is executable
            #[cfg(unix)]
            {
                if _metadata.permissions().mode() & 0o111 != 0 {
                    return true;
                }
            }
            #[cfg(not(unix))]
            {
                // On non-Unix systems, just check if the file exists
                return true;
            }
        }
    }
    false
}

/// Common helper to collect shell RC file paths for both global and user scopes.
/// It first checks if the shell binary is installed via `is_shell_installed()`,
/// then verifies that the RC file actually exists before returning the path.
/// If `home_dir` is `None`, treats paths as global (absolute). Otherwise, treats them as relative to `home_dir`.
#[cfg(unix)]
fn collect_shell_rc_paths(
    entries: &[(&str, &str)],
    home_dir: Option<&Path>,
) -> Vec<String> {
    let mut rc_paths = Vec::new();

    for (rc_sub_path, shell_name) in entries {
        if !is_shell_installed(shell_name) {
            continue;
        }

        let rc_path = if let Some(home) = home_dir {
            home.join(rc_sub_path)
        } else {
            PathBuf::from(rc_sub_path)
        };

        if rc_path.exists() {
            rc_paths.push(rc_path.to_string_lossy().into_owned());
        }
    }

    rc_paths
}

/// Get global shell RC files (e.g., `/etc/bash.bashrc`, `/etc/zsh/zshrc`)
/// for installed shells only.
#[cfg(unix)]
pub fn get_global_shell_rc() -> Result<Vec<String>> {
    let entries = [
        ("/etc/bash.bashrc", "bash"),
        ("/etc/zsh/zshrc", "zsh"),
    ];

    Ok(collect_shell_rc_paths(&entries, None))
}

/// Get per-user shell RC files under `home_dir` for installed shells only.
#[cfg(unix)]
pub fn get_user_shell_rc(home_dir: &Path) -> Result<Vec<String>> {
    let entries = [
        (".bashrc", "bash"),
        (".zshrc", "zsh"),
        (".kshrc", "ksh"),
        (".cshrc", "csh"),
        (".tcshrc", "tcsh"),
        (".config/fish/config.fish", "fish"),
    ];

    Ok(collect_shell_rc_paths(&entries, Some(home_dir)))
}

/// Get per-user shell RC files (Windows stub - returns empty list)
#[cfg(not(unix))]
pub fn get_user_shell_rc(_home_dir: &Path) -> Result<Vec<String>> {
    // Windows doesn't use shell RC files in the same way
    Ok(Vec::new())
}

/// PowerShell profile paths for `epkg.ps1` integration.
///
/// Returns both Windows PowerShell (5.1) and PowerShell Core (7.x) paths
/// to ensure epkg works regardless of which version the user runs.
/// On non-Windows platforms we intentionally skip PowerShell profile integration.
#[cfg(windows)]
pub fn powershell_profile_paths() -> Vec<PathBuf> {
    let mut paths = Vec::new();
    if let Ok(up) = env::var("USERPROFILE") {
        let base = PathBuf::from(up).join("Documents");
        // PowerShell Core (7.x) - the modern cross-platform version
        paths.push(
            base.join("PowerShell")
                .join("Microsoft.PowerShell_profile.ps1"),
        );
        // Windows PowerShell (5.1) - the legacy built-in version
        paths.push(
            base.join("WindowsPowerShell")
                .join("Microsoft.PowerShell_profile.ps1"),
        );
    }
    paths
}

#[cfg(not(windows))]
pub fn powershell_profile_paths() -> Vec<PathBuf> {
    Vec::new()
}

/// Get username from environment variables.
/// On Unix, validates environment variables against real UID when running as setuid.
/// EPKG_USER can override USER when running in VM guest (host username passthrough).
pub fn get_username() -> Result<String> {
    // Security check: if running as setuid, get username from real UID and validate env vars
    #[cfg(unix)]
    if utils::is_suid() {
        let real_username = utils::get_username_from_uid()?;

        // Validate USER environment variable if set
        if let Ok(env_user) = env::var("USER") {
            if !env_user.is_empty() && env_user != real_username {
                return Err(eyre::eyre!(
                    "Security violation: USER environment variable ('{}') does not match real UID username ('{}'). \
                    Environment variables cannot be trusted when running as setuid.",
                    env_user, real_username
                ));
            }
        }

        return Ok(real_username);
    }

    // Try EPKG_USER first (for VM guest - host username passthrough)
    // This allows VM guest running as root to use host's username for environment path alignment
    if let Ok(username) = env::var("EPKG_USER") {
        if !username.is_empty() {
            log::debug!("get_username: using EPKG_USER='{}'", username);
            return Ok(username);
        }
    }

    // Try USER environment variable first (common on Unix/Linux) - only when not setuid
    if let Ok(username) = env::var("USER") {
        if !username.is_empty() {
            return Ok(username);
        }
    }

    // Try USERNAME environment variable (common on Windows) - only when not setuid
    if let Ok(username) = env::var("USERNAME") {
        if !username.is_empty() {
            return Ok(username);
        }
    }

    // Try to get username from HOME path
    let home = get_home()?;
    let path = PathBuf::from(home);
    if let Some(username) = path.file_name().and_then(|n| n.to_str()) {
        if !username.is_empty() {
            return Ok(username.to_string());
        }
    }

    // Fallback: try to get username from real UID (Unix only)
    #[cfg(unix)]
    {
        if let Ok(username) = utils::get_username_from_uid() {
            return Ok(username);
        }
    }

    // If all else fails, return a descriptive error
    Err(eyre::eyre!("Could not determine username. Please ensure either USER or USERNAME environment variables are set. This is required to set up the public environments directory."))
}

/// Get the base path to users' public environments directories
pub fn public_envs_path() -> PathBuf {
    dirs_ref().opt_epkg.join("envs")
}

/// Walk all directories in the given parent directory and call the callback for each.
///
/// This helper function walks the "bottom" directory level, calling the callback
/// for each subdirectory found with (env_path, owner_opt).
pub fn walk_bottom_dir<F>(parent_path: &Path, owner_opt: Option<&str>, callback: &mut F) -> Result<()>
where
    F: FnMut(&Path, Option<&str>) -> Result<()>,
{
    log::debug!("walk_bottom_dir: parent_path='{}'", parent_path.display());
    match fs::read_dir(parent_path) {
        Ok(entries) => {
            for entry in entries.flatten() {
                let env_path = entry.path();
                if env_path.is_dir() {
                    log::debug!("walk_bottom_dir: found env_path='{}'", env_path.display());
                    callback(&env_path, owner_opt)?;
                }
            }
        }
        Err(e) if e.kind() == ErrorKind::PermissionDenied => {
            log::debug!("walk_bottom_dir: skipping '{}' (permission denied)", parent_path.display());
        }
        Err(e) => {
            log::debug!("walk_bottom_dir: skipping '{}' ({})", parent_path.display(), e);
        }
    }
    Ok(())
}

/// Walk all public environments under /opt/epkg/envs/*/*
/// Calls the callback for each environment found with (env_path, Some(owner)).
fn walk_public_envs<F>(callback: &mut F) -> Result<()>
where
    F: FnMut(&Path, Option<&str>) -> Result<()>,
{
    let allusers_envs_base = PathBuf::from("/opt/epkg/envs");
    match fs::read_dir(&allusers_envs_base) {
        Ok(entries) => {
            for entry in entries.flatten() {
                let owner_path = entry.path();
                if owner_path.is_dir() {
                    let owner = owner_path.file_name()
                        .and_then(|n| n.to_str());
                    walk_bottom_dir(&owner_path, owner, callback)?;
                }
            }
        }
        Err(e) if e.kind() == ErrorKind::PermissionDenied => {
            log::debug!("walk_public_envs: skipping '{}' (permission denied)", allusers_envs_base.display());
        }
        Err(e) => {
            log::debug!("walk_public_envs: skipping '{}' ({})", allusers_envs_base.display(), e);
        }
    }
    Ok(())
}

/// Walk environments based on shared_store setting:
/// - If !shared_store: walk $HOME/.epkg/envs/* (private) and also /opt/epkg/envs/*/* (public)
/// - If  shared_store: walk /opt/epkg/envs/*/*
///
/// Calls the callback for each environment found with (env_path, owner_opt).
/// owner_opt is Some(owner) for shared_store, None for private.
///
/// **Note**: This function takes `shared_store` and `user_envs` as parameters to avoid calling
/// `config()` and `dirs_ref()` which would cause deadlock during config initialization.
pub fn walk_environments<F>(shared_store: bool, user_envs: &Path, mut callback: F) -> Result<()>
where
    F: FnMut(&Path, Option<&str>) -> Result<()>,
{
    if shared_store {
        // Walk /opt/epkg/envs/*/*
        walk_public_envs(&mut callback)?;
    } else {
        // Walk $HOME/.epkg/envs/* (private envs)
        walk_bottom_dir(user_envs, None, &mut callback)?;

        // Also walk public envs so users can see others' public environments
        walk_public_envs(&mut callback)?;
    }

    Ok(())
}

/// Search for a `.eenv` directory starting from `start_path` and moving upward.
/// Searches start_path/.eenv, then parent directories up to root or 100 levels.
/// Returns the first existing `.eenv` directory path, or None if not found.
pub fn find_nearest_dot_eenv(start_path: &Path) -> Option<PathBuf> {
    // Convert relative start path to absolute if possible
    let mut current = if start_path.is_absolute() {
        start_path.to_path_buf()
    } else {
        // Try to get absolute path by joining with current directory
        match std::env::current_dir() {
            Ok(cwd) => cwd.join(start_path),
            Err(_) => start_path.to_path_buf(), // fallback to relative
        }
    };
    log::debug!("find_nearest_dot_eenv: start_path='{}', current='{}'",
                start_path.display(), current.display());
    let mut depth = 0;
    while depth < 10 {
        let dot_eenv = current.join(".eenv");
        log::trace!("find_nearest_dot_eenv: checking {} (depth={})", dot_eenv.display(), depth);
        if dot_eenv.exists() && dot_eenv.is_dir() {
            log::debug!("find_nearest_dot_eenv: found .eenv at {}", dot_eenv.display());
            return Some(dot_eenv);
        }
        // Move to parent directory
        if !current.pop() {
            break;
        }
        depth += 1;
    }
    log::trace!("find_nearest_dot_eenv: no .eenv found");
    None
}