//! # command-stream
//!
//! Modern shell command execution library with streaming, async iteration, and event support.
//!
//! This library provides a Rust equivalent to the JavaScript command-stream library,
//! offering powerful shell command execution with streaming capabilities.
//!
//! ## Features
//!
//! - Async command execution with tokio
//! - Streaming output via async iterators
//! - Event-based output handling (on, once, emit)
//! - Virtual commands for common operations (cat, ls, mkdir, etc.)
//! - Shell operator support (&&, ||, ;, |)
//! - Pipeline support with `.pipe()` method and `Pipeline` builder
//! - Global state management for shell settings
//! - `cmd!` macro for ergonomic command creation (similar to JS `$` tagged template literals)
//! - Cross-platform support
//!
//! ## Module Organization
//!
//! The codebase follows a modular architecture similar to the JavaScript implementation:
//!
//! - `ansi` - ANSI escape code handling utilities
//! - `commands` - Virtual command implementations
//! - `events` - Event emitter for stream events
//! - `macros` - The `cmd!` macro for ergonomic command creation
//! - `pipeline` - Pipeline execution support
//! - `quote` - Shell quoting utilities
//! - `shell_parser` - Shell command parsing
//! - `state` - Global state management
//! - `stream` - Async streaming and iteration support
//! - `trace` - Logging and tracing utilities
//! - `utils` - Command results and virtual command helpers
//!
//! ## Quick Start
//!
//! ```rust,no_run
//! use command_stream::{run, cmd};
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//!     // Execute a simple command
//!     let result = run("echo hello world").await?;
//!     println!("{}", result.stdout);
//!
//!     // Using the cmd! macro (similar to JS $ tagged template)
//!     let name = "world";
//!     let result = cmd!("echo hello {}", name).await?;
//!     println!("{}", result.stdout);
//!
//!     // Using pipelines
//!     use command_stream::Pipeline;
//!     let result = Pipeline::new()
//!         .add("echo hello world")
//!         .add("grep world")
//!         .run()
//!         .await?;
//!
//!     Ok(())
//! }
//! ```

// Modular utility modules (following JavaScript modular pattern)
pub mod ansi;
pub mod events;
#[doc(hidden)]
pub mod macros;
pub mod pipeline;
pub mod quote;
pub mod state;
pub mod stream;
pub mod trace;

// Core modules
pub mod commands;
pub mod shell_parser;
pub mod utils;

use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, Command};
use tokio::sync::mpsc;

pub use commands::{CommandContext, StreamChunk};
pub use shell_parser::{needs_real_shell, parse_shell_command, ParsedCommand};
pub use utils::{CommandResult, VirtualUtils};

// Re-export modular utilities at crate root for convenient access
pub use ansi::{AnsiConfig, AnsiUtils};
pub use events::{EventData, EventType, StreamEmitter};
pub use pipeline::{Pipeline, PipelineBuilder, PipelineExt};
pub use quote::quote;
pub use state::{
    get_shell_settings, global_state, reset_global_state, set_shell_option, unset_shell_option,
    GlobalState, ShellSettings,
};
pub use stream::{AsyncIterator, IntoStream, OutputChunk, OutputStream, StreamingRunner};
pub use trace::trace;

/// Resolve a working directory that is safe to spawn a child process in.
///
/// When no explicit cwd is requested the child normally inherits the parent's
/// working directory. But if that directory has been deleted or become
/// inaccessible (the "getcwd() failed" scenario from issue #44), inheriting it
/// makes the OS-level spawn fail. In that case fall back to a directory that is
/// known to exist so the command still runs.
///
/// Normal behavior is preserved: when an explicit cwd is given, or when the
/// inherited working directory is valid, this returns the requested value
/// (`None` meaning "inherit").
fn resolve_spawn_cwd(cwd: Option<&PathBuf>) -> Option<PathBuf> {
    // An explicit directory is always honored as-is.
    if let Some(c) = cwd {
        return Some(c.clone());
    }

    // No explicit cwd: we would inherit the parent's working directory. Make
    // sure that directory is actually usable before relying on inheritance.
    match std::env::current_dir() {
        Ok(_) => None,
        Err(e) => {
            let fallback = std::env::var_os("HOME")
                .or_else(|| std::env::var_os("USERPROFILE"))
                .map(PathBuf::from)
                .unwrap_or_else(std::env::temp_dir);
            trace(
                "ProcessRunner",
                &format!(
                    "current_dir() failed ({}); spawning in fallback directory {}",
                    e,
                    fallback.display()
                ),
            );
            if fallback.exists() {
                Some(fallback)
            } else {
                Some(std::env::temp_dir())
            }
        }
    }
}

