pub mod cargo_build;
pub mod cargo_llvm_cov;
pub mod cargo_test;
pub mod docker_build;
pub mod dotnet;
pub mod eslint;
pub mod fallback;
pub mod git_log;
pub mod go_cover;
pub mod go_test;
pub mod gradle;
pub mod jest;
pub mod jest_cov;
pub mod kubectl;
pub mod kubectl_logs;
pub mod mvn;
pub mod mypy;
pub mod npm_ls;
pub mod pytest;
pub mod pytest_cov;
pub mod terraform;
pub mod tsc;

use serde::Serialize;
use std::time::Instant;

use crate::output::AgError;

#[derive(Serialize, serde::Deserialize, Debug)]
pub struct RunOutput {
    pub exit_code: i32,
    pub duration_ms: u64,
    pub tool: String,
    pub summary: String,
    pub passed: usize,
    pub failed: usize,
    pub skipped: usize,
    pub failures: Vec<Diagnostic>,
    pub warnings: Vec<Diagnostic>,
    pub output_lines: usize,
    pub output_tokens_saved: usize,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tail: Option<String>,
}

#[derive(Serialize, serde::Deserialize, Debug, Clone)]
pub struct Diagnostic {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub location: Option<String>,
    pub message: String,
}

pub struct RawOutput {
    pub exit_code: i32,
    pub stdout: String,
    pub stderr: String,
    pub duration_ms: u64,
}

pub fn execute(command: &[String], _timeout_secs: u64) -> Result<RawOutput, AgError> {
    if command.is_empty() {
        return Err(AgError::InvalidArgument {
            flag: "command".to_string(),
            message: "no command provided".to_string(),
        });
    }

    let start = Instant::now();
    let result = std::process::Command::new(&command[0])
        .args(&command[1..])
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn();

    let child = match result {
        Ok(c) => c,
        Err(e) => {
            return Err(AgError::Internal {
                message: format!("failed to spawn `{}`: {e}", command[0]),
            });
        }
    };

    let output = child.wait_with_output().map_err(|e| AgError::Internal {
        message: format!("failed to wait for command: {e}"),
    })?;

    let duration_ms = start.elapsed().as_millis() as u64;

    Ok(RawOutput {
        exit_code: output.status.code().unwrap_or(-1),
        stdout: String::from_utf8_lossy(&output.stdout).to_string(),
        stderr: String::from_utf8_lossy(&output.stderr).to_string(),
        duration_ms,
    })
}

pub fn detect_tool(command: &[String]) -> &'static str {
    let cmd = command.join(" ").to_lowercase();

    if cmd.contains("cargo llvm-cov") || cmd.contains("cargo-llvm-cov") {
        return "cargo_llvm_cov";
    }
    if cmd.starts_with("cargo test") {
        return "cargo_test";
    }
    if cmd.starts_with("cargo clippy") {
        return "cargo_clippy";
    }
    if cmd.starts_with("cargo build") || cmd.starts_with("cargo check") {
        return "cargo_build";
    }
    if cmd.contains("--coverage") || cmd.starts_with("c8 ") || cmd.starts_with("nyc ") {
        return "jest_cov";
    }
    if cmd.contains("--cov")
        || cmd.starts_with("coverage report")
        || cmd.starts_with("coverage run")
    {
        return "pytest_cov";
    }
    if cmd.contains("pytest") || cmd.contains("python -m pytest") {
        return "pytest";
    }
    if cmd.contains("-cover") || cmd.contains("-coverprofile") || cmd.starts_with("go tool cover") {
        return "go_cover";
    }
    if cmd.starts_with("go test") {
        return "go_test";
    }
    if cmd.contains("vitest") {
        return "jest";
    }
    if cmd.contains("jest") || cmd.starts_with("npm test") || cmd.starts_with("npx jest") {
        return "jest";
    }
    if cmd.starts_with("tsc") || cmd.starts_with("npx tsc") {
        return "tsc";
    }
    if cmd.contains("eslint") {
        return "eslint";
    }
    if cmd.contains("mypy") || cmd.contains("python -m mypy") {
        return "mypy";
    }
    if cmd.starts_with("git log") {
        return "git_log";
    }
    if cmd.starts_with("docker build") || cmd.starts_with("docker buildx") {
        return "docker_build";
    }
    if cmd.starts_with("terraform") {
        return "terraform";
    }
    if cmd.contains("kubectl logs") || cmd.contains("docker logs") {
        return "kubectl_logs";
    }
    if cmd.starts_with("kubectl") {
        return "kubectl";
    }
    if cmd.starts_with("mvn") || cmd.contains("mvnw") {
        return "mvn";
    }
    if cmd.starts_with("gradle") || cmd.contains("gradlew") {
        return "gradle";
    }
    if cmd.starts_with("dotnet") {
        return "dotnet";
    }
    if cmd.starts_with("npm list") || cmd.starts_with("npm ls") {
        return "npm_ls";
    }

    "unknown"
}

