use std::error::Error;
use std::fmt;
use std::fs::{self, File};
use std::io;
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::thread;

/// Error type for command and script execution operations
#[derive(Debug)]
pub enum RunError {
    /// The command string was empty
    EmptyCommand,
    /// An error occurred while executing a command
    CommandExecutionFailed(io::Error),
    /// A command executed successfully but returned an error
    CommandFailed(String),
    /// An error occurred while preparing a script for execution
    ScriptPreparationFailed(String),
    /// An error occurred in a child process
    ChildProcessError(String),
    /// Failed to create a temporary directory
    TempDirCreationFailed(io::Error),
    /// Failed to create a script file
    FileCreationFailed(io::Error),
    /// Failed to write to a script file
    FileWriteFailed(io::Error),
    /// Failed to set file permissions
    PermissionError(io::Error),
}

/// Implement Display for RunError to provide human-readable error messages
impl fmt::Display for RunError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            RunError::EmptyCommand => write!(f, "Empty command"),
            RunError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e),
            RunError::CommandFailed(e) => write!(f, "{}", e),
            RunError::ScriptPreparationFailed(e) => write!(f, "{}", e),
            RunError::ChildProcessError(e) => write!(f, "{}", e),
            RunError::TempDirCreationFailed(e) => {
                write!(f, "Failed to create temporary directory: {}", e)
            }
            RunError::FileCreationFailed(e) => write!(f, "Failed to create script file: {}", e),
            RunError::FileWriteFailed(e) => write!(f, "Failed to write to script file: {}", e),
            RunError::PermissionError(e) => write!(f, "Failed to set file permissions: {}", e),
        }
    }
}

// Implement Error trait for RunError
impl Error for RunError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            RunError::CommandExecutionFailed(e) => Some(e),
            RunError::TempDirCreationFailed(e) => Some(e),
            RunError::FileCreationFailed(e) => Some(e),
            RunError::FileWriteFailed(e) => Some(e),
            RunError::PermissionError(e) => Some(e),
            _ => None,
        }
    }
}

/// A structure to hold command execution results
#[derive(Debug, Clone)]
pub struct CommandResult {
    pub stdout: String,
    pub stderr: String,
    pub success: bool,
    pub code: i32,
}

impl CommandResult {
    // Implementation methods can be added here as needed
}

/// Dedent a multiline string by removing common leading whitespace.
/// This is a local copy to avoid cyclic dependency with herolib-core.
fn dedent(text: &str) -> String {
    let lines: Vec<&str> = text.lines().collect();

    // Find the minimum indentation level (ignore empty lines)
    let min_indent = lines
        .iter()
        .filter(|line| !line.trim().is_empty())
        .map(|line| {
            let mut spaces = 0;
            for c in line.chars() {
                if c == ' ' {
                    spaces += 1;
                } else if c == '\t' {
                    spaces += 4; // Count tabs as 4 spaces
                } else {
                    break;
                }
            }
            spaces
        })
        .min()
        .unwrap_or(0);

    // Remove that many spaces from the beginning of each line
    lines
        .iter()
        .map(|line| {
            if line.trim().is_empty() {
                return String::new();
            }

            let mut count = 0;
            let mut chars = line.chars().peekable();

            // Skip initial spaces up to min_indent
            while count < min_indent && chars.peek().is_some() {
                match chars.peek() {
                    Some(' ') => {
                        chars.next();
                        count += 1;
                    }
                    Some('\t') => {
                        chars.next();
                        count += 4;
                    }
                    _ => break,
                }
            }

            // Return the remaining characters
            chars.collect::<String>()
        })
        .collect::<Vec<String>>()
        .join("\n")
}