/// Error type for command-stream operations
#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("Command failed with exit code {code}: {message}")]
    CommandFailed { code: i32, message: String },

    #[error("Command not found: {0}")]
    CommandNotFound(String),

    #[error("Parse error: {0}")]
    ParseError(String),

    #[error("Cancelled")]
    Cancelled,
}

/// Result type for command-stream operations
pub type Result<T> = std::result::Result<T, Error>;

/// Options for command execution
#[derive(Debug, Clone)]
pub struct RunOptions {
    /// Mirror output to parent stdout/stderr
    pub mirror: bool,
    /// Capture output in result
    pub capture: bool,
    /// Standard input handling
    pub stdin: StdinOption,
    /// Working directory
    pub cwd: Option<PathBuf>,
    /// Environment variables
    pub env: Option<HashMap<String, String>>,
    /// Interactive mode (TTY forwarding)
    pub interactive: bool,
    /// Enable shell operator parsing
    pub shell_operators: bool,
    /// Enable tracing for this command
    pub trace: bool,
}

impl Default for RunOptions {
    fn default() -> Self {
        RunOptions {
            mirror: true,
            capture: true,
            stdin: StdinOption::Inherit,
            cwd: None,
            env: None,
            interactive: false,
            shell_operators: true,
            trace: true,
        }
    }
}

/// Standard input options
#[derive(Debug, Clone)]
pub enum StdinOption {
    /// Inherit from parent process
    Inherit,
    /// Pipe (allow writing to stdin)
    Pipe,
    /// Provide string content
    Content(String),
    /// Null device
    Null,
}

/// A running or completed process
pub struct ProcessRunner {
    command: String,
    options: RunOptions,
    child: Option<Child>,
    result: Option<CommandResult>,
    started: bool,
    finished: bool,
    cancelled: bool,
    output_tx: Option<mpsc::Sender<StreamChunk>>,
    output_rx: Option<mpsc::Receiver<StreamChunk>>,
}

impl ProcessRunner {
    /// Create a new process runner
    pub fn new(command: impl Into<String>, options: RunOptions) -> Self {
        let (tx, rx) = mpsc::channel(1024);
        ProcessRunner {
            command: command.into(),
            options,
            child: None,
            result: None,
            started: false,
            finished: false,
            cancelled: false,
            output_tx: Some(tx),
            output_rx: Some(rx),
        }
    }

