//! Package management utilities for different operating systems.
//!
//! This module provides a unified interface for package management across different platforms
//! (Ubuntu/Debian with APT, macOS with Homebrew). It includes platform detection, package
//! installation/removal, updates, and package search functionality.
//!
//! # Example
//!
//! ```no_run
//! use herolib_os::os::{PackHero, Platform};
//!
//! let mut pkg = PackHero::new();
//! if pkg.platform() == Platform::MacOS {
//!     pkg.install("curl")?;
//! }
//! # Ok::<(), herolib_os::os::PackageError>(())
//! ```

use regex::Regex;
use std::process::Command;

/// Parse a semantic version string (e.g., "1.25.5") into comparable integer
///
/// Converts "1.25.5" to (1 << 16) | (25 << 8) | 5 = 0x011905
/// This allows simple numeric comparison: v1 >= v2
///
/// # Arguments
///
/// * `version_str` - Version string like "1.25.5", "1.25", or "1"
///
/// # Returns
///
/// A u32 that can be compared directly
///
/// # Example
///
/// ```ignore
/// let v1 = parse_version("1.25.5");
/// let v2 = parse_version("1.24.0");
/// assert!(v1 > v2);
/// ```
pub fn parse_version(version_str: &str) -> u32 {
    let parts: Vec<&str> = version_str.split('.').collect();

    let major = parts
        .get(0)
        .and_then(|s| s.parse::<u32>().ok())
        .unwrap_or(0);

    let minor = parts
        .get(1)
        .and_then(|s| s.parse::<u32>().ok())
        .unwrap_or(0);

    let patch = parts
        .get(2)
        .and_then(|s| s.parse::<u32>().ok())
        .unwrap_or(0);

    // Pack into u32: major (8 bits) | minor (8 bits) | patch (8 bits)
    (major << 16) | (minor << 8) | patch
}

/// Builder for checking binary/command version requirements
///
/// Provides a fluent interface to check if a binary is installed and meets a minimum version.
/// Uses regex to extract version from command output, with optional filtering.
///
/// # Example
///
/// ```no_run
/// use herolib_os::os::BinaryVersionChecker;
///
/// // Simple check with auto-detected version extraction
/// BinaryVersionChecker::new("go version")
///     .min_version("1.25.5")
///     .check()?;
///
/// // With optional output line filter
/// BinaryVersionChecker::new("rustc --version")
///     .min_version("1.92.0")
///     .output_line_filter(r"rustc")  // Optional: filters to lines containing "rustc"
///     .check()?;
/// # Ok::<(), herolib_os::os::PackageError>(())
/// ```
#[derive(Clone)]
pub struct BinaryVersionChecker {
    command: String,
    min_version: String,
    output_line_filter: Option<String>,
}

impl BinaryVersionChecker {
    /// Create a new binary version checker
    ///
    /// # Arguments
    ///
    /// * `command` - Full command to execute (e.g., "go version", "rustc --version")
    pub fn new(command: &str) -> Self {
        Self {
            command: command.to_string(),
            min_version: String::new(),
            output_line_filter: None,
        }
    }

    /// Set the minimum required version (required)
    ///
    /// # Arguments
    ///
    /// * `min_version` - Minimum version string (e.g., "1.25.5")
    pub fn min_version(mut self, min_version: &str) -> Self {
        self.min_version = min_version.to_string();
        self
    }

    /// Set an optional regex filter for output lines
    ///
    /// Only lines matching this regex will be searched for version numbers.
    /// If not set, the entire output is searched.
    ///
    /// # Arguments
    ///
    /// * `filter` - Regex pattern to filter output lines (e.g., "rustc", "go version")
    pub fn output_line_filter(mut self, filter: &str) -> Self {
        self.output_line_filter = Some(filter.to_string());
        self
    }

    /// Check if the binary is installed and meets the minimum version requirement
    ///
    /// # Returns
    ///
    /// * `Ok(true)` - Binary found and version >= min_version
    /// * `Ok(false)` - Binary found but version < min_version
    /// * `Err(PackageError)` - Binary not found or version parsing failed
    pub fn check(&self) -> Result<bool, PackageError> {
        if self.min_version.is_empty() {
            return Err(PackageError::CommandFailed(
                "min_version is required".to_string(),
            ));
        }

        let output = Command::new("sh")
            .arg("-c")
            .arg(&self.command)
            .output()
            .map_err(PackageError::CommandExecutionFailed)?;

        if !output.status.success() {
            return Err(PackageError::CommandFailed(format!(
                "Command '{}' failed",
                self.command
            )));
        }

        let stdout = String::from_utf8_lossy(&output.stdout);

        // Filter output if line filter is set
        let output_to_search = if let Some(filter) = &self.output_line_filter {
            stdout
                .lines()
                .filter(|line| {
                    if let Ok(re) = Regex::new(filter) {
                        re.is_match(line)
                    } else {
                        line.contains(filter)
                    }
                })
                .collect::<Vec<_>>()
                .join("\n")
        } else {
            stdout.to_string()
        };

        // Extract version number
        let version_str = extract_version_from_output(&output_to_search).ok_or_else(|| {
            PackageError::CommandFailed(format!(
                "Could not extract version from: {}",
                output_to_search
            ))
        })?;

        let current_version = parse_version(&version_str);
        let required_version = parse_version(&self.min_version);

        Ok(current_version >= required_version)
    }
}

/// Legacy convenience function for backward compatibility
///
/// Prefer using BinaryVersionChecker builder instead.
pub fn check_binary_version(
    command: &str,
    version_flag: &str,
    min_version: &str,
    _output_contains: &str,
) -> Result<bool, PackageError> {
    let full_command = format!("{} {}", command, version_flag);
    BinaryVersionChecker::new(&full_command)
        .min_version(min_version)
        .check()
}

