//! Task backend abstraction for different execution environments
//!
//! This module provides a pluggable backend system for task execution.
//! The default backend is `Host`, which runs tasks directly on the host machine.
//! For Dagger container execution, use the `cuenv-dagger` crate.

use super::{Task, TaskResult};
use crate::OutputCapture;
use crate::config::BackendConfig;
use crate::environment::Environment;
use crate::{Error, Result};
use async_trait::async_trait;
use std::path::Path;
use std::process::Stdio;
use std::sync::Arc;
use tokio::process::Command;

/// Context for a single task execution, grouping all parameters
/// needed by [`TaskBackend::execute`].
pub struct TaskExecutionContext<'a> {
    /// Name of the task being executed
    pub name: &'a str,
    /// Task definition
    pub task: &'a Task,
    /// Environment variables for the task
    pub environment: &'a Environment,
    /// Root directory of the project
    pub project_root: &'a Path,
    /// Whether to capture or stream output
    pub capture_output: OutputCapture,
}

/// Trait for task execution backends
#[async_trait]
pub trait TaskBackend: Send + Sync {
    /// Execute a single task and return the result
    async fn execute(&self, ctx: &TaskExecutionContext<'_>) -> Result<TaskResult>;

    /// Get the name of the backend
    fn name(&self) -> &'static str;
}

/// Host backend - executes tasks directly on the host machine
pub struct HostBackend;

impl Default for HostBackend {
    fn default() -> Self {
        Self
    }
}

impl HostBackend {
    pub fn new() -> Self {
        Self
    }
}

#[async_trait]
impl TaskBackend for HostBackend {
    async fn execute(&self, ctx: &TaskExecutionContext<'_>) -> Result<TaskResult> {
        tracing::info!(
            task = %ctx.name,
            backend = "host",
            "Executing task on host"
        );

        let command_spec = ctx
            .task
            .command_spec(|command| ctx.environment.resolve_command(command))?;

        let mut cmd = Command::new(&command_spec.program);
        cmd.args(&command_spec.args);

        // Set working directory
        cmd.current_dir(ctx.project_root);

        // Set environment variables
        cmd.env_clear();
        for (k, v) in &ctx.environment.vars {
            cmd.env(k, v);
        }

        // Apply task-level env vars (plain values and passthrough from host)
        for (key, value) in &ctx.task.env {
            if let Some(s) = value.as_str() {
                if let Some(host_var) = super::output_refs::parse_passthrough(s) {
                    if let Ok(host_val) = std::env::var(host_var) {
                        cmd.env(key, host_val);
                    }
                } else if !s.starts_with("cuenv:ref:") {
                    cmd.env(key, s);
                }
            } else if let Some(n) = value.as_i64() {
                cmd.env(key, n.to_string());
            } else if let Some(b) = value.as_bool() {
                cmd.env(key, b.to_string());
            }
        }

        // Execute - always capture output for consistent behavior
        if ctx.capture_output.should_capture() {
            let output = cmd
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .output()
                .await
                .map_err(|e| Error::Io {
                    source: e,
                    path: None,
                    operation: format!("spawn task {}", ctx.name),
                })?;

            let stdout = String::from_utf8_lossy(&output.stdout).to_string();
            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
            let exit_code = output.status.code().unwrap_or(-1);
            let success = output.status.success();

            if !success {
                tracing::warn!(task = %ctx.name, exit = exit_code, "Task failed");
            }

            Ok(TaskResult {
                name: ctx.name.to_string(),
                exit_code: Some(exit_code),
                stdout,
                stderr,
                success,
            })
        } else {
            // Stream output directly to terminal (interactive mode)
            let status = cmd
                .stdout(Stdio::inherit())
                .stderr(Stdio::inherit())
                .status()
                .await
                .map_err(|e| Error::Io {
                    source: e,
                    path: None,
                    operation: format!("spawn task {}", ctx.name),
                })?;

            let exit_code = status.code().unwrap_or(-1);
            let success = status.success();

            if !success {
                tracing::warn!(task = %ctx.name, exit = exit_code, "Task failed");
            }

            Ok(TaskResult {
                name: ctx.name.to_string(),
                exit_code: Some(exit_code),
                stdout: String::new(), // Output went to terminal
                stderr: String::new(),
                success,
            })
        }
    }

    fn name(&self) -> &'static str {
        "host"
    }
}

/// Type alias for a backend factory function
pub type BackendFactory = fn(Option<&BackendConfig>, std::path::PathBuf) -> Arc<dyn TaskBackend>;

/// Create a backend based on configuration.
///
/// This function only handles the `host` backend. For `dagger` backend support,
/// use `create_backend_with_factory` and provide a factory from `cuenv-dagger`.
pub fn create_backend(
    config: Option<&BackendConfig>,
    project_root: std::path::PathBuf,
    cli_backend: Option<&str>,
) -> Arc<dyn TaskBackend> {
    create_backend_with_factory(config, project_root, cli_backend, None)
}

/// Create a backend with an optional factory for non-host backends.
///
/// The `dagger_factory` parameter should be `Some(cuenv_dagger::create_dagger_backend)`
/// when the dagger backend is available.
pub fn create_backend_with_factory(
    config: Option<&BackendConfig>,
    project_root: std::path::PathBuf,
    cli_backend: Option<&str>,
    dagger_factory: Option<BackendFactory>,
) -> Arc<dyn TaskBackend> {
    // CLI override takes precedence, then config, then default to host
    let backend_type = if let Some(b) = cli_backend {
        b.to_string()
    } else if let Some(c) = config {
        c.backend_type.clone()
    } else {
        "host".to_string()
    };

    match backend_type.as_str() {
        "dagger" => {
            if let Some(factory) = dagger_factory {
                factory(config, project_root)
            } else {
                tracing::error!(
                    "Dagger backend requested but not available. \
                     Add cuenv-dagger dependency to enable it. \
                     Falling back to host backend."
                );
                Arc::new(HostBackend::new())
            }
        }
        _ => Arc::new(HostBackend::new()),
    }
}