/// Prepare a script file and return the path and interpreter
fn prepare_script_file(
    script_content: &str,
) -> Result<(PathBuf, String, tempfile::TempDir), RunError> {
    // Dedent the script
    let dedented = dedent(script_content);

    // Create a temporary directory
    let temp_dir = tempfile::tempdir().map_err(RunError::TempDirCreationFailed)?;

    // Determine script extension and interpreter
    #[cfg(target_os = "windows")]
    let (ext, interpreter) = (".bat", "cmd.exe".to_string());

    #[cfg(any(target_os = "macos", target_os = "linux"))]
    let (ext, interpreter) = (".sh", "/bin/bash".to_string());

    // Create the script file
    let script_path = temp_dir.path().join(format!("script{}", ext));
    let mut file = File::create(&script_path).map_err(RunError::FileCreationFailed)?;

    // For Unix systems, ensure the script has a shebang line with -e flag
    #[cfg(any(target_os = "macos", target_os = "linux"))]
    {
        let script_with_shebang = if dedented.trim_start().starts_with("#!") {
            // Script already has a shebang, use it as is
            dedented
        } else {
            // Add shebang with -e flag to ensure script fails on errors
            format!("#!/bin/bash -e\n{}", dedented)
        };

        // Write the script content with shebang
        file.write_all(script_with_shebang.as_bytes())
            .map_err(RunError::FileWriteFailed)?;
    }

    // For Windows, just write the script as is
    #[cfg(target_os = "windows")]
    {
        file.write_all(dedented.as_bytes())
            .map_err(RunError::FileWriteFailed)?;
    }

    // Make the script executable (Unix only)
    #[cfg(any(target_os = "macos", target_os = "linux"))]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = fs::metadata(&script_path)
            .map_err(|e| RunError::PermissionError(e))?
            .permissions();
        perms.set_mode(0o755); // rwxr-xr-x
        fs::set_permissions(&script_path, perms).map_err(RunError::PermissionError)?;
    }

    Ok((script_path, interpreter, temp_dir))
}

/// Capture output from Child's stdio streams with optional printing
fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult, RunError> {
    // Prepare to read stdout & stderr line-by-line
    let stdout = child.stdout.take();
    let stderr = child.stderr.take();

    // Process stdout
    let stdout_handle = if let Some(out) = stdout {
        let reader = BufReader::new(out);
        let silent_clone = silent;
        // Spawn a thread to capture and optionally print stdout
        Some(std::thread::spawn(move || {
            let mut local_buffer = String::new();
            for line in reader.lines() {
                if let Ok(l) = line {
                    // Print the line if not silent and flush immediately
                    if !silent_clone {
                        println!("{}", l);
                        std::io::stdout().flush().unwrap_or(());
                    }
                    // Store it in our captured buffer
                    local_buffer.push_str(&l);
                    local_buffer.push('\n');
                }
            }
            local_buffer
        }))
    } else {
        None
    };

    // Process stderr
    let stderr_handle = if let Some(err) = stderr {
        let reader = BufReader::new(err);
        let silent_clone = silent;
        // Spawn a thread to capture and optionally print stderr
        Some(std::thread::spawn(move || {
            let mut local_buffer = String::new();
            for line in reader.lines() {
                if let Ok(l) = line {
                    // Print the line if not silent and flush immediately
                    if !silent_clone {
                        // Print all stderr messages
                        eprintln!("\x1b[31mERROR: {}\x1b[0m", l); // Red color for errors
                        std::io::stderr().flush().unwrap_or(());
                    }
                    // Store it in our captured buffer
                    local_buffer.push_str(&l);
                    local_buffer.push('\n');
                }
            }
            local_buffer
        }))
    } else {
        None
    };

    // Wait for the child process to exit
    let status = child.wait().map_err(|e| {
        RunError::ChildProcessError(format!("Failed to wait on child process: {}", e))
    })?;

    // Join our stdout thread if it exists
    let captured_stdout = if let Some(handle) = stdout_handle {
        handle.join().unwrap_or_default()
    } else {
        "Failed to capture stdout".to_string()
    };

    // Join our stderr thread if it exists
    let captured_stderr = if let Some(handle) = stderr_handle {
        handle.join().unwrap_or_default()
    } else {
        "Failed to capture stderr".to_string()
    };

    // If the command failed, print the stderr if it wasn't already printed
    if !status.success() && silent && !captured_stderr.is_empty() {
        eprintln!("\x1b[31mCommand failed with error:\x1b[0m");
        for line in captured_stderr.lines() {
            eprintln!("\x1b[31m{}\x1b[0m", line);
        }
    }

    // Return the command result
    Ok(CommandResult {
        stdout: captured_stdout,
        stderr: captured_stderr,
        success: status.success(),
        code: status.code().unwrap_or(-1),
    })
}