/// Extract semantic version from output string using regex
///
/// Matches patterns like:
/// - 1.25.5
/// - v1.25.5
/// - go1.25.5
/// - go version go1.25.5 darwin/arm64
/// - rustc 1.92.0
/// - node v18.0.0
fn extract_version_from_output(output: &str) -> Option<String> {
    // Regex pattern matches semantic versions: X.Y or X.Y.Z or X.Y.Z.W
    // Handles optional 'v' or word prefix like 'go', 'rustc', 'node', etc.
    // Pattern: optional prefix + digits.digits(.digits)?(.digits)?
    if let Ok(re) = Regex::new(r"(?:^|[^\w])([a-z]*[v]?)(\d+\.\d+(?:\.\d+)?(?:\.\d+)?)") {
        if let Some(caps) = re.captures(output) {
            if let Some(version) = caps.get(2) {
                return Some(version.as_str().to_string());
            }
        }
    }

    // Fallback: simple pattern search for digit.digit
    if let Ok(re) = Regex::new(r"(\d+\.\d+(?:\.\d+)?(?:\.\d+)?)") {
        if let Some(caps) = re.captures(output) {
            if let Some(version) = caps.get(1) {
                return Some(version.as_str().to_string());
            }
        }
    }

    None
}

/// A structure to hold command execution results.
///
/// Contains the captured stdout, stderr, exit code, and success status of a command.
#[derive(Debug, Clone)]
pub struct CommandResult {
    /// Standard output from the command
    pub stdout: String,
    /// Standard error output from the command
    pub stderr: String,
    /// Whether the command completed successfully (exit code 0)
    pub success: bool,
    /// Exit code returned by the command
    pub code: i32,
}

/// Error type for package management operations
#[derive(Debug)]
pub enum PackageError {
    /// Command failed with error message
    CommandFailed(String),
    /// Command execution failed with IO error
    CommandExecutionFailed(std::io::Error),
    /// Unsupported platform
    UnsupportedPlatform(String),
    /// Other error
    Other(String),
}

impl std::fmt::Display for PackageError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PackageError::CommandFailed(msg) => write!(f, "Command failed: {}", msg),
            PackageError::CommandExecutionFailed(e) => write!(f, "Command execution failed: {}", e),
            PackageError::UnsupportedPlatform(msg) => write!(f, "Unsupported platform: {}", msg),
            PackageError::Other(msg) => write!(f, "Error: {}", msg),
        }
    }
}

impl std::error::Error for PackageError {}

/// Platform enum for detecting the current operating system
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Platform {
    /// Ubuntu Linux
    Ubuntu,
    /// macOS
    MacOS,
    /// Unknown platform
    Unknown,
}

impl Platform {
    /// Detect the current platform
    pub fn detect() -> Self {
        // Check for macOS
        if std::path::Path::new("/usr/bin/sw_vers").exists() {
            return Platform::MacOS;
        }

        // Check for Ubuntu
        if std::path::Path::new("/etc/lsb-release").exists() {
            // Read the file to confirm it's Ubuntu
            if let Ok(content) = std::fs::read_to_string("/etc/lsb-release") {
                if content.contains("Ubuntu") {
                    return Platform::Ubuntu;
                }
            }
        }

        Platform::Unknown
    }
}

// Thread-local storage for debug flag
thread_local! {
    static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
}

/// Sets the debug flag for the current thread.
///
/// This is used internally to control debug output for package operations.
pub fn set_thread_local_debug(debug: bool) {
    DEBUG.with(|cell| {
        *cell.borrow_mut() = debug;
    });
}

/// Gets the debug flag for the current thread.
pub fn thread_local_debug() -> bool {
    DEBUG.with(|cell| *cell.borrow())
}

/// Executes a package management command and returns the result.
///
/// This is a low-level function that executes arbitrary package management commands.
/// It captures both stdout and stderr and returns them along with the exit code.
///
/// # Arguments
///
/// * `args` - Command line arguments (first element is the executable)
/// * `debug` - If `true`, prints the command being executed
pub fn execute_package_command(args: &[&str], debug: bool) -> Result<CommandResult, PackageError> {
    // Save the current debug flag
    let previous_debug = thread_local_debug();

    // Set the thread-local debug flag
    set_thread_local_debug(debug);

    if debug {
        println!("Executing command: {}", args.join(" "));
    }

    let output = Command::new(args[0]).args(&args[1..]).output();

    // Restore the previous debug flag
    set_thread_local_debug(previous_debug);

    match output {
        Ok(output) => {
            let stdout = String::from_utf8_lossy(&output.stdout).to_string();
            let stderr = String::from_utf8_lossy(&output.stderr).to_string();

            let result = CommandResult {
                stdout,
                stderr,
                success: output.status.success(),
                code: output.status.code().unwrap_or(-1),
            };

            // Always output stdout/stderr when debug is true
            if debug {
                if !result.stdout.is_empty() {
                    println!("Command stdout: {}", result.stdout);
                }

                if !result.stderr.is_empty() {
                    println!("Command stderr: {}", result.stderr);
                }

                if result.success {
                    println!("Command succeeded with code {}", result.code);
                } else {
                    println!("Command failed with code {}", result.code);
                }
            }

            if result.success {
                Ok(result)
            } else {
                // If command failed and debug is false, output stderr
                if !debug {
                    println!(
                        "Command failed with code {}: {}",
                        result.code,
                        result.stderr.trim()
                    );
                }
                Err(PackageError::CommandFailed(format!(
                    "Command failed with code {}: {}",
                    result.code,
                    result.stderr.trim()
                )))
            }
        }
        Err(e) => {
            // Always output error information
            println!("Command execution failed: {}", e);
            Err(PackageError::CommandExecutionFailed(e))
        }
    }
}

