use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;

// Spinner/progress lines: " ✔ Loaded image" / " ✔ Cataloged packages"
static PROGRESS_RE: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"(?m)^\s*[✔✘⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s+(?:Loaded|Cataloged|Indexed|Scanned|Tagged)[^\n]*\n?")
        .unwrap()
});
// Table separator lines: "├──────┤" / "╞══════╡"
static TABLE_SEP_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^[├╞╘╒╔╠║╚╝╗╬╫╪┼─═╟╞]+[^\n]*\n?").unwrap());

const SEVERITY_ORDER: &[&str] = &["Critical", "High", "Medium", "Low", "Negligible"];

// ── compress grype output ─────────────────────────────────────────────────────

pub fn compress_grype(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = PROGRESS_RE.replace_all(&cleaned, "");
    let s = TABLE_SEP_RE.replace_all(&s, "");

    let mut critical: Vec<&str> = Vec::new();
    let mut high: Vec<&str> = Vec::new();
    let mut medium: Vec<&str> = Vec::new();
    let mut low_count = 0usize;
    let mut negligible_count = 0usize;
    let mut summary_line: Option<&str> = None;
    let mut header: Option<&str> = None;

    for line in s.lines() {
        let t = line.trim();
        if t.is_empty() {
            continue;
        }
        // Summary line
        if t.contains("vulnerabilit") && (t.contains("found") || t.contains("Fixed")) {
            summary_line = Some(line);
            continue;
        }
        // Table header
        if t.starts_with("NAME") && t.contains("VULNERABILITY") {
            header = Some(line);
            continue;
        }
        // Classify by severity column (last word on the row)
        let severity = SEVERITY_ORDER
            .iter()
            .find(|&&s| t.contains(s))
            .copied()
            .unwrap_or("");
        match severity {
            "Critical" => critical.push(line),
            "High" => high.push(line),
            "Medium" if medium.len() < 10 => medium.push(line),
            "Low" => low_count += 1,
            "Negligible" => negligible_count += 1,
            _ => {}
        }
    }

    let mut out: Vec<String> = Vec::new();
    if let Some(h) = header {
        out.push(h.to_string());
    }
    out.extend(critical.iter().map(|l| l.to_string()));
    out.extend(high.iter().map(|l| l.to_string()));
    out.extend(medium.iter().map(|l| l.to_string()));
    if low_count > 0 || negligible_count > 0 {
        out.push(format!(
            "  … {} Low + {} Negligible vulnerabilities (suppressed)",
            low_count, negligible_count
        ));
    }
    if let Some(sum) = summary_line {
        out.push(String::new());
        out.push(sum.to_string());
    }

    if out.is_empty() {
        return compactor::collapse_blanks(&s);
    }
    out.join("\n")
}

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

    #[test]
    fn strips_progress_and_keeps_critical() {
        let raw = " ✔ Loaded image\n ✔ Cataloged packages\nNAME       INSTALLED  FIXED-IN  TYPE  VULNERABILITY  SEVERITY\nlog4j-core  2.14.0    2.17.0   java  CVE-2021-44228  Critical\ncommons-io  2.6       2.7      java  CVE-2021-29425  Low\n\n2 vulnerabilities found\n";
        let out = compress_grype(raw);
        assert!(!out.contains("Loaded image"), "{out}");
        assert!(out.contains("CVE-2021-44228"), "{out}");
        assert!(out.contains("Low") || out.contains("suppressed"), "{out}");
        assert!(out.contains("vulnerabilit"), "{out}");
    }

    #[test]
    fn suppresses_low_and_negligible_with_count() {
        let mut lines =
            vec!["NAME  INSTALLED  FIXED-IN  TYPE  VULNERABILITY  SEVERITY".to_string()];
        for i in 0..5 {
            lines.push(format!("pkg-{i}  1.0  1.1  deb  CVE-2021-{i:05}  Low"));
        }
        for i in 0..3 {
            lines.push(format!(
                "pkg2-{i}  1.0  1.1  deb  CVE-2022-{i:05}  Negligible"
            ));
        }
        let out = compress_grype(&lines.join("\n"));
        assert!(out.contains("suppressed"), "{out}");
        assert!(out.contains('5'), "{out}");
        assert!(out.contains('3'), "{out}");
    }

    #[test]
    fn no_vulns_passthrough() {
        let raw = " ✔ Loaded image\n ✔ Cataloged packages\nNo vulnerabilities found\n";
        let out = compress_grype(raw);
        assert!(
            out.contains("No vulnerabilities") || out.is_empty(),
            "{out}"
        );
    }
}
