use anyhow::{Context, Result};
use libc;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use tokio::time::{sleep, timeout};

#[derive(Debug, Clone)]
pub struct RetryConfig {
    pub max_attempts: u32,
    pub initial_delay_ms: u64,
    pub max_delay_ms: u64,
    pub backoff_multiplier: f64,
}

impl Default for RetryConfig {
    fn default() -> Self {
        Self {
            max_attempts: 3,
            initial_delay_ms: 1000,
            max_delay_ms: 30000,
            backoff_multiplier: 2.0,
        }
    }
}

impl RetryConfig {
    pub fn new(max_attempts: u32) -> Self {
        Self {
            max_attempts,
            ..Default::default()
        }
    }

    pub fn with_delay(mut self, delay_ms: u64) -> Self {
        self.initial_delay_ms = delay_ms;
        self
    }

    pub fn with_max_delay(mut self, max_delay_ms: u64) -> Self {
        self.max_delay_ms = max_delay_ms;
        self
    }
}

pub fn validate_python_env() -> Result<()> {
    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let venv_python = manifest.join("venv").join("bin").join("python");

    if !venv_python.exists() {
        anyhow::bail!(
            "Python venv not found at {:?}\n\
             Run: /opt/homebrew/bin/python3.11 -m venv venv",
            venv_python
        );
    }

    let rt = tokio::runtime::Runtime::new()?;
    let output = rt
        .block_on(async { Command::new(&venv_python).arg("--version").output().await })
        .context("Failed to run Python")?;

    if !output.status.success() {
        anyhow::bail!("Python validation failed");
    }

    let version = String::from_utf8_lossy(&output.stdout);
    tracing::info!("Python version: {}", version.trim());

    if !version.contains("3.11") {
        tracing::warn!("Expected Python 3.11, got: {}", version.trim());
    }

    let script_path = manifest.join("scripts");
    if !script_path.exists() {
        anyhow::bail!("Scripts directory not found at {:?}", script_path);
    }

    tracing::info!("Python environment validated successfully");
    Ok(())
}

pub struct PythonExecutor {
    venv_python: PathBuf,
    scripts_dir: PathBuf,
}

impl PythonExecutor {
    pub fn new() -> Result<Self> {
        let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        let venv_python = manifest.join("venv").join("bin").join("python");
        let scripts_dir = manifest.join("scripts");

        if !venv_python.exists() {
            anyhow::bail!(
                "Python venv not found at {:?}. Run: /opt/homebrew/bin/python3.11 -m venv venv",
                venv_python
            );
        }

        if !scripts_dir.exists() {
            anyhow::bail!("Scripts directory not found at {:?}", scripts_dir);
        }

        Ok(Self {
            venv_python,
            scripts_dir,
        })
    }

    pub fn validate_env(&self) -> Result<()> {
        let rt = tokio::runtime::Runtime::new()?;
        let output = rt
            .block_on(async {
                Command::new(&self.venv_python)
                    .arg("--version")
                    .output()
                    .await
            })
            .context("Failed to run Python")?;

        if !output.status.success() {
            anyhow::bail!("Python validation failed");
        }

        let version = String::from_utf8_lossy(&output.stdout);
        if !version.contains("3.11") {
            tracing::warn!("Python version mismatch: {}", version);
        }

        Ok(())
    }

    pub async fn run(
        &self,
        script_name: &str,
        args: &[&str],
        uuid: Option<&str>,
        log_prefix: &str,
        timeout_duration: Option<Duration>,
    ) -> Result<()> {
        let script_path = self.scripts_dir.join(script_name);

        if !script_path.exists() {
            anyhow::bail!("Script not found: {:?}", script_path);
        }

        let mut cmd = Command::new(&self.venv_python);
        cmd.arg(&script_path);

        for arg in args {
            cmd.arg(arg);
        }

        if let Some(u) = uuid {
            cmd.arg("--uuid").arg(u);
        }

        cmd.stdout(Stdio::piped());
        cmd.stderr(Stdio::piped());
        cmd.kill_on_drop(true);
        // Create new process group for clean termination
        cmd.process_group(0);

        tracing::info!("[{}] Starting: {:?}", log_prefix, script_name);

        let mut child = cmd
            .spawn()
            .with_context(|| format!("Failed to run {}", script_name))?;
        let child_pid = child.id();

        let stdout = child.stdout.take().context("Failed to capture stdout")?;
        let stderr = child.stderr.take().context("Failed to capture stderr")?;

        let mut stdout_reader = BufReader::new(stdout).lines();
        let mut stderr_reader = BufReader::new(stderr).lines();

        let run_future = async {
            loop {
                tokio::select! {
                    line = stdout_reader.next_line() => {
                        match line {
                            Ok(Some(line)) => {
                                if line.starts_with(&format!("{}_", log_prefix)) {
                                    tracing::info!("[{}] {}", log_prefix, line);
                                }
                            }
                            Ok(None) => break,
                            Err(e) => tracing::warn!("[{}] stdout error: {}", log_prefix, e),
                        }
                    }
                    line = stderr_reader.next_line() => {
                        match line {
                            Ok(Some(line)) => {
                                if line.starts_with(&format!("{}_", log_prefix)) {
                                    tracing::info!("[{}] {}", log_prefix, line);
                                }
                            }
                            Ok(None) => {}
                            Err(e) => tracing::warn!("[{}] stderr error: {}", log_prefix, e),
                        }
                    }
                    status = child.wait() => {
                        match status {
                            Ok(status) => {
                                if !status.success() {
                                    tracing::error!("[{}] Process failed: {}", log_prefix, status);
                                    return Err(anyhow::anyhow!("{} exited with: {}", script_name, status));
                                }
                                tracing::info!("[{}] Completed successfully", log_prefix);
                            }
                            Err(e) => tracing::error!("[{}] wait error: {}", log_prefix, e),
                        }
                        break;
                    }
                }
            }
            Ok(())
        };

        if let Some(duration) = timeout_duration {
            match timeout(duration, run_future).await {
                Ok(Ok(())) => {}
                Ok(Err(e)) => return Err(e),
                Err(_) => {
                    // Try to kill the entire process group
                    if let Some(pid) = child_pid {
                        let pgid = pid as i32;
                        unsafe {
                            libc::killpg(pgid, libc::SIGKILL);
                        }
                    }
                    child.kill().await.context("Failed to kill process")?;
                    anyhow::bail!("{} timed out after {:?}", script_name, duration);
                }
            }
        } else {
            run_future.await?;
        }

        Ok(())
    }

