use crate::services::cli_process;
use crate::services::process_registry::ProcessRegistry;
use chrono::Duration as ChronoDuration;
use regex::Regex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
use tokio::sync::Mutex as AsyncMutex;
use tokio::time::{timeout, Duration};

const DEFAULT_EXHAUSTION_RETRY_AFTER_SECS: i64 = 60 * 60;
const MAX_EXHAUSTION_RETRY_AFTER_SECS: i64 = 7 * 24 * 60 * 60;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentExhaustionInfo {
    pub reason: String,
    pub retry_after: ChronoDuration,
}

impl AgentExhaustionInfo {
    pub fn exhausted_until_rfc3339(&self) -> String {
        (chrono::Utc::now() + self.retry_after).to_rfc3339()
    }
}

fn normalize_process_stderr(stderr: &str) -> String {
    let trimmed = stderr.trim();
    if trimmed.is_empty() {
        return String::new();
    }

    if trimmed.contains("Cannot start a runtime from within a runtime") {
        return "Le processus agent a plante avec un conflit de runtime Tokio. Orchai desactive MCP Tuleap sur les executions automatisees Codex ; relancez la tache. Si l'erreur persiste, mettez a jour ou desactivez le binaire local tuleap-mcp.".to_string();
    }

    trimmed.to_string()
}

fn parse_retry_after_seconds(message: &str) -> Option<i64> {
    let unit_patterns = [
        (
            Regex::new(r"(?i)(\d+)\s*(?:d|day|days|jour|jours)").expect("valid day regex"),
            86_400i64,
        ),
        (
            Regex::new(r"(?i)(\d+)\s*(?:h|hr|hrs|hour|hours|heure|heures)")
                .expect("valid hour regex"),
            3_600i64,
        ),
        (
            Regex::new(r"(?i)(\d+)\s*(?:m|min|mins|minute|minutes)").expect("valid minute regex"),
            60i64,
        ),
        (
            Regex::new(r"(?i)(\d+)\s*(?:s|sec|secs|second|seconds|seconde|secondes)")
                .expect("valid second regex"),
            1i64,
        ),
    ];

    let mut total_secs = 0i64;
    let mut matched_any_unit = false;

    for (regex, multiplier) in &unit_patterns {
        for caps in regex.captures_iter(message) {
            let Some(raw) = caps.get(1) else {
                continue;
            };
            let Ok(value) = raw.as_str().parse::<i64>() else {
                continue;
            };
            total_secs = total_secs.saturating_add(value.saturating_mul(*multiplier));
            matched_any_unit = true;
        }
    }

    if matched_any_unit && total_secs > 0 {
        return Some(total_secs.min(MAX_EXHAUSTION_RETRY_AFTER_SECS));
    }

    let explicit_retry_after_seconds = Regex::new(
        r"(?i)(?:retry|try again|next reset|available again|reessayez|réessayez|attendez)[^0-9]{0,24}(\d{1,6})\s*(?:s|sec|secs|second|seconds|seconde|secondes)",
    )
    .expect("valid explicit retry-after regex");

    let secs = explicit_retry_after_seconds
        .captures(message)
        .and_then(|caps| caps.get(1))
        .and_then(|raw| raw.as_str().parse::<i64>().ok())?;
    if secs <= 0 {
        return None;
    }
    Some(secs.min(MAX_EXHAUSTION_RETRY_AFTER_SECS))
}

pub fn detect_agent_exhaustion(error: &str) -> Option<AgentExhaustionInfo> {
    let reason = error.trim();
    if reason.is_empty() {
        return None;
    }

    let lowered = reason.to_lowercase();
    let exhaustion_markers = [
        "insufficient_quota",
        "quota exceeded",
        "exceeded your current quota",
        "usage limit",
        "token limit",
        "out of credits",
        "not enough credits",
        "credit balance",
        "rate limit",
        "too many requests",
        "status code: 429",
        "http 429",
        "plus de token",
        "tokens are exhausted",
        "limite de quota",
        "limite de taux",
    ];

    if !exhaustion_markers
        .iter()
        .any(|marker| lowered.contains(marker))
    {
        return None;
    }

    let retry_after_seconds =
        parse_retry_after_seconds(&lowered).unwrap_or(DEFAULT_EXHAUSTION_RETRY_AFTER_SECS);

    Some(AgentExhaustionInfo {
        reason: reason.to_string(),
        retry_after: ChronoDuration::seconds(retry_after_seconds),
    })
}

