//! Regex pass that removes form-asymmetric location info from
//! compiler-error strings before they are fed back to the model.
//!
//! The intent (parent spec §strip_locations) is **symmetric
//! degradation**: both cohorts lose the localisation information
//! their compiler natively produces. The JSON cohort would
//! otherwise get JSON-pointer fragments; the AIL cohort would get
//! `at byte N` offsets. Neither survives this pass.

use regex::Regex;
use std::sync::OnceLock;

struct Patterns {
    json_pointer: Regex,
    byte_offset: Regex,
    line: Regex,
    column: Regex,
    line_col: Regex,
    file_prefix: Regex,
    caused_by_chain: Regex,
}

fn patterns() -> &'static Patterns {
    static P: OnceLock<Patterns> = OnceLock::new();
    P.get_or_init(|| Patterns {
        json_pointer: Regex::new(r"\$\.[A-Za-z0-9._\[\]]+").unwrap(),
        byte_offset: Regex::new(r"\bat byte \d+\b").unwrap(),
        line: Regex::new(r"\bline \d+\b").unwrap(),
        column: Regex::new(r"\bcolumn \d+\b").unwrap(),
        line_col: Regex::new(r"\bat \d+:\d+\b").unwrap(),
        file_prefix: Regex::new(r"(?m)^[^:\s]+:\d+:\d+:\s*").unwrap(),
        // Captures from "\nCaused by:" (inclusive) through end of string.
        caused_by_chain: Regex::new(r"(?s)\nCaused by:.*$").unwrap(),
    })
}

/// Strip form-asymmetric location info from a compiler-error string.
///
/// Order matters: collapse the anyhow `Caused by:` chain first so the
/// per-line regexes operate only on the leading message line.
pub fn strip_locations(s: &str) -> String {
    let p = patterns();
    let mut out = p.caused_by_chain.replace(s, "").into_owned();
    out = p.file_prefix.replace_all(&out, "").into_owned();
    out = p.json_pointer.replace_all(&out, "").into_owned();
    out = p.byte_offset.replace_all(&out, "").into_owned();
    out = p.line_col.replace_all(&out, "").into_owned();
    out = p.line.replace_all(&out, "").into_owned();
    out = p.column.replace_all(&out, "").into_owned();
    // Collapse runs of whitespace introduced by removed location tokens.
    let ws = Regex::new(r" {2,}").unwrap();
    ws.replace_all(out.trim_end(), " ").into_owned()
}

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

    #[test]
    fn json_pointer_is_removed() {
        let input = "type mismatch at $.defs[0].fn.body.app.fun";
        assert_eq!(strip_locations(input), "type mismatch at");
    }

    #[test]
    fn byte_offset_is_removed() {
        let input = "parse error: expected `)` (end of fn-def), got `(` at byte 28";
        assert_eq!(
            strip_locations(input),
            "parse error: expected `)` (end of fn-def), got `(`",
        );
    }

    #[test]
    fn line_and_column_are_removed() {
        let input = "schema/parse error: missing field `type` at line 7 column 3";
        let stripped = strip_locations(input);
        assert!(!stripped.contains("line "));
        assert!(!stripped.contains("column "));
        assert!(stripped.contains("missing field `type`"));
    }

    #[test]
    fn caused_by_chain_is_collapsed() {
        let input = "Error: top message\n\nCaused by:\n    0: lower message\n    1: even lower";
        assert_eq!(strip_locations(input), "Error: top message");
    }

    #[test]
    fn passthrough_when_no_locations() {
        let input = "error: [unbound-var] main: unknown identifier: `does_not_exist`";
        assert_eq!(strip_locations(input), input);
    }
}
