//! Deep failure analysis for column lineage — categorize root causes.
//!
//! Run: cargo run --example failure_analysis --release

use altimate_core::lineage::column_lineage;
use altimate_core::schema::types::SqlDialect;
use serde::Deserialize;
use std::collections::HashMap;
use std::io::BufRead;
use std::path::Path;

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Case {
    name: String,
    sql: String,
    schema: serde_json::Value,
    expected: HashMap<String, Vec<String>>,
}

fn schema_from_json(value: &serde_json::Value) -> HashMap<String, serde_json::Value> {
    match value {
        serde_json::Value::Object(map) => map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
        _ => HashMap::new(),
    }
}

fn normalize(map: &HashMap<String, Vec<String>>) -> HashMap<String, Vec<String>> {
    map.iter()
        .map(|(k, v)| {
            let mut sorted = v.clone();
            sorted.sort();
            sorted.dedup();
            (k.clone(), sorted)
        })
        .collect()
}

fn normalize_to_short_names(map: &HashMap<String, Vec<String>>) -> HashMap<String, Vec<String>> {
    map.iter()
        .map(|(k, v)| {
            let normalized: Vec<String> = v
                .iter()
                .map(|ref_str| {
                    let parts: Vec<&str> =
                        ref_str.split('.').map(|p| p.trim_matches('"')).collect();
                    if parts.len() >= 3 {
                        let table = parts[parts.len() - 2];
                        let col = parts[parts.len() - 1];
                        format!("\"{table}\".\"{col}\"")
                    } else {
                        ref_str.clone()
                    }
                })
                .collect();
            let mut sorted = normalized;
            sorted.sort();
            sorted.dedup();
            (k.clone(), sorted)
        })
        .collect()
}

/// Extract table from "TABLE"."COL"
fn table_only(ref_str: &str) -> String {
    let parts: Vec<&str> = ref_str.split('.').map(|p| p.trim_matches('"')).collect();
    if parts.len() >= 2 {
        parts[parts.len() - 2].to_uppercase()
    } else {
        String::new()
    }
}

/// Extract column from "TABLE"."COL"
fn col_only(ref_str: &str) -> String {
    let parts: Vec<&str> = ref_str.split('.').map(|p| p.trim_matches('"')).collect();
    parts.last().unwrap_or(&"").to_uppercase()
}