/// Processes Output structure from Command::output() into CommandResult
fn process_command_output(
    output: Result<Output, std::io::Error>,
) -> Result<CommandResult, RunError> {
    match output {
        Ok(out) => {
            let stdout = String::from_utf8_lossy(&out.stdout).to_string();
            let stderr = String::from_utf8_lossy(&out.stderr).to_string();
            // We'll collect stderr but not print it here
            // It will be included in the error message if the command fails

            // If the command failed, print a clear error message
            if !out.status.success() {
                eprintln!(
                    "\x1b[31mCommand failed with exit code: {}\x1b[0m",
                    out.status.code().unwrap_or(-1)
                );
            }

            Ok(CommandResult {
                stdout,
                stderr,
                success: out.status.success(),
                code: out.status.code().unwrap_or(-1),
            })
        }
        Err(e) => Err(RunError::CommandExecutionFailed(e)),
    }
}

/// Common logic for running a command with optional silent mode
fn run_command_internal(command: &str, silent: bool) -> Result<CommandResult, RunError> {
    let mut parts = command.split_whitespace();
    let cmd = match parts.next() {
        Some(c) => c,
        None => return Err(RunError::EmptyCommand),
    };

    let args: Vec<&str> = parts.collect();

    // Spawn the child process with piped stdout & stderr
    let child = Command::new(cmd)
        .args(&args)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .map_err(RunError::CommandExecutionFailed)?;

    handle_child_output(child, silent)
}

/// Execute a script with the given interpreter and path
fn execute_script_internal(
    interpreter: &str,
    script_path: &Path,
    silent: bool,
) -> Result<CommandResult, RunError> {
    #[cfg(target_os = "windows")]
    let command_args = vec!["/c", script_path.to_str().unwrap_or("")];

    #[cfg(any(target_os = "macos", target_os = "linux"))]
    let command_args = vec!["-e", script_path.to_str().unwrap_or("")];

    if silent {
        // For silent execution, use output() which captures but doesn't display
        let output = Command::new(interpreter).args(&command_args).output();

        let result = process_command_output(output)?;

        // If the script failed, return an error
        if !result.success {
            return Err(RunError::CommandFailed(format!(
                "Script execution failed with exit code {}: {}",
                result.code,
                result.stderr.trim()
            )));
        }

        Ok(result)
    } else {
        // For normal execution, spawn and handle the output streams
        let child = Command::new(interpreter)
            .args(&command_args)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .map_err(RunError::CommandExecutionFailed)?;

        let result = handle_child_output(child, false)?;

        // If the script failed, return an error
        if !result.success {
            return Err(RunError::CommandFailed(format!(
                "Script execution failed with exit code {}: {}",
                result.code,
                result.stderr.trim()
            )));
        }

        Ok(result)
    }
}

/// Run a multiline script with optional silent mode
fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> {
    // Prepare the script file first to get the content with shebang
    let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?;

    // Print the script being executed if not silent
    if !silent {
        println!("\x1b[36mExecuting script:\x1b[0m");

        // Read the script file to get the content with shebang
        if let Ok(script_content) = fs::read_to_string(&script_path) {
            for (i, line) in script_content.lines().enumerate() {
                println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
            }
        } else {
            // Fallback to original script if reading fails
            for (i, line) in script.lines().enumerate() {
                println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
            }
        }

        println!("\x1b[36m---\x1b[0m");
    }

    // _temp_dir is kept in scope until the end of this function to ensure
    // it's not dropped prematurely, which would clean up the directory

    // Execute the script and handle the result
    let result = execute_script_internal(&interpreter, &script_path, silent);

    // If there was an error, print a clear error message only if it's not a CommandFailed error
    // (which would already have printed the stderr)
    if let Err(ref e) = result {
        if !matches!(e, RunError::CommandFailed(_)) {
            eprintln!("\x1b[31mScript execution failed: {}\x1b[0m", e);
        }
    }

    result
}