pub fn inject_json_flag(command: &[String]) -> Vec<String> {
    let tool = detect_tool(command);
    let flag = match tool {
        "kubectl" => Some("-o=json"),
        "terraform" => Some("-json"),
        "npm_ls" => Some("--json"),
        "eslint" => Some("--format=json"),
        "mypy" => Some("--output=json"),
        _ => None,
    };

    match flag {
        Some(f) => {
            let mut cmd = command.to_vec();
            if !cmd.iter().any(|a| a.contains("json")) {
                cmd.push(f.to_string());
            }
            cmd
        }
        None => command.to_vec(),
    }
}

pub fn parse_output(tool: &str, raw: &RawOutput) -> RunOutput {
    let combined = format!("{}\n{}", raw.stdout, raw.stderr);
    let output_lines = combined.lines().count();
    let raw_tokens = combined.len() / 4;

    let parsed = match tool {
        "cargo_llvm_cov" => cargo_llvm_cov::parse(&combined),
        "cargo_test" => cargo_test::parse(&combined),
        "cargo_build" | "cargo_clippy" => cargo_build::parse(&combined),
        "pytest_cov" => pytest_cov::parse(&combined),
        "pytest" => pytest::parse(&combined),
        "go_cover" => go_cover::parse(&combined),
        "go_test" => go_test::parse(&combined),
        "jest_cov" => jest_cov::parse(&combined),
        "jest" => jest::parse(&combined),
        "tsc" => tsc::parse(&combined),
        "eslint" => eslint::parse(&combined),
        "mypy" => mypy::parse(&combined),
        "git_log" => git_log::parse(&combined),
        "docker_build" => docker_build::parse(&combined),
        "terraform" => terraform::parse(&combined),
        "kubectl" => kubectl::parse(&combined),
        "kubectl_logs" => kubectl_logs::parse(&combined),
        "mvn" => mvn::parse(&combined),
        "gradle" => gradle::parse(&combined),
        "dotnet" => dotnet::parse(&combined),
        "npm_ls" => npm_ls::parse(&combined),
        _ => fallback::parse(&combined, raw.exit_code),
    };

    let parsed_tokens = estimate_parsed_tokens(&parsed);

    RunOutput {
        exit_code: raw.exit_code,
        duration_ms: raw.duration_ms,
        tool: tool.to_string(),
        summary: parsed.summary,
        passed: parsed.passed,
        failed: parsed.failed,
        skipped: parsed.skipped,
        failures: parsed.failures,
        warnings: parsed.warnings,
        output_lines,
        output_tokens_saved: raw_tokens.saturating_sub(parsed_tokens),
        tail: parsed.tail,
    }
}

fn estimate_parsed_tokens(parsed: &ParsedResult) -> usize {
    let mut total = parsed.summary.len();
    for d in &parsed.failures {
        total += d.name.len() + d.message.len() + d.location.as_ref().map_or(0, |l| l.len());
    }
    for d in &parsed.warnings {
        total += d.name.len() + d.message.len() + d.location.as_ref().map_or(0, |l| l.len());
    }
    if let Some(ref t) = parsed.tail {
        total += t.len();
    }
    total / 4
}

pub struct ParsedResult {
    pub summary: String,
    pub passed: usize,
    pub failed: usize,
    pub skipped: usize,
    pub failures: Vec<Diagnostic>,
    pub warnings: Vec<Diagnostic>,
    pub tail: Option<String>,
}

