use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;

// "Inspecting N files" header
static INSPECTING_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^Inspecting \d+ files?\n?").unwrap());
// Offense location line: "path/to/file.rb:10:5: C: RuleName: message"
static OFFENSE_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^([^:]+):\d+:\d+: ([CWEF]): ([A-Za-z0-9:/]+): ").unwrap());
// Dot progress line (all dots + F markers)
static DOTS_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^[\.CFEW]+\n?").unwrap());
// "N files inspected, X offense(s) detected"
static SUMMARY_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^\d+ files? inspected[^\n]+\n?").unwrap());

// ── rubocop ───────────────────────────────────────────────────────────────────

pub fn compress_rubocop(raw: &str, exit_code: i32) -> String {
    let cleaned = compactor::normalise(raw);
    let s = INSPECTING_RE.replace_all(&cleaned, "");
    let s = DOTS_RE.replace_all(&s, "");

    if exit_code == 0 {
        // No offenses: keep only the summary line
        let summary: Vec<&str> = s.lines().filter(|l| SUMMARY_RE.is_match(l)).collect();
        if !summary.is_empty() {
            return summary.join("\n");
        }
        return "rubocop: no offenses detected".to_string();
    }

    // Failure: group offenses by cop name
    let mut cop_counts: HashMap<String, usize> = HashMap::new();
    let mut error_lines: Vec<String> = Vec::new();
    let mut summary_lines: Vec<String> = Vec::new();

    for line in s.lines() {
        if SUMMARY_RE.is_match(line) {
            summary_lines.push(line.to_string());
            continue;
        }
        if let Some(caps) = OFFENSE_RE.captures(line) {
            let severity = caps[2].to_string();
            let cop = caps[3].to_string();
            // Errors (E/F) always shown individually
            if severity == "E" || severity == "F" {
                error_lines.push(line.to_string());
            } else {
                *cop_counts.entry(cop).or_insert(0) += 1;
            }
            continue;
        }
        // Source/caret lines inside offense blocks — skip
        if line.starts_with("  ") {
            continue;
        }
        // Warnings summary or section headers — keep
        if !line.trim().is_empty() {
            error_lines.push(line.to_string());
        }
    }

    // Emit grouped offense lines
    let mut grouped: Vec<String> = Vec::new();
    for (cop, count) in &cop_counts {
        if *count > 1 {
            grouped.push(format!("{cop} (×{count})"));
        } else {
            grouped.push(cop.clone());
        }
    }
    grouped.sort();

    let mut result = grouped;
    result.extend(error_lines);
    result.extend(summary_lines);
    compactor::collapse_blanks(&result.join("\n"))
}

// ── bundler-audit ─────────────────────────────────────────────────────────────

pub fn compress_bundle_audit(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    // Keep only vulnerability lines + summary
    let out: Vec<&str> = cleaned
        .lines()
        .filter(|l| {
            let t = l.trim();
            t.starts_with("Name:")
                || t.starts_with("Version:")
                || t.starts_with("Advisory:")
                || t.starts_with("Criticality:")
                || t.starts_with("URL:")
                || t.starts_with("Title:")
                || t.contains("vulnerabilit")
                || t.contains("Vulnerabilities found")
                || t.contains("No vulnerabilities found")
                || t.starts_with("Insecure Source")
        })
        .collect();
    if out.is_empty() {
        cleaned
    } else {
        out.join("\n")
    }
}

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

    #[test]
    fn success_returns_summary_only() {
        let raw = "Inspecting 42 files\n..........................................\n42 files inspected, no offenses detected\n";
        let out = compress_rubocop(raw, 0);
        assert!(!out.contains("Inspecting"), "{out}");
        assert!(!out.contains("......"), "{out}");
        assert!(
            out.contains("no offenses") || out.contains("42 files"),
            "{out}"
        );
    }

    #[test]
    fn failure_groups_repeated_cops() {
        let block = |cop: &str, n: usize| -> String {
            (0..n)
                .map(|i| {
                    format!("app/foo.rb:{i}:1: C: {cop}: Message here\n  let x = 1\n      ^\n")
                })
                .collect()
        };
        let raw = format!(
            "{}{}",
            block("Style/FrozenStringLiteralComment", 8),
            block("Layout/TrailingWhitespace", 3)
        );
        let raw = format!(
            "Inspecting 2 files\n..FF..FF\n\n{raw}2 files inspected, 11 offenses detected\n"
        );
        let out = compress_rubocop(&raw, 1);
        assert!(
            out.contains("×8") || out.contains("(×8)"),
            "should group 8 occurrences: {out}"
        );
        assert!(
            !out.contains("let x = 1"),
            "source snippets should be stripped: {out}"
        );
    }

    #[test]
    fn bundle_audit_keeps_vulnerability_info() {
        let raw = "Name: rack\nVersion: 2.0.8\nAdvisory: CVE-2022-12345\nCriticality: High\nTitle: Remote code execution\nURL: https://nvd.nist.gov/...\n1 vulnerability found!\n";
        let out = compress_bundle_audit(raw);
        assert!(out.contains("CVE-2022-12345"), "{out}");
        assert!(out.contains("High"), "{out}");
    }
}