/// Trait for package managers
/// Trait for platform-specific package management operations.
///
/// This trait defines a common interface for package managers across different platforms.
/// Each platform (Ubuntu, macOS) implements this trait with their specific package manager.
pub trait PackageManager {
    /// Installs a package on the system.
    ///
    /// # Arguments
    ///
    /// * `package` - The name of the package to install
    fn install(&self, package: &str) -> Result<CommandResult, PackageError>;

    /// Removes an installed package from the system.
    ///
    /// # Arguments
    ///
    /// * `package` - The name of the package to remove
    fn remove(&self, package: &str) -> Result<CommandResult, PackageError>;

    /// Updates the package manager's package lists from repositories.
    fn update(&self) -> Result<CommandResult, PackageError>;

    /// Upgrades all installed packages to their latest versions.
    fn upgrade(&self) -> Result<CommandResult, PackageError>;

    /// Lists all installed packages on the system.
    ///
    /// # Returns
    ///
    /// A vector of package names that are currently installed.
    fn list_installed(&self) -> Result<Vec<String>, PackageError>;

    /// Searches for packages matching the given query.
    ///
    /// # Arguments
    ///
    /// * `query` - A search term or pattern to find packages
    fn search(&self, query: &str) -> Result<Vec<String>, PackageError>;

    /// Checks if a specific package is installed on the system.
    ///
    /// # Arguments
    ///
    /// * `package` - The name of the package to check
    fn is_installed(&self, package: &str) -> Result<bool, PackageError>;
}

/// APT package manager implementation for Ubuntu/Debian systems.
///
/// This struct implements the `PackageManager` trait using the APT package manager,
/// which is the standard package management system for Ubuntu and Debian-based distributions.
pub struct AptPackageManager {
    /// Whether to print debug output for package operations
    debug: bool,
}

impl AptPackageManager {
    /// Creates a new APT package manager instance.
    ///
    /// # Arguments
    ///
    /// * `debug` - If `true`, prints debug information for package operations
    pub fn new(debug: bool) -> Self {
        Self { debug }
    }
}

impl PackageManager for AptPackageManager {
    fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
        // Use -y to make it non-interactive and --quiet to reduce output
        execute_package_command(
            &["apt-get", "install", "-y", "--quiet", package],
            self.debug,
        )
    }

    fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
        // Use -y to make it non-interactive and --quiet to reduce output
        execute_package_command(&["apt-get", "remove", "-y", "--quiet", package], self.debug)
    }

    fn update(&self) -> Result<CommandResult, PackageError> {
        // Use -y to make it non-interactive and --quiet to reduce output
        execute_package_command(&["apt-get", "update", "-y", "--quiet"], self.debug)
    }

    fn upgrade(&self) -> Result<CommandResult, PackageError> {
        // Use -y to make it non-interactive and --quiet to reduce output
        execute_package_command(&["apt-get", "upgrade", "-y", "--quiet"], self.debug)
    }

    fn list_installed(&self) -> Result<Vec<String>, PackageError> {
        let result = execute_package_command(&["dpkg", "--get-selections"], self.debug)?;
        let packages = result
            .stdout
            .lines()
            .filter_map(|line| {
                let parts: Vec<&str> = line.split_whitespace().collect();
                if parts.len() >= 2 && parts[1] == "install" {
                    Some(parts[0].to_string())
                } else {
                    None
                }
            })
            .collect();
        Ok(packages)
    }

    fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
        let result = execute_package_command(&["apt-cache", "search", query], self.debug)?;
        let packages = result
            .stdout
            .lines()
            .map(|line| {
                let parts: Vec<&str> = line.split_whitespace().collect();
                if !parts.is_empty() {
                    parts[0].to_string()
                } else {
                    String::new()
                }
            })
            .filter(|s| !s.is_empty())
            .collect();
        Ok(packages)
    }

    fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
        let result = execute_package_command(&["dpkg", "-s", package], self.debug);
        match result {
            Ok(cmd_result) => Ok(cmd_result.success),
            Err(_) => Ok(false),
        }
    }
}

/// Homebrew package manager implementation for macOS systems.
///
/// This struct implements the `PackageManager` trait using Homebrew,
/// which is the standard package manager for macOS.
pub struct BrewPackageManager {
    /// Whether to print debug output for package operations
    debug: bool,
}

impl BrewPackageManager {
    /// Creates a new Homebrew package manager instance.
    ///
    /// # Arguments
    ///
    /// * `debug` - If `true`, prints debug information for package operations
    pub fn new(debug: bool) -> Self {
        Self { debug }
    }
}

impl PackageManager for BrewPackageManager {
    fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
        // Use --quiet to reduce output
        execute_package_command(&["brew", "install", "--quiet", package], self.debug)
    }

    fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
        // Use --quiet to reduce output
        execute_package_command(&["brew", "uninstall", "--quiet", package], self.debug)
    }

    fn update(&self) -> Result<CommandResult, PackageError> {
        // Use --quiet to reduce output
        execute_package_command(&["brew", "update", "--quiet"], self.debug)
    }

    fn upgrade(&self) -> Result<CommandResult, PackageError> {
        // Use --quiet to reduce output
        execute_package_command(&["brew", "upgrade", "--quiet"], self.debug)
    }

    fn list_installed(&self) -> Result<Vec<String>, PackageError> {
        let result = execute_package_command(&["brew", "list", "--formula"], self.debug)?;
        let packages = result
            .stdout
            .lines()
            .map(|line| line.trim().to_string())
            .filter(|s| !s.is_empty())
            .collect();
        Ok(packages)
    }

    fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
        let result = execute_package_command(&["brew", "search", query], self.debug)?;
        let packages = result
            .stdout
            .lines()
            .map(|line| line.trim().to_string())
            .filter(|s| !s.is_empty())
            .collect();
        Ok(packages)
    }

    fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
        let result = execute_package_command(&["brew", "list", package], self.debug);
        match result {
            Ok(cmd_result) => Ok(cmd_result.success),
            Err(_) => Ok(false),
        }
    }
}

