//! Diagnose lineage mismatches — categorize by root cause.
//!
//! Run: cargo run --example diagnose_lineage

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>>,
    source_query_type: Option<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 just the column name from "TABLE"."COL" → 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 col_name_match = 0u64; // right column, wrong table
    let mut extra_sources_only = 0u64; // has all expected + some extra
    let mut missing_sources_only = 0u64; // missing expected sources
    let mut extra_cols_only = 0u64; // extra output columns
    let mut empty_actual = 0u64;
    let mut other = 0u64;

    // Per-column stats
    let mut col_total = 0u64;
    let mut col_exact = 0u64;
    let mut col_right_col_wrong_table = 0u64;
    let mut col_superset = 0u64; // actual is superset of expected
    let mut col_extra_only = 0u64;
    let mut col_missing = 0u64;

    let mut samples_superset = 0u64;
    let mut samples_wrong_table = 0u64;

    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 result = match column_lineage(&case.sql, dialect, &schema, None) {
            Ok(r) => r,
            Err(_) => continue,
        };

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

        if actual == expected {
            exact += 1;
            continue;
        }

        if actual.is_empty() && !expected.is_empty() {
            empty_actual += 1;
            continue;
        }

        // Per-column analysis
        let mut query_has_superset = false;
        let mut query_has_wrong_table = false;
        let mut query_has_extra_cols = false;
        let mut query_has_missing = false;

        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));
                    if all_expected_present {
                        col_superset += 1;
                        query_has_superset = true;
                    } else {
                        // Check if column names match but tables differ
                        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 col_overlap = exp_cols.iter().filter(|c| act_cols.contains(c)).count();
                        if col_overlap > 0 {
                            col_right_col_wrong_table += 1;
                            query_has_wrong_table = true;
                            if samples_wrong_table < 10 {
                                samples_wrong_table += 1;
                                println!(
                                    "WRONG_TABLE: {} col '{}': exp={:?} act={:?}",
                                    case.name, col, exp_sources, act_sources
                                );
                                println!("  SQL(120): {}", &case.sql[..case.sql.len().min(120)]);
                            }
                        } else {
                            col_extra_only += 1;
                        }
                    }
                }
                None => {
                    col_missing += 1;
                    query_has_missing = true;
                }
            }
        }

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

        // Categorize query
        if query_has_superset && !query_has_wrong_table && !query_has_missing {
            extra_sources_only += 1;
            if samples_superset < 5 {
                samples_superset += 1;
                println!("\nSUPERSET: {}", case.name);
                for (col, exp) in expected.iter().take(2) {
                    let act = actual.get(col).cloned().unwrap_or_default();
                    let extra: Vec<_> = act.iter().filter(|s| !exp.contains(s)).collect();
                    println!("  col '{}': extra={:?}", col, extra);
                }
                println!("  SQL (80): {}", &case.sql[..case.sql.len().min(80)]);
            }
        } else if query_has_wrong_table {
            col_name_match += 1;
        } else if query_has_missing {
            missing_sources_only += 1;
        } else if query_has_extra_cols {
            extra_cols_only += 1;
        } else {
            other += 1;
            if other <= 5 {
                println!("\nOTHER: {}", case.name);
                for (col, exp) in expected.iter().take(2) {
                    let act = actual.get(col).cloned().unwrap_or_default();
                    println!("  col '{}': exp={:?} act={:?}", col, exp, act);
                }
                println!("  SQL(120): {}", &case.sql[..case.sql.len().min(120)]);
            }
        }
    }

    println!("\n=== Query-Level Mismatch Categories ===");
    println!("Total:              {total}");
    println!(
        "Exact match:        {exact} ({:.1}%)",
        exact as f64 / total as f64 * 100.0
    );
    println!("Empty actual:       {empty_actual}");
    println!("Superset (extra):   {extra_sources_only} (has all expected + extras)");
    println!("Wrong table:        {col_name_match} (right col, wrong table name)");
    println!("Missing sources:    {missing_sources_only}");
    println!("Extra columns:      {extra_cols_only}");
    println!("Other:              {other}");

    println!("\n=== Per-Column Stats (across {} columns) ===", col_total);
    println!("Exact:              {col_exact}");
    println!("Superset:           {col_superset} (actual has all expected + extra)");
    println!("Right col wrong tbl:{col_right_col_wrong_table}");
    println!("Extra only:         {col_extra_only}");
    println!("Missing in actual:  {col_missing}");

    let effective_match = exact as f64 + extra_sources_only as f64;
    println!("\n=== If we accept superset matches ===");
    println!(
        "Match rate: {:.1}% ({}/{})",
        effective_match / total as f64 * 100.0,
        effective_match as u64,
        total
    );
}
