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

static COMPILING_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^   Compiling [^\n]+\n?").unwrap());
static DOWNLOADING_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^  Downloading [^\n]+\n?").unwrap());
static LOCKING_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^   Locking [^\n]+\n?").unwrap());
static FRESH_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^   Fresh [^\n]+\n?").unwrap());
static CHECKING_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^   Checking [^\n]+\n?").unwrap());
#[allow(dead_code)]
static TIMING_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^\s+Finished .+ in \d+\.\d+s\n?").unwrap());
static TEST_PASS_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^test [^\n]+ \.\.\. ok\n?").unwrap());
static TREE_DEDUP_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^([│├└─ ]*\w[^\n]*) \(\*\)\n?").unwrap());

// ── cargo build / check / clippy ─────────────────────────────────────────────

/// Compress `cargo build` / `cargo check` output.
/// On success: strip "Compiling" progress lines, keep warnings + summary.
/// On failure: strip progress, keep all error lines.
pub fn compress_build(raw: &str, _exit_code: i32) -> String {
    let cleaned = compactor::normalise(raw);
    let s = COMPILING_RE.replace_all(&cleaned, "");
    let s = CHECKING_RE.replace_all(&s, "");
    let s = FRESH_RE.replace_all(&s, "");
    let s = DOWNLOADING_RE.replace_all(&s, "");
    compactor::collapse_blanks(&s)
}

static CLIPPY_WARN_RULE_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"^warning: ([a-zA-Z_:]+)").unwrap());

/// Compress `cargo clippy` output.
/// Groups identical lint rules: "warning: clippy::unwrap_used (×14)" instead of 14 blocks.
/// Always keeps error lines and the final "N warnings emitted" summary.
pub fn compress_clippy(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = COMPILING_RE.replace_all(&cleaned, "");
    let s = CHECKING_RE.replace_all(&s, "");

    let mut rule_counts: HashMap<String, usize> = HashMap::new();
    let mut errors_and_summary: Vec<String> = Vec::new();
    let mut in_warning_block = false;
    let mut current_rule: Option<String> = None;

    for line in s.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            in_warning_block = false;
            continue;
        }
        // Error lines always kept
        if line.starts_with("error") {
            in_warning_block = false;
            current_rule = None;
            errors_and_summary.push(line.to_string());
            continue;
        }
        // Summary lines ("warning: N warnings emitted", "error: aborting due to N errors")
        if (line.starts_with("warning:") || line.starts_with("error:"))
            && (line.contains("warning") && line.ends_with("emitted")
                || line.contains("error") && line.contains("aborting"))
        {
            errors_and_summary.push(line.to_string());
            continue;
        }
        // Start of a new warning block
        if let Some(caps) = CLIPPY_WARN_RULE_RE.captures(line) {
            let rule = caps[1].to_string();
            *rule_counts.entry(rule.clone()).or_insert(0) += 1;
            current_rule = Some(rule);
            in_warning_block = true;
            continue;
        }
        // " --> src/file.rs:10:5" location line — include first occurrence only
        if in_warning_block && line.trim_start().starts_with("-->") {
            if let Some(ref rule) = current_rule {
                if rule_counts.get(rule).copied().unwrap_or(0) == 1 {
                    errors_and_summary.push(line.to_string());
                }
            }
            continue;
        }
        // note/help/= lines inside a warning block — skip
        if in_warning_block
            && (trimmed.starts_with('|')
                || trimmed.starts_with('=')
                || trimmed.starts_with("note:")
                || trimmed.starts_with("help:"))
        {
            continue;
        }
        // Not in a warning block — keep
        if !in_warning_block {
            errors_and_summary.push(line.to_string());
        }
    }

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

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

// ── cargo test ────────────────────────────────────────────────────────────────

/// Compress `cargo test` output (standard libtest format).
pub fn compress_test(raw: &str, exit_code: i32) -> String {
    // Detect nextest format: lines like "   PASS test_name 2ms" or "   FAIL test_name 5ms"
    if raw.contains("   PASS ") || raw.contains("   FAIL ") || raw.contains("   RETRY ") {
        return compress_nextest(raw, exit_code);
    }
    let cleaned = compactor::normalise(raw);
    let s = COMPILING_RE.replace_all(&cleaned, "");

    if exit_code == 0 {
        let summary: Vec<&str> = s
            .lines()
            .filter(|l| l.starts_with("test result") || l.starts_with("running "))
            .collect();
        return summary.join("\n");
    }
    let s = TEST_PASS_RE.replace_all(&s, "");
    compactor::collapse_blanks(&s)
}