/// Cross-platform package manager factory.
///
/// PackHero automatically detects the current platform and provides a unified interface
/// for package management operations. It handles platform-specific details transparently,
/// allowing the same code to work on both Ubuntu (APT) and macOS (Homebrew).
///
/// # Example
///
/// ```no_run
/// use herolib_os::os::PackHero;
///
/// let mut pkg = PackHero::new();
/// pkg.set_debug(true);
/// pkg.install("curl")?;
/// pkg.update()?;
/// let installed = pkg.list_installed()?;
/// # Ok::<(), herolib_os::os::PackageError>(())
/// ```
pub struct PackHero {
    /// The detected platform
    platform: Platform,
    /// Whether to print debug output
    debug: bool,
}

impl PackHero {
    /// Creates a new PackHero instance with automatic platform detection.
    ///
    /// Platform detection is done immediately, and the detected platform is cached
    /// for the lifetime of this instance.
    pub fn new() -> Self {
        let platform = Platform::detect();
        Self {
            platform,
            debug: false,
        }
    }

    /// Sets the debug mode and returns a mutable reference for method chaining.
    ///
    /// When debug mode is enabled, package operations will print diagnostic information.
    pub fn set_debug(&mut self, debug: bool) -> &mut Self {
        self.debug = debug;
        self
    }

    /// Returns whether debug mode is currently enabled.
    pub fn debug(&self) -> bool {
        self.debug
    }

    /// Returns the detected platform for this instance.
    pub fn platform(&self) -> Platform {
        self.platform
    }

    /// Gets the appropriate package manager for the detected platform.
    ///
    /// # Returns
    ///
    /// `Ok(Box<dyn PackageManager>)` with the platform-specific implementation,
    /// or `Err(PackageError::UnsupportedPlatform)` if the platform is unknown.
    fn get_package_manager(&self) -> Result<Box<dyn PackageManager>, PackageError> {
        match self.platform {
            Platform::Ubuntu => Ok(Box::new(AptPackageManager::new(self.debug))),
            Platform::MacOS => Ok(Box::new(BrewPackageManager::new(self.debug))),
            Platform::Unknown => Err(PackageError::UnsupportedPlatform(
                "Unsupported platform".to_string(),
            )),
        }
    }

    /// Installs a package using the platform's package manager.
    ///
    /// # Arguments
    ///
    /// * `package` - The name of the package to install
    pub fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
        let pm = self.get_package_manager()?;
        pm.install(package)
    }

    /// Removes an installed package using the platform's package manager.
    ///
    /// # Arguments
    ///
    /// * `package` - The name of the package to remove
    pub fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
        let pm = self.get_package_manager()?;
        pm.remove(package)
    }

    /// Updates the package manager's repository lists.
    ///
    /// On Ubuntu: Updates APT package lists.
    /// On macOS: Updates Homebrew package lists.
    pub fn update(&self) -> Result<CommandResult, PackageError> {
        let pm = self.get_package_manager()?;
        pm.update()
    }

    /// Upgrades all installed packages to their latest versions.
    ///
    /// On Ubuntu: Runs `apt-get upgrade`.
    /// On macOS: Runs `brew upgrade`.
    pub fn upgrade(&self) -> Result<CommandResult, PackageError> {
        let pm = self.get_package_manager()?;
        pm.upgrade()
    }

    /// Lists all installed packages on the system.
    ///
    /// # Returns
    ///
    /// A vector of package names currently installed.
    pub fn list_installed(&self) -> Result<Vec<String>, PackageError> {
        let pm = self.get_package_manager()?;
        pm.list_installed()
    }

    /// Searches for packages matching the given query.
    ///
    /// # Arguments
    ///
    /// * `query` - A search term or pattern to find packages
    pub fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
        let pm = self.get_package_manager()?;
        pm.search(query)
    }

    /// Checks if a specific package is currently installed.
    ///
    /// # Arguments
    ///
    /// * `package` - The name of the package to check
    pub fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
        let pm = self.get_package_manager()?;
        pm.is_installed(package)
    }

    /// Removes one or more APT packages on Ubuntu/Debian (private).
    ///
    /// Internal function - use package_remove() for public API.
    fn apt_remove(&self, packages: &str) -> Result<(), PackageError> {
        // Only attempt on Ubuntu
        if self.platform() != Platform::Ubuntu {
            return Ok(()); // Silent success on non-Ubuntu
        }

        // Split comma-separated packages
        let package_list: Vec<&str> = packages.split(',').map(|s| s.trim()).collect();

        // Try to remove all packages
        let mut cmd = Command::new("apt-get");
        cmd.arg("remove").arg("-y");
        for package in package_list {
            cmd.arg(package);
        }

        let result = cmd.output().map_err(PackageError::CommandExecutionFailed)?;

        if result.status.success() {
            Ok(())
        } else {
            let stderr = String::from_utf8_lossy(&result.stderr);
            Err(PackageError::CommandFailed(format!(
                "apt-get remove failed: {}",
                stderr
            )))
        }
    }

    /// Removes one or more Homebrew packages on macOS (private).
    ///
    /// Internal function - use package_remove() for public API.
    fn brew_remove(&self, packages: &str) -> Result<(), PackageError> {
        // Only attempt on macOS
        if self.platform() != Platform::MacOS {
            return Ok(()); // Silent success on non-macOS
        }

        // Split comma-separated packages
        let package_list: Vec<&str> = packages.split(',').map(|s| s.trim()).collect();

        // Try to remove all packages
        // Note: brew uninstall doesn't have -y flag, use --force instead
        let mut cmd = Command::new("brew");
        cmd.arg("uninstall").arg("--force");
        for package in package_list {
            cmd.arg(package);
        }

        let result = cmd.output().map_err(PackageError::CommandExecutionFailed)?;

        if result.status.success() {
            Ok(())
        } else {
            let stderr = String::from_utf8_lossy(&result.stderr);
            Err(PackageError::CommandFailed(format!(
                "brew uninstall failed: {}",
                stderr
            )))
        }
    }

    /// Removes one or more packages using the appropriate package manager.
    ///
    /// This function automatically detects the platform and uses either APT (Ubuntu)
    /// or Homebrew (macOS) to remove packages. Packages are separated by commas.
    ///
    /// # Arguments
    ///
    /// * `packages` - Comma-separated package names to remove
    ///   - Ubuntu: "golang-go,git"
    ///   - macOS: "go,delve,gopls"
    ///
    /// # Returns
    ///
    /// * `Ok(())` - Packages were removed
    /// * `Err(PackageError)` - Removal failed or unsupported platform
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use herolib_os::os::PackHero;
    ///
    /// let hero = PackHero::new();
    /// hero.package_remove("go,delve")?;  // On macOS
    /// hero.package_remove("golang-go")?; // On Ubuntu
    /// # Ok::<(), herolib_os::os::PackageError>(())
    /// ```
    pub fn package_remove(&self, packages: &str) -> Result<(), PackageError> {
        match self.platform() {
            Platform::Ubuntu => self.apt_remove(packages),
            Platform::MacOS => self.brew_remove(packages),
            Platform::Unknown => Err(PackageError::CommandFailed(
                "Cannot remove packages on unknown platform".to_string(),
            )),
        }
    }
}

