use std::path::Path;

use compliance_core::models::{Finding, ScanType, Severity};
use compliance_core::CoreError;
use tokio::process::Command;

use crate::pipeline::dedup;

use super::run_with_timeout;

pub(super) async fn run_clippy(repo_path: &Path, repo_id: &str) -> Result<Vec<Finding>, CoreError> {
    let child = Command::new("cargo")
        .args([
            "clippy",
            "--message-format=json",
            "--quiet",
            "--",
            "-W",
            "clippy::all",
        ])
        .current_dir(repo_path)
        .env("RUSTC_WRAPPER", "")
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .map_err(|e| CoreError::Scanner {
            scanner: "clippy".to_string(),
            source: Box::new(e),
        })?;

    let output = run_with_timeout(child, "clippy").await?;
    let stdout = String::from_utf8_lossy(&output.stdout);
    let mut findings = Vec::new();

    for line in stdout.lines() {
        let msg: serde_json::Value = match serde_json::from_str(line) {
            Ok(v) => v,
            Err(_) => continue,
        };

        if msg.get("reason").and_then(|v| v.as_str()) != Some("compiler-message") {
            continue;
        }

        let message = match msg.get("message") {
            Some(m) => m,
            None => continue,
        };

        let level = message.get("level").and_then(|v| v.as_str()).unwrap_or("");

        if level != "warning" && level != "error" {
            continue;
        }

        let text = message
            .get("message")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();

        let code = message
            .get("code")
            .and_then(|v| v.get("code"))
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();

        if text.starts_with("aborting due to") || code.is_empty() {
            continue;
        }

        let (file_path, line_number) = extract_primary_span(message);

        let severity = if level == "error" {
            Severity::High
        } else {
            Severity::Low
        };

        let fingerprint = dedup::compute_fingerprint(&[
            repo_id,
            "clippy",
            &code,
            &file_path,
            &line_number.to_string(),
        ]);

        let mut finding = Finding::new(
            repo_id.to_string(),
            fingerprint,
            "clippy".to_string(),
            ScanType::Lint,
            format!("[clippy] {text}"),
            text,
            severity,
        );
        finding.rule_id = Some(code);
        if !file_path.is_empty() {
            finding.file_path = Some(file_path);
        }
        if line_number > 0 {
            finding.line_number = Some(line_number);
        }
        findings.push(finding);
    }

    Ok(findings)
}

fn extract_primary_span(message: &serde_json::Value) -> (String, u32) {
    let spans = match message.get("spans").and_then(|v| v.as_array()) {
        Some(s) => s,
        None => return (String::new(), 0),
    };

    for span in spans {
        if span.get("is_primary").and_then(|v| v.as_bool()) == Some(true) {
            let file = span
                .get("file_name")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();
            let line = span.get("line_start").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
            return (file, line);
        }
    }

    (String::new(), 0)
}

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

    #[test]
    fn extract_primary_span_with_primary() {
        let msg = serde_json::json!({
            "spans": [
                {
                    "file_name": "src/lib.rs",
                    "line_start": 42,
                    "is_primary": true
                }
            ]
        });
        let (file, line) = extract_primary_span(&msg);
        assert_eq!(file, "src/lib.rs");
        assert_eq!(line, 42);
    }

    #[test]
    fn extract_primary_span_no_primary() {
        let msg = serde_json::json!({
            "spans": [
                {
                    "file_name": "src/lib.rs",
                    "line_start": 42,
                    "is_primary": false
                }
            ]
        });
        let (file, line) = extract_primary_span(&msg);
        assert_eq!(file, "");
        assert_eq!(line, 0);
    }

    #[test]
    fn extract_primary_span_multiple_spans() {
        let msg = serde_json::json!({
            "spans": [
                {
                    "file_name": "src/other.rs",
                    "line_start": 10,
                    "is_primary": false
                },
                {
                    "file_name": "src/main.rs",
                    "line_start": 99,
                    "is_primary": true
                }
            ]
        });
        let (file, line) = extract_primary_span(&msg);
        assert_eq!(file, "src/main.rs");
        assert_eq!(line, 99);
    }

    #[test]
    fn extract_primary_span_no_spans() {
        let msg = serde_json::json!({});
        let (file, line) = extract_primary_span(&msg);
        assert_eq!(file, "");
        assert_eq!(line, 0);
    }

    #[test]
    fn extract_primary_span_empty_spans() {
        let msg = serde_json::json!({ "spans": [] });
        let (file, line) = extract_primary_span(&msg);
        assert_eq!(file, "");
        assert_eq!(line, 0);
    }

    #[test]
    fn parse_clippy_compiler_message_line() {
        let line = r#"{"reason":"compiler-message","message":{"level":"warning","message":"unused variable","code":{"code":"unused_variables"},"spans":[{"file_name":"src/main.rs","line_start":5,"is_primary":true}]}}"#;
        let msg: serde_json::Value = serde_json::from_str(line).unwrap();

        assert_eq!(
            msg.get("reason").and_then(|v| v.as_str()),
            Some("compiler-message")
        );
        let message = msg.get("message").unwrap();
        assert_eq!(
            message.get("level").and_then(|v| v.as_str()),
            Some("warning")
        );
        assert_eq!(
            message.get("message").and_then(|v| v.as_str()),
            Some("unused variable")
        );
        assert_eq!(
            message
                .get("code")
                .and_then(|v| v.get("code"))
                .and_then(|v| v.as_str()),
            Some("unused_variables")
        );

        let (file, line_num) = extract_primary_span(message);
        assert_eq!(file, "src/main.rs");
        assert_eq!(line_num, 5);
    }

    #[test]
    fn skip_non_compiler_message() {
        let line = r#"{"reason":"build-script-executed","package_id":"foo 0.1.0"}"#;
        let msg: serde_json::Value = serde_json::from_str(line).unwrap();
        assert_ne!(
            msg.get("reason").and_then(|v| v.as_str()),
            Some("compiler-message")
        );
    }

    #[test]
    fn skip_aborting_message() {
        let text = "aborting due to 3 previous errors";
        assert!(text.starts_with("aborting due to"));
    }
}