// ── cargo nextest ─────────────────────────────────────────────────────────────

/// Compress `cargo nextest run` output.
/// nextest format: "   PASS test_name  0.123s" / "   FAIL test_name  0.456s"
/// On success: collapse all PASS lines to a count, keep Summary.
/// On failure: keep all FAIL lines + their stderr + Summary; strip PASS + timing.
pub fn compress_nextest(raw: &str, exit_code: i32) -> String {
    let cleaned = compactor::normalise(raw);
    let s = COMPILING_RE.replace_all(&cleaned, "");

    // Strip per-test timing prefix: "   [ 0.001s]" lines
    let timing_re = once_cell::sync::Lazy::force(&NEXTEST_TIMING_RE);
    let s = timing_re.replace_all(&s, "");

    // Handle `cargo nextest list` output
    if s.lines()
        .any(|l| l.contains(": test") || l.trim().ends_with("::"))
    {
        let tests: Vec<&str> = s
            .lines()
            .filter(|l| l.contains("::") && !l.trim().is_empty())
            .collect();
        if !tests.is_empty() {
            return format!("{} tests listed:\n{}", tests.len(), tests.join("\n"));
        }
    }

    let pass_count = s
        .lines()
        .filter(|l| l.trim_start().starts_with("PASS "))
        .count();

    if exit_code == 0 {
        let summary: Vec<&str> = s
            .lines()
            .filter(|l| {
                let t = l.trim_start();
                t.starts_with("Summary") || t.contains("passed") || t.contains("test run")
            })
            .collect();
        let summary_str = if summary.is_empty() {
            s.lines()
                .rfind(|l| !l.trim().is_empty())
                .unwrap_or("")
                .to_string()
        } else {
            summary.join("\n")
        };
        if pass_count > 0 {
            return format!("{pass_count} tests passed\n{summary_str}");
        }
        return summary_str;
    }

    // Failure: keep FAIL lines + the output block beneath each + Summary
    let mut out: Vec<&str> = Vec::new();
    let mut in_fail_block = false;
    for line in s.lines() {
        let t = line.trim_start();
        if t.starts_with("FAIL ") {
            in_fail_block = true;
            out.push(line);
            continue;
        }
        if t.starts_with("PASS ") {
            in_fail_block = false;
            continue;
        }
        if t.starts_with("Summary") || t.starts_with("error") {
            in_fail_block = false;
            out.push(line);
            continue;
        }
        // Retry indicator
        if t.starts_with("RETRY ") {
            continue;
        }
        if in_fail_block {
            out.push(line);
        }
    }
    if pass_count > 0 {
        out.insert(0, "");
    }
    let result = out.join("\n");
    if pass_count > 0 {
        return format!("{pass_count} passed (not shown)\n{result}");
    }
    compactor::collapse_blanks(&result)
}

static NEXTEST_TIMING_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^\s+\[\s*\d+\.\d+s\][^\n]*\n?").unwrap());

// ── cargo add / remove ────────────────────────────────────────────────────────

pub fn compress_add(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = DOWNLOADING_RE.replace_all(&cleaned, "");
    let s = LOCKING_RE.replace_all(&s, "");
    compactor::collapse_blanks(&s)
}

// ── cargo tree ────────────────────────────────────────────────────────────────

pub fn compress_tree(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let lines: Vec<&str> = cleaned.lines().collect();
    if lines.len() <= 60 {
        return cleaned;
    }
    let s = TREE_DEDUP_RE.replace_all(&cleaned, "");
    let remaining: Vec<&str> = s.lines().filter(|l| !l.trim().is_empty()).collect();
    if remaining.len() <= 80 {
        return remaining.join("\n");
    }
    format!(
        "{}\n... [{} more entries omitted] ...",
        remaining[..80].join("\n"),
        remaining.len() - 80
    )
}

// ── cargo update ─────────────────────────────────────────────────────────────

pub fn compress_update(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = LOCKING_RE.replace_all(&cleaned, "");
    compactor::collapse_blanks(&s)
}

// ── cargo fmt ────────────────────────────────────────────────────────────────

pub fn compress_fmt(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    if cleaned.trim().is_empty() {
        return "(fmt: no changes)".to_string();
    }
    let out: Vec<&str> = cleaned
        .lines()
        .filter(|l| l.starts_with("Diff in") || l.starts_with('+') || l.starts_with('-'))
        .collect();
    if out.is_empty() {
        cleaned
    } else {
        out.join("\n")
    }
}