/// Check if the dagger backend should be used based on configuration
pub fn should_use_dagger(config: Option<&BackendConfig>, cli_backend: Option<&str>) -> bool {
    let backend_type = if let Some(b) = cli_backend {
        b
    } else if let Some(c) = config {
        &c.backend_type
    } else {
        "host"
    };

    backend_type == "dagger"
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_host_backend_new() {
        let backend = HostBackend::new();
        assert_eq!(backend.name(), "host");
    }

    #[test]
    fn test_host_backend_default() {
        let backend = HostBackend;
        assert_eq!(backend.name(), "host");
    }

    #[test]
    fn test_host_backend_name() {
        let backend = HostBackend;
        assert_eq!(backend.name(), "host");
    }

    #[test]
    fn test_should_use_dagger_cli_override_dagger() {
        // CLI override takes precedence
        assert!(should_use_dagger(None, Some("dagger")));
    }

    #[test]
    fn test_should_use_dagger_cli_override_host() {
        // CLI override to host
        assert!(!should_use_dagger(None, Some("host")));
    }

    #[test]
    fn test_should_use_dagger_config_dagger() {
        let config = BackendConfig {
            backend_type: "dagger".to_string(),
            options: None,
        };
        assert!(should_use_dagger(Some(&config), None));
    }

    #[test]
    fn test_should_use_dagger_config_host() {
        let config = BackendConfig {
            backend_type: "host".to_string(),
            options: None,
        };
        assert!(!should_use_dagger(Some(&config), None));
    }

    #[test]
    fn test_should_use_dagger_default() {
        // No config, no CLI - defaults to host
        assert!(!should_use_dagger(None, None));
    }

    #[test]
    fn test_should_use_dagger_cli_overrides_config() {
        let config = BackendConfig {
            backend_type: "dagger".to_string(),
            options: None,
        };
        // CLI override to host, even though config says dagger
        assert!(!should_use_dagger(Some(&config), Some("host")));
    }

    #[test]
    fn test_create_backend_defaults_to_host() {
        let backend = create_backend(None, std::path::PathBuf::from("."), None);
        assert_eq!(backend.name(), "host");
    }

    #[test]
    fn test_create_backend_with_cli_host() {
        let backend = create_backend(None, std::path::PathBuf::from("."), Some("host"));
        assert_eq!(backend.name(), "host");
    }

    #[test]
    fn test_create_backend_with_config_host() {
        let config = BackendConfig {
            backend_type: "host".to_string(),
            options: None,
        };
        let backend = create_backend(Some(&config), std::path::PathBuf::from("."), None);
        assert_eq!(backend.name(), "host");
    }

    #[test]
    fn test_create_backend_unknown_type_defaults_to_host() {
        let config = BackendConfig {
            backend_type: "unknown".to_string(),
            options: None,
        };
        let backend = create_backend(Some(&config), std::path::PathBuf::from("."), None);
        // Unknown backend types fall back to host
        assert_eq!(backend.name(), "host");
    }

    #[test]
    fn test_create_backend_dagger_without_factory() {
        let config = BackendConfig {
            backend_type: "dagger".to_string(),
            options: None,
        };
        // Without factory, dagger falls back to host
        let backend = create_backend(Some(&config), std::path::PathBuf::from("."), None);
        assert_eq!(backend.name(), "host");
    }

    #[test]
    fn test_create_backend_with_factory_dagger() {
        // Create a mock factory that returns a host backend (for testing)
        fn mock_dagger_factory(
            _config: Option<&BackendConfig>,
            _project_root: std::path::PathBuf,
        ) -> Arc<dyn TaskBackend> {
            Arc::new(HostBackend::new())
        }

        let config = BackendConfig {
            backend_type: "dagger".to_string(),
            options: None,
        };

        let backend = create_backend_with_factory(
            Some(&config),
            std::path::PathBuf::from("."),
            None,
            Some(mock_dagger_factory),
        );
        // The mock factory returns a host backend, but the factory was called
        assert_eq!(backend.name(), "host");
    }

    #[test]
    fn test_create_backend_with_factory_cli_overrides_to_dagger() {
        fn mock_dagger_factory(
            _config: Option<&BackendConfig>,
            _project_root: std::path::PathBuf,
        ) -> Arc<dyn TaskBackend> {
            Arc::new(HostBackend::new())
        }

        // CLI says dagger, even with no config
        let backend = create_backend_with_factory(
            None,
            std::path::PathBuf::from("."),
            Some("dagger"),
            Some(mock_dagger_factory),
        );
        assert_eq!(backend.name(), "host"); // Mock returns host
    }

    #[test]
    fn test_create_backend_with_factory_cli_overrides_config() {
        fn mock_dagger_factory(
            _config: Option<&BackendConfig>,
            _project_root: std::path::PathBuf,
        ) -> Arc<dyn TaskBackend> {
            Arc::new(HostBackend::new())
        }

        let config = BackendConfig {
            backend_type: "dagger".to_string(),
            options: None,
        };

        // CLI says host, config says dagger - CLI wins
        let backend = create_backend_with_factory(
            Some(&config),
            std::path::PathBuf::from("."),
            Some("host"),
            Some(mock_dagger_factory),
        );
        assert_eq!(backend.name(), "host");
    }

    #[test]
    fn test_backend_config_debug() {
        let config = BackendConfig {
            backend_type: "host".to_string(),
            options: None,
        };
        let debug_str = format!("{:?}", config);
        assert!(debug_str.contains("host"));
    }
}