/// A builder for configuring and executing commands or scripts.
///
/// This builder allows fine-grained control over command execution including error handling,
/// output suppression, async execution, and logging. It supports both single commands and
/// multiline scripts.
///
/// # Example
///
/// ```no_run
/// use herolib_os::process::run;
///
/// // Run a simple command
/// let result = run("echo hello").execute()?;
/// assert!(result.success);
///
/// // Run with options
/// let result = run("curl https://example.com")
///     .silent(true)
///     .log(true)
///     .execute()?;
/// # Ok::<(), herolib_os::process::RunError>(())
/// ```
pub struct RunBuilder<'a> {
    /// The command or script to run
    cmd: &'a str,
    /// Whether to return an error if the command fails (default: true)
    die: bool,
    /// Whether to suppress output to stdout/stderr (default: false)
    silent: bool,
    /// Whether to run the command asynchronously (default: false)
    async_exec: bool,
    /// Whether to log command execution (default: false)
    log: bool,
}

impl<'a> RunBuilder<'a> {
    /// Creates a new RunBuilder with default settings.
    ///
    /// # Arguments
    ///
    /// * `cmd` - The command or script to run
    ///
    /// # Defaults
    ///
    /// - `die`: `true` - Returns error if command fails
    /// - `silent`: `false` - Captures and displays output
    /// - `async_exec`: `false` - Runs synchronously
    /// - `log`: `false` - No execution logging
    pub fn new(cmd: &'a str) -> Self {
        Self {
            cmd,
            die: true,
            silent: false,
            async_exec: false,
            log: false,
        }
    }

    /// Sets whether to return an error if the command fails.
    ///
    /// When `true` (default), a non-zero exit code causes the command to return an error.
    /// When `false`, the command returns `Ok` with `success: false` instead.
    pub fn die(mut self, die: bool) -> Self {
        self.die = die;
        self
    }

    /// Sets whether to suppress output to stdout/stderr.
    ///
    /// When `true`, command output is captured but not displayed.
    /// When `false` (default), output is displayed in real-time.
    pub fn silent(mut self, silent: bool) -> Self {
        self.silent = silent;
        self
    }

    /// Sets whether to run the command asynchronously.
    ///
    /// When `true`, the command runs in a background thread and returns immediately.
    /// When `false` (default), the function blocks until the command completes.
    pub fn async_exec(mut self, async_exec: bool) -> Self {
        self.async_exec = async_exec;
        self
    }

    /// Sets whether to log command execution details.
    ///
    /// When `true`, prints execution start, completion status, and any errors.
    /// When `false` (default), runs silently.
    pub fn log(mut self, log: bool) -> Self {
        self.log = log;
        self
    }