// ── cargo bench ──────────────────────────────────────────────────────────────

pub fn compress_bench(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = COMPILING_RE.replace_all(&cleaned, "");
    let s = CHECKING_RE.replace_all(&s, "");
    // Keep: "test <name> ... bench: N ns/iter (+/- M)" lines + errors
    let out: Vec<&str> = s
        .lines()
        .filter(|l| l.contains("ns/iter") || l.contains("error") || l.starts_with("running"))
        .collect();
    if out.is_empty() {
        compactor::collapse_blanks(&s)
    } else {
        out.join("\n")
    }
}

// ── cargo doc ────────────────────────────────────────────────────────────────

pub fn compress_doc(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = COMPILING_RE.replace_all(&cleaned, "");
    let s = CHECKING_RE.replace_all(&s, "");
    // Keep "warning:" lines and "Documenting" progress (first + last only)
    let doc_lines: Vec<&str> = s
        .lines()
        .filter(|l| l.starts_with("   Documenting"))
        .collect();
    let other: Vec<&str> = s
        .lines()
        .filter(|l| !l.starts_with("   Documenting"))
        .collect();
    let doc_summary = if doc_lines.len() > 2 {
        format!("   Documenting [+{} crates] ...", doc_lines.len())
    } else {
        doc_lines.join("\n")
    };
    let rest = compactor::collapse_blanks(&other.join("\n"));
    if doc_summary.is_empty() {
        rest
    } else {
        format!("{doc_summary}\n{rest}")
    }
}

// ── cargo publish ─────────────────────────────────────────────────────────────

pub fn compress_publish(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = COMPILING_RE.replace_all(&cleaned, "");
    compactor::collapse_blanks(&s)
}

// ── cargo run ────────────────────────────────────────────────────────────────

pub fn compress_run(raw: &str, exit_code: i32) -> String {
    let cleaned = compactor::normalise(raw);
    // Strip build progress — preserve actual program output
    let s = COMPILING_RE.replace_all(&cleaned, "");
    let s = CHECKING_RE.replace_all(&s, "");
    let out = compactor::collapse_blanks(&s);
    // If failure with no content — pass original through
    if exit_code != 0 && out.trim().is_empty() {
        cleaned
    } else {
        out
    }
}

// ── top-level dispatcher ──────────────────────────────────────────────────────