fn main() {
    let fixture_path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../tests/fixtures/lineage/snowflake_ground_truth.jsonl");

    let file = std::fs::File::open(&fixture_path).expect("failed to open fixture");
    let reader = std::io::BufReader::with_capacity(1024 * 1024, file);

    let dialect = SqlDialect::Snowflake;

    let mut total = 0u64;
    let mut exact = 0u64;
    let mut parse_fail = 0u64;
    let mut empty_actual = 0u64;

    // Root cause categories for per-column mismatches
    let mut col_total = 0u64;
    let mut col_exact = 0u64;

    // Category: actual has table alias (short name) not in schema
    let mut alias_not_resolved = 0u64;
    // Category: column is alias (not in any schema table)
    let mut column_alias_not_traced = 0u64;
    // Category: right column, but mapped to wrong schema table
    let mut wrong_schema_table = 0u64;
    // Category: actual is superset of expected
    let mut superset = 0u64;
    // Category: actual is subset of expected (missing sources)
    let mut subset = 0u64;
    // Category: completely different (no overlap)
    let mut no_overlap = 0u64;
    // Category: expected column not in actual output
    let mut missing_output = 0u64;
    // Category: actual column not in expected (extra output)
    let mut extra_output = 0u64;

    // Track which tables are aliases (not in schema)
    let mut alias_tables: HashMap<String, u64> = HashMap::new();
    // Track column names that are aliases
    let mut alias_columns: HashMap<String, u64> = HashMap::new();

    for line in reader.lines() {
        let line = line.expect("failed to read line");
        let line = line.trim();
        if line.is_empty() {
            continue;
        }

        let case: Case = match serde_json::from_str(line) {
            Ok(c) => c,
            Err(_) => continue,
        };
        total += 1;

        let schema = schema_from_json(&case.schema);
        let schema_tables: Vec<String> = schema.keys().map(|k| k.to_uppercase()).collect();

        // Get all columns per table from schema
        let mut schema_columns: HashMap<String, Vec<String>> = HashMap::new();
        for (tbl, val) in &schema {
            if let serde_json::Value::Object(cols) = val {
                schema_columns.insert(
                    tbl.to_uppercase(),
                    cols.keys().map(|c| c.to_uppercase()).collect(),
                );
            }
        }

        let result = match column_lineage(&case.sql, dialect, &schema, None) {
            Ok(r) => r,
            Err(_) => {
                parse_fail += 1;
                continue;
            }
        };

        let actual = normalize_to_short_names(&result.column_dict);
        let expected = normalize(&case.expected);

        if actual == expected {
            exact += 1;
            // Count columns for exact matches
            for _ in expected.values() {
                col_total += 1;
                col_exact += 1;
            }
            continue;
        }

        if actual.is_empty() && !expected.is_empty() {
            empty_actual += 1;
            for _ in expected.values() {
                col_total += 1;
                missing_output += 1;
            }
            continue;
        }

        // Per-column analysis
        for (col, exp_sources) in &expected {
            col_total += 1;
            match actual.get(col) {
                Some(act_sources) if act_sources == exp_sources => {
                    col_exact += 1;
                }
                Some(act_sources) => {
                    // Check if actual is superset of expected
                    let all_expected_present = exp_sources.iter().all(|e| act_sources.contains(e));
                    let all_actual_in_expected =
                        act_sources.iter().all(|a| exp_sources.contains(a));

                    if all_expected_present && !all_actual_in_expected {
                        superset += 1;
                    } else if !all_expected_present && all_actual_in_expected {
                        subset += 1;
                    } else {
                        // Mixed: some right, some wrong, some missing
                        // Check if any actual sources use aliases (not in schema)
                        let mut has_alias_table = false;
                        let mut has_alias_column = false;

                        for act_src in act_sources {
                            let tbl = table_only(act_src);
                            let col_name = col_only(act_src);

                            if !tbl.is_empty()
                                && !schema_tables.iter().any(|s| s.eq_ignore_ascii_case(&tbl))
                            {
                                has_alias_table = true;
                                *alias_tables.entry(tbl.clone()).or_default() += 1;
                            }

                            // Check if column exists in any schema table
                            let col_exists_in_schema = schema_columns
                                .values()
                                .any(|cols| cols.iter().any(|c| c.eq_ignore_ascii_case(&col_name)));
                            if !col_exists_in_schema {
                                has_alias_column = true;
                                *alias_columns.entry(col_name.clone()).or_default() += 1;
                            }
                        }

                        if has_alias_table && !has_alias_column {
                            alias_not_resolved += 1;
                        } else if has_alias_column {
                            column_alias_not_traced += 1;
                        } else {
                            // Both actual and expected have schema tables, but wrong mapping
                            let exp_cols: Vec<String> =
                                exp_sources.iter().map(|s| col_only(s)).collect();
                            let act_cols: Vec<String> =
                                act_sources.iter().map(|s| col_only(s)).collect();
                            let overlap = exp_cols.iter().filter(|c| act_cols.contains(c)).count();
                            if overlap == 0 {
                                no_overlap += 1;
                            } else {
                                wrong_schema_table += 1;
                            }
                        }
                    }
                }
                None => {
                    missing_output += 1;
                }
            }
        }

        // Extra output columns
        for col in actual.keys() {
            if !expected.contains_key(col) {
                extra_output += 1;
            }
        }
    }

    println!("\n=== Query-Level Summary ===");
    println!("Total:          {total}");
    println!(
        "Exact match:    {exact} ({:.1}%)",
        exact as f64 / total as f64 * 100.0
    );
    println!("Parse fail:     {parse_fail}");
    println!("Empty actual:   {empty_actual}");
    println!(
        "Mismatched:     {}",
        total - exact - parse_fail - empty_actual
    );

    println!("\n=== Per-Column Root Cause Analysis ({col_total} columns) ===");
    println!(
        "Exact match:              {col_exact} ({:.1}%)",
        col_exact as f64 / col_total as f64 * 100.0
    );
    println!(
        "Alias table not resolved: {alias_not_resolved} (source uses alias like 'A' not in schema)"
    );
    println!(
        "Column alias not traced:  {column_alias_not_traced} (column name doesn't exist in schema)"
    );
    println!("Wrong schema table:       {wrong_schema_table} (right col name, wrong table)");
    println!("Superset (extra sources): {superset}");
    println!("Subset (missing sources): {subset}");
    println!("No overlap:               {no_overlap}");
    println!("Missing output column:    {missing_output}");
    println!("Extra output column:      {extra_output}");

    let fixable = alias_not_resolved + column_alias_not_traced;
    println!("\n=== Opportunity ===");
    println!(
        "Alias resolution fixable: {fixable} columns ({:.1}% of total)",
        fixable as f64 / col_total as f64 * 100.0
    );

    // Top alias tables
    let mut alias_vec: Vec<_> = alias_tables.into_iter().collect();
    alias_vec.sort_by(|a, b| b.1.cmp(&a.1));
    println!("\n=== Top 20 Unresolved Table Aliases ===");
    for (tbl, count) in alias_vec.iter().take(20) {
        println!("  {tbl}: {count}");
    }

    // Top alias columns (columns not in any schema table)
    let mut alias_col_vec: Vec<_> = alias_columns.into_iter().collect();
    alias_col_vec.sort_by(|a, b| b.1.cmp(&a.1));
    println!("\n=== Top 20 Unresolved Column Aliases ===");
    for (col, count) in alias_col_vec.iter().take(20) {
        println!("  {col}: {count}");
    }
}
