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

static WATCH_STATUS_RE: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"(?m)^\[\d{2}:\d{2}:\d{2}\] (Starting compilation|File change detected)[^\n]*\n?")
        .unwrap()
});
static TSUP_PROGRESS_RE: Lazy<Regex> = Lazy::new(||
    // tsup: "DTS Build start", "DTS ⚡️ Build success" verbose lines
    Regex::new(r"(?m)^(DTS Build|CLI Building|ESM|CJS)\s[^\n]+\n?").unwrap());
static ESBUILD_META_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^\s+(node_modules/[^\s]+ \d+(\.\d+)? [km]b)\n?").unwrap());

// ── tsc ───────────────────────────────────────────────────────────────────────

pub fn compress_tsc(raw: &str, exit_code: i32) -> String {
    let cleaned = compactor::normalise(raw);
    // Strip watch mode timestamps
    let s = WATCH_STATUS_RE.replace_all(&cleaned, "");
    if exit_code == 0 {
        // On success, strip informational lines; keep any warnings
        let kept: Vec<&str> = s
            .lines()
            .filter(|l| l.contains("error TS") || l.contains("warning TS") || l.is_empty())
            .collect();
        let out = kept.join("\n");
        if out.trim().is_empty() {
            return "tsc: no errors".to_string();
        }
        return compactor::collapse_blanks(&out);
    }
    // On failure: keep error lines (file.ts(L,C): error TSxxxx: message)
    // Truncate at 60 errors
    let errors: Vec<&str> = s
        .lines()
        .filter(|l| l.contains("error TS") || l.contains("warning TS"))
        .take(60)
        .collect();
    let total: usize = s.lines().filter(|l| l.contains("error TS")).count();
    let mut out = errors.join("\n");
    if total > 60 {
        out.push_str(&format!(
            "\n... [{} more errors — run tsc to see all] ...",
            total - 60
        ));
    }
    out
}

// ── tsup / unbuild ────────────────────────────────────────────────────────────

pub fn compress_tsup(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = TSUP_PROGRESS_RE.replace_all(&cleaned, "");
    // Keep bundle size lines and errors
    let kept: Vec<&str> = s
        .lines()
        .filter(|l| {
            let l = l.trim();
            !l.is_empty()
                && (l.contains("dist/")
                    || l.contains("Built in")
                    || l.contains("error")
                    || l.starts_with("✓")
                    || l.starts_with("✗"))
        })
        .collect();
    if kept.is_empty() {
        return compactor::collapse_blanks(&s);
    }
    kept.join("\n")
}

// ── esbuild ───────────────────────────────────────────────────────────────────

pub fn compress_esbuild(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    // Strip per-module lines; keep summary + errors
    let s = ESBUILD_META_RE.replace_all(&cleaned, "");
    compactor::collapse_blanks(&s)
}

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

pub fn compress_typescript(prog: &str, subcmd: &str, raw: &str, exit_code: i32) -> String {
    match prog {
        "tsc" => compress_tsc(raw, exit_code),
        "tsup" | "unbuild" => compress_tsup(raw),
        "esbuild" => compress_esbuild(raw),
        _ => {
            // Generic: could be tsc invoked via npx, check subcmd
            if subcmd.contains("tsc") {
                return compress_tsc(raw, exit_code);
            }
            compactor::normalise(raw)
        }
    }
}

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

    #[test]
    fn tsc_success_no_errors_returns_clean_message() {
        let out = compress_tsc("", 0);
        assert!(out.contains("no errors") || out.is_empty(), "{out}");
    }

    #[test]
    fn tsc_failure_keeps_error_lines() {
        let raw = "src/main.ts(10,5): error TS2345: Argument of type 'string'\nsrc/lib.ts(3,1): error TS2304: Cannot find name\n";
        let out = compress_tsc(raw, 1);
        assert!(out.contains("TS2345"), "{out}");
        assert!(out.contains("TS2304"), "{out}");
    }

    #[test]
    fn tsc_truncates_many_errors() {
        let raw = (0..80)
            .map(|i| format!("src/f{i}.ts(1,1): error TS2304: msg\n"))
            .collect::<String>();
        let out = compress_tsc(&raw, 1);
        assert!(out.contains("more errors"), "{out}");
    }

    #[test]
    fn tsup_keeps_dist_lines() {
        let raw =
            "DTS Build start\ndist/index.js  42.1 kb\ndist/index.d.ts  8.2 kb\nBuilt in 1.2s\n";
        let out = compress_tsup(raw);
        assert!(out.contains("dist/"), "{out}");
    }
}