pub fn compress_cargo(subcmd: &str, raw: &str, exit_code: i32) -> String {
    match subcmd {
        "build" | "b" => compress_build(raw, exit_code),
        "check" | "c" => compress_build(raw, exit_code),
        "clippy" => compress_clippy(raw),
        "test" | "t" => compress_test(raw, exit_code),
        "nextest" => compress_nextest(raw, exit_code),
        "add" => compress_add(raw),
        "remove" | "rm" => compress_add(raw),
        "tree" => compress_tree(raw),
        "update" => compress_update(raw),
        "fmt" => compress_fmt(raw),
        "bench" => compress_bench(raw),
        "doc" => compress_doc(raw),
        "publish" => compress_publish(raw),
        "run" | "r" => compress_run(raw, exit_code),
        _ => {
            let cleaned = compactor::normalise(raw);
            let s = COMPILING_RE.replace_all(&cleaned, "");
            compactor::collapse_blanks(&s)
        }
    }
}

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

    #[test]
    fn build_strips_compiling_lines() {
        let raw = "   Compiling foo v0.1.0\n   Compiling bar v1.2.3\nerror[E0412]: not found\n";
        let out = compress_build(raw, 1);
        assert!(!out.contains("Compiling foo"), "{out}");
        assert!(out.contains("error"));
    }

    #[test]
    fn build_success_keeps_finished_line() {
        let raw = "   Compiling foo v0.1.0\n    Finished dev [unoptimized] target(s) in 2.31s\n";
        let out = compress_build(raw, 0);
        assert!(!out.contains("Compiling"), "{out}");
        assert!(
            out.contains("Finished"),
            "Finished summary must survive: {out}"
        );
    }

    #[test]
    fn test_success_keeps_summary_only() {
        let raw = "running 3 tests\ntest foo ... ok\ntest bar ... ok\ntest baz ... ok\ntest result: ok. 3 passed\n";
        let out = compress_test(raw, 0);
        assert!(!out.contains("test foo ... ok"), "{out}");
        assert!(out.contains("test result"));
    }

    #[test]
    fn test_failure_strips_passing_keeps_fails() {
        let raw = "test foo ... ok\ntest bar ... FAILED\ntest result: FAILED. 1 failed\n";
        let out = compress_test(raw, 1);
        assert!(!out.contains("test foo ... ok"), "{out}");
        assert!(out.contains("FAILED"));
    }

    #[test]
    fn tree_truncates_large_output() {
        let raw = (0..100)
            .map(|i| format!("│   dep{i} v0.1.{i}"))
            .collect::<Vec<_>>()
            .join("\n");
        let out = compress_tree(&raw);
        assert!(out.contains("omitted"), "{out}");
    }

    #[test]
    fn update_strips_locking() {
        let raw =
            "   Locking 42 packages\n   Updating crates.io index\n   Updating foo v0.1 -> v0.2\n";
        let out = compress_update(raw);
        assert!(!out.contains("Locking"), "{out}");
        assert!(out.contains("Updating foo"));
    }

    #[test]
    fn fmt_no_changes_returns_message() {
        assert!(compress_fmt("").contains("no changes"));
    }

    #[test]
    fn clippy_groups_repeated_warnings() {
        let block = |rule: &str| {
            format!("warning: {rule}\n --> src/foo.rs:1:1\n  |\n1 | let x = 1;\n  |\n  = note: see docs\n")
        };
        let raw = format!(
            "{}{}{}",
            block("clippy::unwrap_used"),
            block("clippy::unwrap_used"),
            block("clippy::expect_used")
        );
        let out = compress_clippy(&raw);
        assert!(
            out.contains("×2") || out.contains("(×2)"),
            "should group repeated warning: {out}"
        );
        assert!(
            !out.contains("let x = 1"),
            "code snippet should be stripped: {out}"
        );
    }

    #[test]
    fn clippy_always_keeps_error_lines() {
        let raw = "   Checking foo v0.1.0\nerror[E0425]: cannot find value `x`\n --> src/lib.rs:5:9\nwarning: clippy::unwrap_used\n --> src/foo.rs:1:1\nwarning: 1 warning emitted\n";
        let out = compress_clippy(&raw);
        assert!(out.contains("E0425"), "{out}");
        assert!(!out.contains("Checking foo"), "{out}");
    }

    #[test]
    fn nextest_all_pass_collapses_to_summary() {
        let raw = "   Compiling foo v0.1.0\n   PASS foo::test_a  0.001s\n   PASS foo::test_b  0.002s\n   PASS foo::test_c  0.003s\nSummary [0.123s] 3 tests run: 3 passed, 0 failed\n";
        let out = compress_nextest(raw, 0);
        assert!(!out.contains("PASS foo::test_a"), "{out}");
        assert!(
            out.contains("3") && (out.contains("passed") || out.contains("Summary")),
            "{out}"
        );
    }

    #[test]
    fn nextest_failure_keeps_fail_lines() {
        let raw = "   PASS foo::test_a  0.001s\n   FAIL foo::test_b  0.100s\nthread 'main' panicked at 'assertion failed'\nSummary [0.200s] 2 tests run: 1 passed, 1 failed\n";
        let out = compress_nextest(raw, 1);
        assert!(out.contains("FAIL foo::test_b"), "{out}");
        assert!(!out.contains("PASS foo::test_a"), "{out}");
        assert!(out.contains("Summary") || out.contains("panicked"), "{out}");
    }

    #[test]
    fn nextest_strips_retry_lines() {
        let raw = "   RETRY foo::test_flaky  attempt 1\n   RETRY foo::test_flaky  attempt 2\n   FAIL foo::test_flaky  0.500s\nSummary [1.000s] 1 tests run: 0 passed, 1 failed\n";
        let out = compress_nextest(raw, 1);
        assert!(!out.contains("RETRY"), "{out}");
        assert!(out.contains("FAIL"), "{out}");
    }

    #[test]
    fn nextest_list_shows_test_names() {
        let raw =
            "foo::unit::test_a: test\nfoo::unit::test_b: test\nfoo::integration::test_c: test\n";
        let out = compress_nextest(raw, 0);
        assert!(out.contains("listed") || out.contains("test_a"), "{out}");
    }

    #[test]
    fn nextest_strips_timing_prefix_lines() {
        let raw = "      [ 0.001s] PASS foo::test_a\n      [ 0.002s] PASS foo::test_b\nSummary 2 passed\n";
        let out = compress_nextest(raw, 0);
        assert!(!out.contains("0.001s"), "{out}");
    }
}