    pub async fn run_with_output(
        &self,
        script_name: &str,
        args: &[&str],
        uuid: Option<&str>,
        log_prefix: &str,
        timeout_duration: Option<Duration>,
    ) -> Result<()> {
        self.run(script_name, args, uuid, log_prefix, timeout_duration)
            .await
    }

    pub async fn run_with_retry(
        &self,
        script_name: &str,
        args: &[&str],
        uuid: Option<&str>,
        log_prefix: &str,
        timeout_duration: Option<Duration>,
        retry_config: Option<RetryConfig>,
    ) -> Result<()> {
        let config = retry_config.unwrap_or_default();
        let mut attempt = 0;
        let mut delay_ms = config.initial_delay_ms;

        loop {
            attempt += 1;

            match self
                .run(script_name, args, uuid, log_prefix, timeout_duration)
                .await
            {
                Ok(()) => {
                    if attempt > 1 {
                        tracing::info!(
                            "[{}] Succeeded on attempt {}/{}",
                            log_prefix,
                            attempt,
                            config.max_attempts
                        );
                    }
                    return Ok(());
                }
                Err(e) => {
                    if attempt >= config.max_attempts {
                        tracing::error!(
                            "[{}] Failed after {} attempts: {}",
                            log_prefix,
                            attempt,
                            e
                        );
                        return Err(e);
                    }

                    tracing::warn!(
                        "[{}] Attempt {}/{} failed: {}. Retrying in {}ms...",
                        log_prefix,
                        attempt,
                        config.max_attempts,
                        e,
                        delay_ms
                    );

                    sleep(Duration::from_millis(delay_ms)).await;

                    delay_ms = (delay_ms as f64 * config.backoff_multiplier) as u64;
                    delay_ms = delay_ms.min(config.max_delay_ms);
                }
            }
        }
    }

    pub fn script_path(&self, script_name: &str) -> PathBuf {
        self.scripts_dir.join(script_name)
    }

    pub fn python_path(&self) -> &PathBuf {
        &self.venv_python
    }
}

impl Default for PythonExecutor {
    fn default() -> Self {
        Self::new().expect("Failed to create PythonExecutor")
    }
}

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

    #[test]
    fn test_python_executor_new_with_venv() {
        let executor = PythonExecutor::new();
        assert!(
            executor.is_ok(),
            "PythonExecutor should create successfully with venv"
        );
    }

    #[test]
    fn test_python_executor_paths() {
        let executor = PythonExecutor::new().unwrap();
        let python_path = executor.python_path();
        assert!(
            python_path.exists(),
            "Python path should exist: {:?}",
            python_path
        );
        assert!(
            python_path.to_string_lossy().contains("venv"),
            "Should be in venv"
        );
    }

    #[test]
    fn test_script_path() {
        let executor = PythonExecutor::new().unwrap();
        let script_path = executor.script_path("asr_processor.py");
        assert!(script_path.to_string_lossy().contains("scripts"));
        assert!(script_path.to_string_lossy().contains("asr_processor.py"));
    }

    #[test]
    fn test_script_path_nonexistent() {
        let executor = PythonExecutor::new().unwrap();
        let path = executor.script_path("nonexistent_script.py");
        assert!(!path.exists(), "Nonexistent script path should not exist");
    }

    #[test]
    fn test_python_path_is_executable() {
        let executor = PythonExecutor::new().unwrap();
        let path = executor.python_path();
        let metadata = std::fs::metadata(path);
        assert!(metadata.is_ok(), "Python path should be accessible");
    }

    #[test]
    fn test_retry_config_default() {
        let config = RetryConfig::default();
        assert_eq!(config.max_attempts, 3);
        assert_eq!(config.initial_delay_ms, 1000);
        assert_eq!(config.max_delay_ms, 30000);
        assert_eq!(config.backoff_multiplier, 2.0);
    }

    #[test]
    fn test_retry_config_builder() {
        let config = RetryConfig::new(5).with_delay(2000).with_max_delay(60000);
        assert_eq!(config.max_attempts, 5);
        assert_eq!(config.initial_delay_ms, 2000);
        assert_eq!(config.max_delay_ms, 60000);
    }

    #[tokio::test]
    async fn test_retry_config_clone() {
        let config = RetryConfig::default();
        let cloned = config.clone();
        assert_eq!(cloned.max_attempts, config.max_attempts);
    }
}