/// Builder for installing binaries and managing PATH entries
///
/// Provides a fluent interface to copy binaries and optionally add them to PATH.
/// Sensible defaults:
/// - `target_name`: Defaults to the filename from source path
/// - `target_dir`: Defaults to `/usr/local/bin`
/// - `add_to_path`: Defaults to `true` (automatically updates PATH in shell profiles)
///
/// # Example
///
/// ```no_run
/// use herolib_os::os::CmdInstaller;
///
/// CmdInstaller::new("/tmp/go/bin/go")
///     .install()?;
/// // Installs to /usr/local/bin/go and adds to PATH
///
/// CmdInstaller::new("/tmp/my-tool")
///     .target_name("my-tool")
///     .target_dir("/opt/tools")
///     .add_to_path(false)
///     .install()?;
/// # Ok::<(), herolib_os::os::PackageError>(())
/// ```
pub struct CmdInstaller {
    binary_path: String,
    target_name: Option<String>,
    target_dir: Option<String>,
    add_to_path: bool,
}

impl CmdInstaller {
    /// Create a new command installer from a binary path
    ///
    /// # Arguments
    ///
    /// * `binary_path` - Path to the binary to install (e.g., "/tmp/go/bin/go")
    pub fn new(binary_path: &str) -> Self {
        Self {
            binary_path: binary_path.to_string(),
            target_name: None,
            target_dir: None,
            add_to_path: true,
        }
    }

    /// Set the target name for the installed binary
    ///
    /// If not specified, defaults to the filename from the source path.
    pub fn target_name(mut self, name: &str) -> Self {
        self.target_name = Some(name.to_string());
        self
    }

    /// Set the target directory for installation
    ///
    /// If not specified, defaults to `/usr/local/bin`.
    pub fn target_dir(mut self, dir: &str) -> Self {
        self.target_dir = Some(dir.to_string());
        self
    }

    /// Set whether to automatically add target directory to PATH
    ///
    /// Defaults to `true` (PATH is automatically updated).
    pub fn add_to_path(mut self, add: bool) -> Self {
        self.add_to_path = add;
        self
    }

    /// Install the binary with configured options
    ///
    /// # Returns
    ///
    /// * `Ok(String)` - Success message
    /// * `Err(PackageError)` - If installation failed
    pub fn install(self) -> Result<String, PackageError> {
        use std::path::Path;

        // Determine target name: use provided or extract from source filename
        let target_name = if let Some(name) = self.target_name {
            name
        } else {
            Path::new(&self.binary_path)
                .file_name()
                .and_then(|n| n.to_str())
                .ok_or_else(|| {
                    PackageError::Other(format!(
                        "Cannot determine target name from path: {}",
                        self.binary_path
                    ))
                })?
                .to_string()
        };

        // Determine target directory: use provided or default based on OS
        let target_dir = if let Some(dir) = self.target_dir {
            dir
        } else {
            // macOS: ~/hero/bin/, Linux: /usr/local/bin/
            #[cfg(target_os = "macos")]
            {
                let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
                format!("{}/hero/bin", home)
            }
            #[cfg(not(target_os = "macos"))]
            {
                "/usr/local/bin".to_string()
            }
        };

        // Ensure target directory exists
        std::fs::create_dir_all(&target_dir).map_err(|e| {
            PackageError::Other(format!(
                "Failed to create target directory {}: {}",
                target_dir, e
            ))
        })?;

        // Build target binary path
        let target_binary = format!("{}/{}", target_dir, target_name);

        // Copy the binary to target directory
        std::fs::copy(&self.binary_path, &target_binary).map_err(|e| {
            PackageError::Other(format!(
                "Failed to copy binary from {} to {}: {}",
                self.binary_path, target_binary, e
            ))
        })?;

        // Make binary executable on Unix
        #[cfg(not(target_os = "windows"))]
        {
            use std::os::unix::fs::PermissionsExt;
            let perms = std::fs::Permissions::from_mode(0o755);
            std::fs::set_permissions(&target_binary, perms).map_err(|e| {
                PackageError::Other(format!("Failed to make binary executable: {}", e))
            })?;
        }

        // Add to PATH if requested
        // Note: profile_path_add_remove is not yet implemented
        // For now, just log a message about adding to PATH
        if self.add_to_path {
            eprintln!(
                "Note: Please add {} to your PATH manually (profile management not yet implemented)",
                target_dir
            );
        }

        Ok(format!(
            "Binary '{}' installed to {} successfully",
            target_name, target_dir
        ))
    }
}