impl ParsedResult {
    /// Create a result with common fields. Sets `warnings` to empty and `tail` to `None`.
    pub fn new(
        summary: String,
        passed: usize,
        failed: usize,
        skipped: usize,
        failures: Vec<Diagnostic>,
    ) -> Self {
        Self {
            summary,
            passed,
            failed,
            skipped,
            failures,
            warnings: vec![],
            tail: None,
        }
    }
}

/// Declare a `static LazyLock<Regex>` in one line.
macro_rules! define_regex {
    ($name:ident, $pattern:expr) => {
        static $name: std::sync::LazyLock<regex::Regex> =
            std::sync::LazyLock::new(|| regex::Regex::new($pattern).unwrap());
    };
}
pub(crate) use define_regex;

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

    #[test]
    fn detect_cargo_test() {
        let cmd = vec!["cargo".into(), "test".into()];
        assert_eq!(detect_tool(&cmd), "cargo_test");
    }

    #[test]
    fn detect_cargo_clippy() {
        let cmd = vec!["cargo".into(), "clippy".into()];
        assert_eq!(detect_tool(&cmd), "cargo_clippy");
    }

    #[test]
    fn detect_pytest_cov() {
        let cmd = vec!["pytest".into(), "--cov=src".into()];
        assert_eq!(detect_tool(&cmd), "pytest_cov");
    }

    #[test]
    fn detect_pytest() {
        let cmd = vec!["pytest".into(), "-v".into()];
        assert_eq!(detect_tool(&cmd), "pytest");
    }

    #[test]
    fn detect_go_cover() {
        let cmd = vec!["go".into(), "test".into(), "-cover".into(), "./...".into()];
        assert_eq!(detect_tool(&cmd), "go_cover");
    }

    #[test]
    fn detect_coverage_report() {
        let cmd = vec!["coverage".into(), "report".into()];
        assert_eq!(detect_tool(&cmd), "pytest_cov");
    }

    #[test]
    fn detect_go_test() {
        let cmd = vec!["go".into(), "test".into(), "./...".into()];
        assert_eq!(detect_tool(&cmd), "go_test");
    }

    #[test]
    fn detect_jest_cov() {
        let cmd = vec!["jest".into(), "--coverage".into()];
        assert_eq!(detect_tool(&cmd), "jest_cov");
    }

    #[test]
    fn detect_jest() {
        let cmd = vec!["npx".into(), "jest".into()];
        assert_eq!(detect_tool(&cmd), "jest");
    }

    #[test]
    fn detect_vitest() {
        let cmd = vec!["npx".into(), "vitest".into()];
        assert_eq!(detect_tool(&cmd), "jest");
    }

    #[test]
    fn detect_npm_test() {
        let cmd = vec!["npm".into(), "test".into()];
        assert_eq!(detect_tool(&cmd), "jest");
    }

    #[test]
    fn detect_tsc() {
        let cmd = vec!["tsc".into(), "--noEmit".into()];
        assert_eq!(detect_tool(&cmd), "tsc");
    }

    #[test]
    fn detect_eslint() {
        let cmd = vec!["npx".into(), "eslint".into(), "src/".into()];
        assert_eq!(detect_tool(&cmd), "eslint");
    }

    #[test]
    fn detect_cargo_llvm_cov() {
        let cmd = vec!["cargo".into(), "llvm-cov".into(), "--lib".into()];
        assert_eq!(detect_tool(&cmd), "cargo_llvm_cov");
    }

    #[test]
    fn detect_mypy() {
        let cmd = vec!["mypy".into(), "src/".into()];
        assert_eq!(detect_tool(&cmd), "mypy");
    }

    #[test]
    fn detect_git_log() {
        let cmd = vec!["git".into(), "log".into(), "--oneline".into()];
        assert_eq!(detect_tool(&cmd), "git_log");
    }

    #[test]
    fn detect_docker_build() {
        let cmd = vec!["docker".into(), "build".into(), ".".into()];
        assert_eq!(detect_tool(&cmd), "docker_build");
    }

    #[test]
    fn detect_terraform() {
        let cmd = vec!["terraform".into(), "plan".into()];
        assert_eq!(detect_tool(&cmd), "terraform");
    }

    #[test]
    fn detect_kubectl() {
        let cmd = vec![
            "kubectl".into(),
            "describe".into(),
            "pod".into(),
            "myapp".into(),
        ];
        assert_eq!(detect_tool(&cmd), "kubectl");
    }

    #[test]
    fn detect_kubectl_logs() {
        let cmd = vec!["kubectl".into(), "logs".into(), "myapp".into()];
        assert_eq!(detect_tool(&cmd), "kubectl_logs");
    }

    #[test]
    fn detect_mvn() {
        let cmd = vec!["mvn".into(), "test".into()];
        assert_eq!(detect_tool(&cmd), "mvn");
    }

    #[test]
    fn detect_gradle() {
        let cmd = vec!["gradle".into(), "build".into()];
        assert_eq!(detect_tool(&cmd), "gradle");
    }

    #[test]
    fn detect_dotnet() {
        let cmd = vec!["dotnet".into(), "build".into()];
        assert_eq!(detect_tool(&cmd), "dotnet");
    }

    #[test]
    fn detect_npm_ls() {
        let cmd = vec!["npm".into(), "ls".into()];
        assert_eq!(detect_tool(&cmd), "npm_ls");
    }

    #[test]
    fn detect_unknown() {
        let cmd = vec!["echo".into(), "hello".into()];
        assert_eq!(detect_tool(&cmd), "unknown");
    }

    #[test]
    fn execute_echo() {
        let raw = execute(&["echo".into(), "hello".into()], 10).unwrap();
        assert_eq!(raw.exit_code, 0);
        assert!(raw.stdout.contains("hello"));
        assert!(raw.duration_ms < 5000);
    }

    #[test]
    fn execute_failing() {
        let raw = execute(&["false".into()], 10).unwrap();
        assert_ne!(raw.exit_code, 0);
    }

    #[test]
    fn execute_nonexistent() {
        let result = execute(&["ag_nonexistent_binary_xyz".into()], 10);
        assert!(result.is_err());
    }

    #[test]
    fn execute_empty_command() {
        let result = execute(&[], 10);
        assert!(result.is_err());
    }

    #[test]
    fn parse_output_fallback() {
        let raw = RawOutput {
            exit_code: 0,
            stdout: "hello\nworld\n".to_string(),
            stderr: String::new(),
            duration_ms: 10,
        };
        let output = parse_output("unknown", &raw);
        assert_eq!(output.tool, "unknown");
        assert!(output.tail.is_some());
    }

    #[test]
    fn parse_output_cargo_test() {
        let raw = RawOutput {
            exit_code: 0,
            stdout: "test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s\n".to_string(),
            stderr: String::new(),
            duration_ms: 10,
        };
        let output = parse_output("cargo_test", &raw);
        assert_eq!(output.passed, 5);
        assert_eq!(output.failed, 0);
    }

    #[test]
    fn inject_json_kubectl() {
        let cmd = vec!["kubectl".into(), "get".into(), "pods".into()];
        let result = inject_json_flag(&cmd);
        assert!(result.iter().any(|a| a.contains("json")));
    }

    #[test]
    fn inject_json_terraform() {
        let cmd = vec!["terraform".into(), "plan".into()];
        let result = inject_json_flag(&cmd);
        assert!(result.contains(&"-json".to_string()));
    }

    #[test]
    fn inject_json_npm_ls() {
        let cmd = vec!["npm".into(), "ls".into()];
        let result = inject_json_flag(&cmd);
        assert!(result.contains(&"--json".to_string()));
    }

    #[test]
    fn inject_json_unknown_tool_unchanged() {
        let cmd = vec!["echo".into(), "hello".into()];
        let result = inject_json_flag(&cmd);
        assert_eq!(result, cmd);
    }

    #[test]
    fn inject_json_skips_if_already_present() {
        let cmd = vec![
            "kubectl".into(),
            "get".into(),
            "pods".into(),
            "-o=json".into(),
        ];
        let result = inject_json_flag(&cmd);
        assert_eq!(result.iter().filter(|a| a.contains("json")).count(), 1);
    }

    #[test]
    fn tokens_saved_positive() {
        let raw = RawOutput {
            exit_code: 0,
            stdout: "test a ... ok\ntest b ... ok\ntest c ... ok\ntest result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s\n".to_string(),
            stderr: String::new(),
            duration_ms: 10,
        };
        let output = parse_output("cargo_test", &raw);
        assert!(output.output_tokens_saved > 0);
    }
}
