use std::path::Path;

use compliance_core::models::{Finding, ScanType, Severity};
use compliance_core::traits::{ScanOutput, Scanner};
use compliance_core::CoreError;

use crate::pipeline::dedup;

pub struct GitleaksScanner;

impl Scanner for GitleaksScanner {
    fn name(&self) -> &str {
        "gitleaks"
    }

    fn scan_type(&self) -> ScanType {
        ScanType::SecretDetection
    }

    #[tracing::instrument(skip_all)]
    async fn scan(&self, repo_path: &Path, repo_id: &str) -> Result<ScanOutput, CoreError> {
        let output = tokio::process::Command::new("gitleaks")
            .args([
                "detect",
                "--source",
                ".",
                "--report-format",
                "json",
                "--report-path",
                "/dev/stdout",
                "--no-banner",
                "--exit-code",
                "0",
            ])
            .current_dir(repo_path)
            .output()
            .await
            .map_err(|e| CoreError::Scanner {
                scanner: "gitleaks".to_string(),
                source: Box::new(e),
            })?;

        if output.stdout.is_empty() {
            return Ok(ScanOutput::default());
        }

        let results: Vec<GitleaksResult> =
            serde_json::from_slice(&output.stdout).unwrap_or_default();

        let findings = results
            .into_iter()
            .filter(|r| !is_allowlisted(&r.file))
            .map(|r| {
                let severity = match r.rule_id.as_str() {
                    s if s.contains("private-key") => Severity::Critical,
                    s if s.contains("token") || s.contains("password") || s.contains("secret") => {
                        Severity::High
                    }
                    s if s.contains("api-key") => Severity::High,
                    _ => Severity::Medium,
                };

                let fingerprint = dedup::compute_fingerprint(&[
                    repo_id,
                    &r.rule_id,
                    &r.file,
                    &r.start_line.to_string(),
                ]);

                let title = format!("Secret detected: {}", r.description);
                let description = format!(
                    "Potential secret ({}) found in {}:{}. Match: {}",
                    r.rule_id,
                    r.file,
                    r.start_line,
                    r.r#match.chars().take(80).collect::<String>(),
                );

                let mut finding = Finding::new(
                    repo_id.to_string(),
                    fingerprint,
                    "gitleaks".to_string(),
                    ScanType::SecretDetection,
                    title,
                    description,
                    severity,
                );
                finding.rule_id = Some(r.rule_id);
                finding.file_path = Some(r.file);
                finding.line_number = Some(r.start_line);
                finding.code_snippet = Some(r.r#match);
                finding
            })
            .collect();

        Ok(ScanOutput {
            findings,
            sbom_entries: Vec::new(),
        })
    }
}

/// Skip files that commonly contain example/placeholder secrets
fn is_allowlisted(file_path: &str) -> bool {
    let lower = file_path.to_lowercase();
    lower.ends_with(".env.example")
        || lower.ends_with(".env.sample")
        || lower.ends_with(".env.template")
        || lower.contains("/test/")
        || lower.contains("/tests/")
        || lower.contains("/fixtures/")
        || lower.contains("/testdata/")
        || lower.contains("mock")
        || lower.ends_with("_test.go")
        || lower.ends_with(".test.ts")
        || lower.ends_with(".test.js")
        || lower.ends_with(".spec.ts")
        || lower.ends_with(".spec.js")
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
struct GitleaksResult {
    description: String,
    #[serde(rename = "RuleID")]
    rule_id: String,
    file: String,
    start_line: u32,
    #[serde(rename = "Match")]
    r#match: String,
}