/// Check if a command exists in PATH
///
/// # Arguments
///
/// * `cmd` - The command to check
///
/// # Returns
///
/// * `Option<String>` - The full path to the command if found, None otherwise
///
/// # Examples
///
/// ```
/// use herolib_os::os::package::which;
///
/// match which("git") {
///     Some(path) => println!("Git is installed at: {}", path),
///     None => println!("Git is not installed"),
/// }
/// ```
pub fn which(cmd: &str) -> Option<String> {
    #[cfg(target_os = "windows")]
    let which_cmd = "where";

    #[cfg(any(target_os = "macos", target_os = "linux"))]
    let which_cmd = "which";

    let output = Command::new(which_cmd).arg(cmd).output();

    match output {
        Ok(out) => {
            if out.status.success() {
                let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
                Some(path)
            } else {
                None
            }
        }
        Err(_) => None,
    }
}

#[cfg(test)]
mod tests {
    // Import the std::process::Command directly for some test-specific commands
    use super::*;
    use std::process::Command as StdCommand;
    use std::sync::{Arc, Mutex};

    #[test]
    fn test_platform_detection() {
        // Test that platform detection returns a valid platform
        let platform = Platform::detect();
        println!("Detected platform: {:?}", platform);

        // Verify that we get one of the expected platform values
        match platform {
            Platform::Ubuntu | Platform::MacOS | Platform::Unknown => {
                // All valid platforms
            }
        }

        // Test that detection is consistent (calling it twice should return the same result)
        let platform2 = Platform::detect();
        assert_eq!(platform, platform2);

        // Test that the platform detection logic makes sense for the current environment
        match platform {
            Platform::MacOS => {
                // If detected as macOS, sw_vers should exist
                assert!(std::path::Path::new("/usr/bin/sw_vers").exists());
            }
            Platform::Ubuntu => {
                // If detected as Ubuntu, lsb-release should exist and contain "Ubuntu"
                assert!(std::path::Path::new("/etc/lsb-release").exists());
                if let Ok(content) = std::fs::read_to_string("/etc/lsb-release") {
                    assert!(content.contains("Ubuntu"));
                }
            }
            Platform::Unknown => {
                // If unknown, neither macOS nor Ubuntu indicators should be present
                // (or Ubuntu file exists but doesn't contain "Ubuntu")
                if std::path::Path::new("/usr/bin/sw_vers").exists() {
                    // This shouldn't happen - if sw_vers exists, it should be detected as macOS
                    panic!("sw_vers exists but platform detected as Unknown");
                }
            }
        }
    }

    #[test]
    fn test_debug_flag() {
        // Test setting and getting the debug flag
        set_thread_local_debug(true);
        assert_eq!(thread_local_debug(), true);

        set_thread_local_debug(false);
        assert_eq!(thread_local_debug(), false);
    }

    #[test]
    fn test_package_error_display() {
        // Test the Display implementation for PackageError
        let err1 = PackageError::CommandFailed("command failed".to_string());
        assert_eq!(err1.to_string(), "Command failed: command failed");

        let err2 = PackageError::UnsupportedPlatform("test platform".to_string());
        assert_eq!(err2.to_string(), "Unsupported platform: test platform");

        let err3 = PackageError::Other("other error".to_string());
        assert_eq!(err3.to_string(), "Error: other error");

        // We can't easily test CommandExecutionFailed because std::io::Error doesn't implement PartialEq
    }

    // Mock package manager for testing
    struct MockPackageManager {
        // debug field is kept for consistency with real package managers
        #[allow(dead_code)]
        debug: bool,
        install_called: Arc<Mutex<bool>>,
        remove_called: Arc<Mutex<bool>>,
        update_called: Arc<Mutex<bool>>,
        upgrade_called: Arc<Mutex<bool>>,
        list_installed_called: Arc<Mutex<bool>>,
        search_called: Arc<Mutex<bool>>,
        is_installed_called: Arc<Mutex<bool>>,
        // Control what the mock returns
        should_succeed: bool,
    }

    impl MockPackageManager {
        fn new(debug: bool, should_succeed: bool) -> Self {
            Self {
                debug,
                install_called: Arc::new(Mutex::new(false)),
                remove_called: Arc::new(Mutex::new(false)),
                update_called: Arc::new(Mutex::new(false)),
                upgrade_called: Arc::new(Mutex::new(false)),
                list_installed_called: Arc::new(Mutex::new(false)),
                search_called: Arc::new(Mutex::new(false)),
                is_installed_called: Arc::new(Mutex::new(false)),
                should_succeed,
            }
        }
    }

    impl PackageManager for MockPackageManager {
        fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
            *self.install_called.lock().unwrap() = true;
            if self.should_succeed {
                Ok(CommandResult {
                    stdout: format!("Installed package {}", package),
                    stderr: String::new(),
                    success: true,
                    code: 0,
                })
            } else {
                Err(PackageError::CommandFailed(
                    "Mock install failed".to_string(),
                ))
            }
        }

        fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
            *self.remove_called.lock().unwrap() = true;
            if self.should_succeed {
                Ok(CommandResult {
                    stdout: format!("Removed package {}", package),
                    stderr: String::new(),
                    success: true,
                    code: 0,
                })
            } else {
                Err(PackageError::CommandFailed(
                    "Mock remove failed".to_string(),
                ))
            }
        }

        fn update(&self) -> Result<CommandResult, PackageError> {
            *self.update_called.lock().unwrap() = true;
            if self.should_succeed {
                Ok(CommandResult {
                    stdout: "Updated package lists".to_string(),
                    stderr: String::new(),
                    success: true,
                    code: 0,
                })
            } else {
                Err(PackageError::CommandFailed(
                    "Mock update failed".to_string(),
                ))
            }
        }

        fn upgrade(&self) -> Result<CommandResult, PackageError> {
            *self.upgrade_called.lock().unwrap() = true;
            if self.should_succeed {
                Ok(CommandResult {
                    stdout: "Upgraded packages".to_string(),
                    stderr: String::new(),
                    success: true,
                    code: 0,
                })
            } else {
                Err(PackageError::CommandFailed(
                    "Mock upgrade failed".to_string(),
                ))
            }
        }

        fn list_installed(&self) -> Result<Vec<String>, PackageError> {
            *self.list_installed_called.lock().unwrap() = true;
            if self.should_succeed {
                Ok(vec!["package1".to_string(), "package2".to_string()])
            } else {
                Err(PackageError::CommandFailed(
                    "Mock list_installed failed".to_string(),
                ))
            }
        }

        fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
            *self.search_called.lock().unwrap() = true;
            if self.should_succeed {
                Ok(vec![
                    format!("result1-{}", query),
                    format!("result2-{}", query),
                ])
            } else {
                Err(PackageError::CommandFailed(
                    "Mock search failed".to_string(),
                ))
            }
        }

        fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
            *self.is_installed_called.lock().unwrap() = true;
            if self.should_succeed {
                Ok(package == "installed-package")
            } else {
                Err(PackageError::CommandFailed(
                    "Mock is_installed failed".to_string(),
                ))
            }
        }
    }

    // Custom PackHero for testing with a mock package manager
    struct TestPackHero {
        platform: Platform,
        #[allow(dead_code)]
        debug: bool,
        mock_manager: MockPackageManager,
    }

    impl TestPackHero {
        fn new(platform: Platform, debug: bool, should_succeed: bool) -> Self {
            Self {
                platform,
                debug,
                mock_manager: MockPackageManager::new(debug, should_succeed),
            }
        }

        fn get_package_manager(&self) -> Result<&dyn PackageManager, PackageError> {
            match self.platform {
                Platform::Ubuntu | Platform::MacOS => Ok(&self.mock_manager),
                Platform::Unknown => Err(PackageError::UnsupportedPlatform(
                    "Unsupported platform".to_string(),
                )),
            }
        }

        fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
            let pm = self.get_package_manager()?;
            pm.install(package)
        }

        fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
            let pm = self.get_package_manager()?;
            pm.remove(package)
        }

        fn update(&self) -> Result<CommandResult, PackageError> {
            let pm = self.get_package_manager()?;
            pm.update()
        }

        fn upgrade(&self) -> Result<CommandResult, PackageError> {
            let pm = self.get_package_manager()?;
            pm.upgrade()
        }

        fn list_installed(&self) -> Result<Vec<String>, PackageError> {
            let pm = self.get_package_manager()?;
            pm.list_installed()
        }

        fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
            let pm = self.get_package_manager()?;
            pm.search(query)
        }

        fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
            let pm = self.get_package_manager()?;
            pm.is_installed(package)
        }
    }

    #[test]
    fn test_packhero_with_mock_success() {
        // Test PackHero with a mock package manager that succeeds
        let hero = TestPackHero::new(Platform::Ubuntu, false, true);

        // Test install
        let result = hero.install("test-package");
        assert!(result.is_ok());
        assert!(*hero.mock_manager.install_called.lock().unwrap());

        // Test remove
        let result = hero.remove("test-package");
        assert!(result.is_ok());
        assert!(*hero.mock_manager.remove_called.lock().unwrap());

        // Test update
        let result = hero.update();
        assert!(result.is_ok());
        assert!(*hero.mock_manager.update_called.lock().unwrap());

        // Test upgrade
        let result = hero.upgrade();
        assert!(result.is_ok());
        assert!(*hero.mock_manager.upgrade_called.lock().unwrap());

        // Test list_installed
        let result = hero.list_installed();
        assert!(result.is_ok());
        assert_eq!(
            result.unwrap(),
            vec!["package1".to_string(), "package2".to_string()]
        );
        assert!(*hero.mock_manager.list_installed_called.lock().unwrap());

        // Test search
        let result = hero.search("query");
        assert!(result.is_ok());
        assert_eq!(
            result.unwrap(),
            vec!["result1-query".to_string(), "result2-query".to_string()]
        );
        assert!(*hero.mock_manager.search_called.lock().unwrap());

        // Test is_installed
        let result = hero.is_installed("installed-package");
        assert!(result.is_ok());
        assert!(result.unwrap());
        assert!(*hero.mock_manager.is_installed_called.lock().unwrap());

        let result = hero.is_installed("not-installed-package");
        assert!(result.is_ok());
        assert!(!result.unwrap());
    }

    #[test]
    fn test_packhero_with_mock_failure() {
        // Test PackHero with a mock package manager that fails
        let hero = TestPackHero::new(Platform::Ubuntu, false, false);

        // Test install
        let result = hero.install("test-package");
        assert!(result.is_err());
        assert!(*hero.mock_manager.install_called.lock().unwrap());

        // Test remove
        let result = hero.remove("test-package");
        assert!(result.is_err());
        assert!(*hero.mock_manager.remove_called.lock().unwrap());

        // Test update
        let result = hero.update();
        assert!(result.is_err());
        assert!(*hero.mock_manager.update_called.lock().unwrap());

        // Test upgrade
        let result = hero.upgrade();
        assert!(result.is_err());
        assert!(*hero.mock_manager.upgrade_called.lock().unwrap());

        // Test list_installed
        let result = hero.list_installed();
        assert!(result.is_err());
        assert!(*hero.mock_manager.list_installed_called.lock().unwrap());

        // Test search
        let result = hero.search("query");
        assert!(result.is_err());
        assert!(*hero.mock_manager.search_called.lock().unwrap());

        // Test is_installed
        let result = hero.is_installed("installed-package");
        assert!(result.is_err());
        assert!(*hero.mock_manager.is_installed_called.lock().unwrap());
    }

    #[test]
    fn test_packhero_unsupported_platform() {
        // Test PackHero with an unsupported platform
        let hero = TestPackHero::new(Platform::Unknown, false, true);

        // All operations should fail with UnsupportedPlatform error
        let result = hero.install("test-package");
        assert!(result.is_err());
        match result {
            Err(PackageError::UnsupportedPlatform(_)) => (),
            _ => panic!("Expected UnsupportedPlatform error"),
        }

        let result = hero.remove("test-package");
        assert!(result.is_err());
        match result {
            Err(PackageError::UnsupportedPlatform(_)) => (),
            _ => panic!("Expected UnsupportedPlatform error"),
        }

        let result = hero.update();
        assert!(result.is_err());
        match result {
            Err(PackageError::UnsupportedPlatform(_)) => (),
            _ => panic!("Expected UnsupportedPlatform error"),
        }
    }

    // Real-world tests that actually install and remove packages on Ubuntu
    // These tests will only run on Ubuntu and will be skipped on other platforms
    #[test]
    fn test_real_package_operations_on_ubuntu() {
        // Check if we're on Ubuntu
        let platform = Platform::detect();
        if platform != Platform::Ubuntu {
            println!(
                "Skipping real package operations test on non-Ubuntu platform: {:?}",
                platform
            );
            return;
        }

        println!("Running real package operations test on Ubuntu");

        // Create a PackHero instance with debug enabled
        let mut hero = PackHero::new();
        hero.set_debug(true);

        // Test package to install/remove
        let test_package = "wget";

        // First, check if the package is already installed
        let is_installed_before = match hero.is_installed(test_package) {
            Ok(result) => result,
            Err(e) => {
                println!("Error checking if package is installed: {}", e);
                return;
            }
        };

        println!(
            "Package {} is installed before test: {}",
            test_package, is_installed_before
        );

        // If the package is already installed, we'll remove it first
        if is_installed_before {
            println!("Removing existing package {} before test", test_package);
            match hero.remove(test_package) {
                Ok(_) => println!("Successfully removed package {}", test_package),
                Err(e) => {
                    println!("Error removing package {}: {}", test_package, e);
                    return;
                }
            }

            // Verify it was removed
            match hero.is_installed(test_package) {
                Ok(is_installed) => {
                    if is_installed {
                        println!("Failed to remove package {}", test_package);
                        return;
                    } else {
                        println!("Verified package {} was removed", test_package);
                    }
                }
                Err(e) => {
                    println!(
                        "Error checking if package is installed after removal: {}",
                        e
                    );
                    return;
                }
            }
        }

        // Now install the package
        println!("Installing package {}", test_package);
        match hero.install(test_package) {
            Ok(_) => println!("Successfully installed package {}", test_package),
            Err(e) => {
                println!("Error installing package {}: {}", test_package, e);
                return;
            }
        }

        // Verify it was installed
        match hero.is_installed(test_package) {
            Ok(is_installed) => {
                if !is_installed {
                    println!("Failed to install package {}", test_package);
                    return;
                } else {
                    println!("Verified package {} was installed", test_package);
                }
            }
            Err(e) => {
                println!(
                    "Error checking if package is installed after installation: {}",
                    e
                );
                return;
            }
        }

        // Test the search functionality
        println!("Searching for packages with 'wget'");
        match hero.search("wget") {
            Ok(results) => {
                println!("Search results: {:?}", results);
                assert!(
                    results.iter().any(|r| r.contains("wget")),
                    "Search results should contain wget"
                );
            }
            Err(e) => {
                println!("Error searching for packages: {}", e);
                return;
            }
        }

        // Test listing installed packages
        println!("Listing installed packages");
        match hero.list_installed() {
            Ok(packages) => {
                println!("Found {} installed packages", packages.len());
                // Check if our test package is in the list
                assert!(
                    packages.iter().any(|p| p == test_package),
                    "Installed packages list should contain {}",
                    test_package
                );
            }
            Err(e) => {
                println!("Error listing installed packages: {}", e);
                return;
            }
        }

        // Now remove the package if it wasn't installed before
        if !is_installed_before {
            println!("Removing package {} after test", test_package);
            match hero.remove(test_package) {
                Ok(_) => println!("Successfully removed package {}", test_package),
                Err(e) => {
                    println!("Error removing package {}: {}", test_package, e);
                    return;
                }
            }

            // Verify it was removed
            match hero.is_installed(test_package) {
                Ok(is_installed) => {
                    if is_installed {
                        println!("Failed to remove package {}", test_package);
                        return;
                    } else {
                        println!("Verified package {} was removed", test_package);
                    }
                }
                Err(e) => {
                    println!(
                        "Error checking if package is installed after removal: {}",
                        e
                    );
                    return;
                }
            }
        }

        // Test update functionality
        println!("Testing package list update");
        match hero.update() {
            Ok(_) => println!("Successfully updated package lists"),
            Err(e) => {
                println!("Error updating package lists: {}", e);
                return;
            }
        }

        println!("All real package operations tests passed on Ubuntu");
    }

    // Test to check if apt-get is available on the system
    #[test]
    fn test_apt_get_availability() {
        // This test checks if apt-get is available on the system
        let output = StdCommand::new("which")
            .arg("apt-get")
            .output()
            .expect("Failed to execute which apt-get");

        let success = output.status.success();
        let stdout = String::from_utf8_lossy(&output.stdout).to_string();

        println!("apt-get available: {}", success);
        if success {
            println!("apt-get path: {}", stdout.trim());
        }

        // On Ubuntu, this should pass
        if Platform::detect() == Platform::Ubuntu {
            assert!(success, "apt-get should be available on Ubuntu");
        }
    }
}