pub async fn run_agent_command(
    command: &str,
    args: &[String],
    prompt: &str,
    working_dir: &str,
    timeout_secs: u64,
) -> Result<String, String> {
    let mut child = Command::new(command)
        .args(args)
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .current_dir(working_dir)
        .spawn()
        .map_err(|e| format!("Failed to spawn '{}': {}", command, e))?;

    if let Some(mut stdin) = child.stdin.take() {
        stdin
            .write_all(prompt.as_bytes())
            .await
            .map_err(|e| format!("Failed to write prompt to stdin: {}", e))?;
    }

    let output = timeout(Duration::from_secs(timeout_secs), child.wait_with_output())
        .await
        .map_err(|_| format!("CLI command timed out after {}s", timeout_secs))?
        .map_err(|e| format!("Failed to wait process output: {}", e))?;

    if !output.status.success() {
        let stderr = normalize_process_stderr(&String::from_utf8_lossy(&output.stderr));
        let code = output.status.code().unwrap_or(-1);
        if stderr.is_empty() {
            return Err(format!("CLI command exited with code {}", code));
        }
        return Err(format!("CLI command exited with code {}: {}", code, stderr));
    }

    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if stdout.is_empty() {
        return Ok("(empty response)".to_string());
    }

    Ok(stdout)
}

pub async fn run_agent_command_for_task(
    command: &str,
    args: &[String],
    prompt: &str,
    working_dir: &str,
    timeout_secs: u64,
    process_registry: &ProcessRegistry,
    task_id: &str,
) -> Result<String, String> {
    let child = Command::new(command)
        .args(args)
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .current_dir(working_dir)
        .spawn()
        .map_err(|e| format!("Failed to spawn '{}': {}", command, e))?;
    let child = Arc::new(AsyncMutex::new(child));
    let cancellation_requested = Arc::new(AtomicBool::new(false));
    process_registry.register_task(task_id, child.clone(), cancellation_requested.clone());

    let (stdin, stdout, stderr) = {
        let mut child_guard = child.lock().await;
        let stdin = child_guard.stdin.take();
        let stdout = child_guard.stdout.take();
        let stderr = child_guard.stderr.take();
        (stdin, stdout, stderr)
    };

    if let Some(mut stdin) = stdin {
        stdin.write_all(prompt.as_bytes()).await.map_err(|e| {
            process_registry.unregister_task(task_id);
            format!("Failed to write prompt to stdin: {}", e)
        })?;
    }

    let stdout = stdout.ok_or_else(|| {
        process_registry.unregister_task(task_id);
        "Failed to capture stdout".to_string()
    })?;
    let stderr = stderr.ok_or_else(|| {
        process_registry.unregister_task(task_id);
        "Failed to capture stderr".to_string()
    })?;

    let read_future =
        cli_process::collect_process_output(child.clone(), stdout, stderr, |_| Ok(()));

    let (output, stderr_output, status) =
        match timeout(Duration::from_secs(timeout_secs), read_future).await {
            Ok(result) => match result {
                Ok(values) => values,
                Err(e) => {
                    process_registry.unregister_task(task_id);
                    return Err(e);
                }
            },
            Err(_) => {
                cancellation_requested.store(true, Ordering::SeqCst);
                {
                    let mut child_guard = child.lock().await;
                    let _ = child_guard.start_kill();
                }
                process_registry.unregister_task(task_id);
                return Err(format!("CLI command timed out after {}s", timeout_secs));
            }
        };

    process_registry.unregister_task(task_id);

    if cancellation_requested.load(Ordering::SeqCst) {
        return Err("CLI command cancelled".to_string());
    }

    if !status.success() {
        let stderr = normalize_process_stderr(&stderr_output);
        let code = status.code().unwrap_or(-1);
        if stderr.is_empty() {
            return Err(format!("CLI command exited with code {}", code));
        }
        return Err(format!("CLI command exited with code {}: {}", code, stderr));
    }

    let stdout = output.trim().to_string();
    if stdout.is_empty() {
        return Ok("(empty response)".to_string());
    }

    Ok(stdout)
}