    /// Start the process
    pub async fn start(&mut self) -> Result<()> {
        if self.started {
            return Ok(());
        }
        self.started = true;

        utils::trace_lazy("ProcessRunner", || {
            format!("Starting command: {}", self.command)
        });

        // Check if this is a virtual command
        let first_word = self.command.split_whitespace().next().unwrap_or("");
        if let Some(result) = self.try_virtual_command(first_word).await {
            self.result = Some(result);
            self.finished = true;
            return Ok(());
        }

        // Parse command for shell operators (for future use with virtual command pipelines)
        let _parsed = if self.options.shell_operators && !needs_real_shell(&self.command) {
            parse_shell_command(&self.command)
        } else {
            None
        };

        // Execute via real shell if needed
        let shell = find_available_shell();

        let mut cmd = Command::new(&shell.cmd);
        for arg in &shell.args {
            cmd.arg(arg);
        }
        cmd.arg(&self.command);

        // Configure stdin
        match &self.options.stdin {
            StdinOption::Inherit => {
                cmd.stdin(Stdio::inherit());
            }
            StdinOption::Pipe => {
                cmd.stdin(Stdio::piped());
            }
            StdinOption::Content(_) => {
                cmd.stdin(Stdio::piped());
            }
            StdinOption::Null => {
                cmd.stdin(Stdio::null());
            }
        }

        // Configure stdout/stderr
        if self.options.capture || self.options.mirror {
            cmd.stdout(Stdio::piped());
            cmd.stderr(Stdio::piped());
        } else {
            cmd.stdout(Stdio::inherit());
            cmd.stderr(Stdio::inherit());
        }

        // Set working directory. Fall back to a valid directory when the
        // inherited working directory has been deleted (issue #44).
        if let Some(cwd) = resolve_spawn_cwd(self.options.cwd.as_ref()) {
            cmd.current_dir(cwd);
        }

        // Set environment
        if let Some(ref env_vars) = self.options.env {
            for (key, value) in env_vars {
                cmd.env(key, value);
            }
        }

        // Spawn the process
        let child = cmd.spawn()?;
        self.child = Some(child);

        Ok(())
    }

    /// Run the process to completion
    pub async fn run(&mut self) -> Result<CommandResult> {
        self.start().await?;

        if let Some(result) = &self.result {
            return Ok(result.clone());
        }

        let mut child = self.child.take().ok_or_else(|| {
            Error::Io(std::io::Error::new(
                std::io::ErrorKind::Other,
                "Process not started",
            ))
        })?;

        // Handle stdin content if provided
        if let StdinOption::Content(ref content) = self.options.stdin {
            if let Some(mut stdin) = child.stdin.take() {
                let content = content.clone();
                tokio::spawn(async move {
                    let _ = stdin.write_all(content.as_bytes()).await;
                    let _ = stdin.shutdown().await;
                });
            }
        }

        // Collect output
        let mut stdout_content = String::new();
        let mut stderr_content = String::new();

        if let Some(stdout) = child.stdout.take() {
            let mut reader = BufReader::new(stdout).lines();
            while let Ok(Some(line)) = reader.next_line().await {
                if self.options.mirror {
                    println!("{}", line);
                }
                stdout_content.push_str(&line);
                stdout_content.push('\n');
            }
        }

        if let Some(stderr) = child.stderr.take() {
            let mut reader = BufReader::new(stderr).lines();
            while let Ok(Some(line)) = reader.next_line().await {
                if self.options.mirror {
                    eprintln!("{}", line);
                }
                stderr_content.push_str(&line);
                stderr_content.push('\n');
            }
        }

        let status = child.wait().await?;
        let code = status.code().unwrap_or(-1);

        let result = CommandResult {
            stdout: stdout_content,
            stderr: stderr_content,
            code,
        };

        self.result = Some(result.clone());
        self.finished = true;

        Ok(result)
    }