    /// Executes the command or script with the configured options.
    ///
    /// # Returns
    ///
    /// Returns `Ok(CommandResult)` with execution details, or `Err(RunError)` if:
    /// - The command fails and `die` is `true`
    /// - The command cannot be executed (file not found, permission denied, etc.)
    ///
    /// # Example
    ///
    /// ```no_run
    /// use herolib_os::process::run;
    ///
    /// let result = run("ls -la")
    ///     .silent(true)
    ///     .execute()?;
    /// println!("Output: {}", result.stdout);
    /// # Ok::<(), herolib_os::process::RunError>(())
    /// ```
    pub fn execute(self) -> Result<CommandResult, RunError> {
        let trimmed = self.cmd.trim();

        // Log command execution if enabled
        if self.log {
            println!("\x1b[36m[LOG] Executing command: {}\x1b[0m", trimmed);
        }

        // Handle async execution
        if self.async_exec {
            let cmd_copy = trimmed.to_string();
            let silent = self.silent;
            let log = self.log;

            // Spawn a thread to run the command asynchronously
            thread::spawn(move || {
                if log {
                    println!("\x1b[36m[ASYNC] Starting execution\x1b[0m");
                }

                let result = if cmd_copy.contains('\n') {
                    run_script_internal(&cmd_copy, silent)
                } else {
                    run_command_internal(&cmd_copy, silent)
                };

                if log {
                    match &result {
                        Ok(res) => {
                            if res.success {
                                println!("\x1b[32m[ASYNC] Command completed successfully\x1b[0m");
                            } else {
                                eprintln!(
                                    "\x1b[31m[ASYNC] Command failed with exit code: {}\x1b[0m",
                                    res.code
                                );
                            }
                        }
                        Err(e) => {
                            eprintln!("\x1b[31m[ASYNC] Command failed with error: {}\x1b[0m", e);
                        }
                    }
                }
            });

            // Return a placeholder result for async execution
            return Ok(CommandResult {
                stdout: String::new(),
                stderr: String::new(),
                success: true,
                code: 0,
            });
        }

        // Execute the command or script
        let result = if trimmed.contains('\n') {
            // This is a multiline script
            run_script_internal(trimmed, self.silent)
        } else {
            // This is a single command
            run_command_internal(trimmed, self.silent)
        };

        // Handle die=false: convert errors to CommandResult with success=false
        match result {
            Ok(res) => {
                // If the command failed and die is true, return an error
                if !res.success && self.die {
                    return Err(RunError::CommandFailed(format!(
                        "Command failed with exit code {}: {}",
                        res.code,
                        res.stderr.trim()
                    )));
                }
                // If the command failed but die is false, print a warning
                if !res.success && !self.die && !self.silent {
                    eprintln!("\x1b[33mWarning: Command failed with exit code {} but 'die' is false\x1b[0m", res.code);
                }
                Ok(res)
            }
            Err(e) => {
                // Print the error only if it's not a CommandFailed error
                // (which would already have printed the stderr)
                if !matches!(e, RunError::CommandFailed(_)) {
                    eprintln!("\x1b[31mCommand error: {}\x1b[0m", e);
                }

                if self.die {
                    Err(e)
                } else {
                    // Convert error to CommandResult with success=false
                    Ok(CommandResult {
                        stdout: String::new(),
                        stderr: format!("Error: {}", e),
                        success: false,
                        code: -1,
                    })
                }
            }
        }
    }
}

/// Creates a new `RunBuilder` for executing a command or script.
///
/// This is the primary entry point for command execution. It returns a builder that allows
/// configuring execution options before running the command.
///
/// # Arguments
///
/// * `cmd` - A command string or multiline script to execute
///
/// # Returns
///
/// Returns a `RunBuilder` that can be configured and executed.
///
/// # Example
///
/// ```no_run
/// use herolib_os::process::run;
///
/// let result = run("echo hello")
///     .silent(true)
///     .execute()?;
/// # Ok::<(), herolib_os::process::RunError>(())
/// ```
pub fn run(cmd: &str) -> RunBuilder<'_> {
    RunBuilder::new(cmd)
}

/// Runs a command or multiline script with default settings and returns the result.
///
/// This is a convenience function that immediately executes the command synchronously.
/// Output is captured and returned in the `CommandResult`. If the command fails,
/// an error is returned (unless you use `run()` with `die(false)`).
///
/// # Arguments
///
/// * `command` - A command string or multiline script to execute
///
/// # Returns
///
/// Returns `Ok(CommandResult)` with the output and exit code, or `Err(RunError)` if execution fails.
///
/// # Example
///
/// ```no_run
/// use herolib_os::process::run_command;
///
/// let result = run_command("ls -la")?;
/// println!("Output: {}", result.stdout);
/// println!("Success: {}", result.success);
/// # Ok::<(), herolib_os::process::RunError>(())
/// ```
pub fn run_command(command: &str) -> Result<CommandResult, RunError> {
    run(command).execute()
}

/// Runs a command or multiline script silently, suppressing all output.
///
/// This is a convenience function that executes the command synchronously without
/// displaying output. Output is still captured in the returned `CommandResult`.
///
/// # Arguments
///
/// * `command` - A command string or multiline script to execute
///
/// # Returns
///
/// Returns `Ok(CommandResult)` with the captured output and exit code,
/// or `Err(RunError)` if execution fails.
///
/// # Example
///
/// ```no_run
/// use herolib_os::process::run_silent;
///
/// let result = run_silent("curl https://api.example.com")?;
/// println!("Response: {}", result.stdout);
/// # Ok::<(), herolib_os::process::RunError>(())
/// ```
pub fn run_silent(command: &str) -> Result<CommandResult, RunError> {
    run(command).silent(true).execute()
}