pub async fn run_agent_command_streaming<F>(
    command: &str,
    args: &[String],
    prompt: &str,
    working_dir: &str,
    timeout_secs: u64,
    mut on_chunk: F,
) -> Result<String, String>
where
    F: FnMut(&str) -> Result<(), String>,
{
    let mut child = Command::new(command)
        .args(args)
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .current_dir(working_dir)
        .spawn()
        .map_err(|e| format!("Failed to spawn '{}': {}", command, e))?;

    if let Some(mut stdin) = child.stdin.take() {
        stdin
            .write_all(prompt.as_bytes())
            .await
            .map_err(|e| format!("Failed to write prompt to stdin: {}", e))?;
    }

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

    let stderr_task = tokio::spawn(async move {
        let mut stderr_reader = BufReader::new(stderr);
        let mut stderr_output = String::new();
        stderr_reader
            .read_to_string(&mut stderr_output)
            .await
            .map_err(|e| format!("Failed to read stderr: {}", e))?;
        Ok::<String, String>(stderr_output)
    });

    let read_future = async {
        let mut reader = BufReader::new(stdout).lines();
        let mut output = String::new();

        while let Ok(Some(line)) = reader.next_line().await {
            let chunk = if output.is_empty() {
                line
            } else {
                format!("\n{}", line)
            };
            on_chunk(&chunk)?;
            output.push_str(&chunk);
        }

        let status = child
            .wait()
            .await
            .map_err(|e| format!("Failed to wait for process: {}", e))?;

        let stderr_output = stderr_task
            .await
            .map_err(|e| format!("Failed to join stderr reader: {}", e))??;

        Ok::<(String, String, std::process::ExitStatus), String>((output, stderr_output, status))
    };

    let (output, stderr_output, status) = timeout(Duration::from_secs(timeout_secs), read_future)
        .await
        .map_err(|_| format!("CLI command timed out after {}s", timeout_secs))??;

    if !status.success() {
        let stderr = normalize_process_stderr(&stderr_output);
        let code = status.code().unwrap_or(-1);
        if stderr.is_empty() {
            return Err(format!("CLI command exited with code {}", code));
        }
        return Err(format!("CLI command exited with code {}: {}", code, stderr));
    }

    let stdout = output.trim().to_string();
    if stdout.is_empty() {
        return Ok("(empty response)".to_string());
    }

    Ok(stdout)
}

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

    #[test]
    fn test_normalize_process_stderr_runtime_panic() {
        let raw = "thread 'tokio-rt-worker' panicked\nCannot start a runtime from within a runtime";
        let normalized = normalize_process_stderr(raw);
        assert!(normalized.contains("conflit de runtime Tokio"));
    }

    #[test]
    fn test_normalize_process_stderr_passthrough() {
        let raw = "some other stderr";
        assert_eq!(normalize_process_stderr(raw), "some other stderr");
    }

    #[test]
    fn test_detect_agent_exhaustion_returns_none_for_unrelated_error() {
        let err = "CLI command exited with code 1: repository has local changes";
        assert!(detect_agent_exhaustion(err).is_none());
    }

    #[test]
    fn test_detect_agent_exhaustion_detects_quota_and_default_retry() {
        let err = "CLI command exited with code 1: insufficient_quota for this account";
        let exhaustion = detect_agent_exhaustion(err).expect("must detect exhaustion");
        assert_eq!(exhaustion.reason, err);
        assert_eq!(
            exhaustion.retry_after,
            ChronoDuration::seconds(DEFAULT_EXHAUSTION_RETRY_AFTER_SECS)
        );
    }

    #[test]
    fn test_detect_agent_exhaustion_parses_retry_window() {
        let err = "CLI command exited with code 1: Rate limit reached. Try again in 2h 30m 10s";
        let exhaustion = detect_agent_exhaustion(err).expect("must detect exhaustion");
        assert_eq!(exhaustion.retry_after, ChronoDuration::seconds(9_010));
    }

    #[tokio::test]
    async fn test_run_agent_command_streaming_collects_chunks() {
        let args = vec!["-c".to_string(), "cat".to_string()];
        let mut chunks = Vec::new();

        let output = run_agent_command_streaming("sh", &args, "hello\nworld\n", ".", 5, |chunk| {
            chunks.push(chunk.to_string());
            Ok(())
        })
        .await
        .expect("streaming command should succeed");

        assert_eq!(output, "hello\nworld");
        assert_eq!(chunks, vec!["hello".to_string(), "\nworld".to_string()]);
    }
}