    /// Try to execute as a virtual command
    async fn try_virtual_command(&self, cmd_name: &str) -> Option<CommandResult> {
        if !commands::are_virtual_commands_enabled() {
            return None;
        }

        // Parse args from command string
        let parts: Vec<&str> = self.command.split_whitespace().collect();
        let args: Vec<String> = parts.iter().skip(1).map(|s| s.to_string()).collect();

        let ctx = CommandContext {
            args,
            stdin: match &self.options.stdin {
                StdinOption::Content(s) => Some(s.clone()),
                _ => None,
            },
            cwd: self.options.cwd.clone(),
            env: self.options.env.clone(),
            output_tx: self.output_tx.clone(),
            is_cancelled: None,
        };

        match cmd_name {
            "echo" => Some(commands::echo(ctx).await),
            "pwd" => Some(commands::pwd(ctx).await),
            "cd" => Some(commands::cd(ctx).await),
            "true" => Some(commands::r#true(ctx).await),
            "false" => Some(commands::r#false(ctx).await),
            "sleep" => Some(commands::sleep(ctx).await),
            "cat" => Some(commands::cat(ctx).await),
            "ls" => Some(commands::ls(ctx).await),
            "mkdir" => Some(commands::mkdir(ctx).await),
            "rm" => Some(commands::rm(ctx).await),
            "touch" => Some(commands::touch(ctx).await),
            "cp" => Some(commands::cp(ctx).await),
            "mv" => Some(commands::mv(ctx).await),
            "basename" => Some(commands::basename(ctx).await),
            "dirname" => Some(commands::dirname(ctx).await),
            "env" => Some(commands::env(ctx).await),
            "exit" => Some(commands::exit(ctx).await),
            "which" => Some(commands::which(ctx).await),
            "yes" => Some(commands::yes(ctx).await),
            "seq" => Some(commands::seq(ctx).await),
            "test" => Some(commands::test(ctx).await),
            _ => None,
        }
    }

    /// Kill the process
    pub fn kill(&mut self) -> Result<()> {
        self.cancelled = true;
        if let Some(ref mut child) = self.child {
            child.start_kill()?;
        }
        Ok(())
    }

    /// Check if the process is finished
    pub fn is_finished(&self) -> bool {
        self.finished
    }

    /// Get the result if available
    pub fn result(&self) -> Option<&CommandResult> {
        self.result.as_ref()
    }

    /// Get the command string
    pub fn command(&self) -> &str {
        &self.command
    }

    /// Get the options
    pub fn options(&self) -> &RunOptions {
        &self.options
    }
}

/// Shell configuration
#[derive(Debug, Clone)]
struct ShellConfig {
    cmd: String,
    args: Vec<String>,
}

/// Find an available shell
fn find_available_shell() -> ShellConfig {
    let is_windows = cfg!(windows);

    if is_windows {
        // Windows shells
        let shells = [
            ("cmd.exe", vec!["/c"]),
            ("powershell.exe", vec!["-Command"]),
        ];

        for (cmd, args) in shells {
            if which::which(cmd).is_ok() {
                return ShellConfig {
                    cmd: cmd.to_string(),
                    args: args.into_iter().map(String::from).collect(),
                };
            }
        }

        ShellConfig {
            cmd: "cmd.exe".to_string(),
            args: vec!["/c".to_string()],
        }
    } else {
        // Unix shells
        let shells = [
            ("/bin/sh", vec!["-c"]),
            ("/usr/bin/sh", vec!["-c"]),
            ("/bin/bash", vec!["-c"]),
            ("sh", vec!["-c"]),
        ];

        for (cmd, args) in shells {
            if std::path::Path::new(cmd).exists() || which::which(cmd).is_ok() {
                return ShellConfig {
                    cmd: cmd.to_string(),
                    args: args.into_iter().map(String::from).collect(),
                };
            }
        }

        ShellConfig {
            cmd: "/bin/sh".to_string(),
            args: vec!["-c".to_string()],
        }
    }
}

/// Execute a command and return the result
///
/// This is the main entry point for simple command execution.
/// Named `run` instead of `$` since `$` is not a valid Rust identifier.
pub async fn run(command: impl Into<String>) -> Result<CommandResult> {
    let mut runner = ProcessRunner::new(command, RunOptions::default());
    runner.run().await
}

/// Alias for `run` function - for JavaScript-like API feel
/// Since `$` is not valid in Rust, this provides a similar short name
pub use run as execute;

/// Execute a command with custom options
pub async fn exec(command: impl Into<String>, options: RunOptions) -> Result<CommandResult> {
    let mut runner = ProcessRunner::new(command, options);
    runner.run().await
}

/// Create a new process runner without starting it
pub fn create(command: impl Into<String>, options: RunOptions) -> ProcessRunner {
    ProcessRunner::new(command, options)
}

/// Execute a command synchronously (blocking)
pub fn run_sync(command: impl Into<String>) -> Result<CommandResult> {
    let rt = tokio::runtime::Runtime::new()?;
    rt.block_on(run(command))
}

// Tests are located in tests/ directory for better organization
