//! Compare projection-only vs complete lineage against ground truth.
//! If complete mode has more exact matches, the ground truth expects indirect lineage.

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

#[derive(Deserialize)]
struct Case {
    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(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()
}

/// Check if actual is a superset of expected (all expected present)
fn is_superset(
    actual: &HashMap<String, Vec<String>>,
    expected: &HashMap<String, Vec<String>>,
) -> bool {
    for (col, exp_sources) in expected {
        match actual.get(col) {
            None => return false,
            Some(act_sources) => {
                if !exp_sources.iter().all(|e| act_sources.contains(e)) {
                    return false;
                }
            }
        }
    }
    true
}

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("open");
    let reader = std::io::BufReader::with_capacity(1024 * 1024, file);
    let dialect = SqlDialect::Snowflake;

    let mut total = 0u64;
    let mut proj_exact = 0u64;
    let mut comp_exact = 0u64;
    let mut proj_superset = 0u64;
    let mut comp_superset = 0u64;

    // Queries where complete is exact but projection is not
    let mut comp_gains = 0u64;
    // Queries where projection is exact but complete is not
    let mut proj_better = 0u64;

    for line in reader.lines() {
        let line = line.unwrap();
        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 expected = normalize(&case.expected);

        let proj_result = match column_lineage(&case.sql, dialect, &schema, None) {
            Ok(r) => normalize_to_short(&r.column_dict),
            Err(_) => continue,
        };

        let comp_opts = ColumnLineageOptions {
            inject_shared: true,
            ..Default::default()
        };
        let comp_result = match column_lineage(&case.sql, dialect, &schema, Some(&comp_opts)) {
            Ok(r) => normalize_to_short(&r.column_dict),
            Err(_) => continue,
        };

        let proj_is_exact = proj_result == expected;
        let comp_is_exact = comp_result == expected;
        let proj_is_super = is_superset(&proj_result, &expected);
        let comp_is_super = is_superset(&comp_result, &expected);

        if proj_is_exact {
            proj_exact += 1;
        }
        if comp_is_exact {
            comp_exact += 1;
        }
        if proj_is_super {
            proj_superset += 1;
        }
        if comp_is_super {
            comp_superset += 1;
        }

        if comp_is_exact && !proj_is_exact {
            comp_gains += 1;
        }
        if proj_is_exact && !comp_is_exact {
            proj_better += 1;
        }
    }

    println!("Total:                    {total}");
    println!();
    println!("=== Exact Matches ===");
    println!(
        "Projection only:          {proj_exact} ({:.1}%)",
        proj_exact as f64 / total as f64 * 100.0
    );
    println!(
        "Complete (with shared):   {comp_exact} ({:.1}%)",
        comp_exact as f64 / total as f64 * 100.0
    );
    println!();
    println!("=== Superset-Inclusive ===");
    println!(
        "Projection superset:      {proj_superset} ({:.1}%)",
        proj_superset as f64 / total as f64 * 100.0
    );
    println!(
        "Complete superset:        {comp_superset} ({:.1}%)",
        comp_superset as f64 / total as f64 * 100.0
    );
    println!();
    println!("=== Head-to-Head ===");
    println!("Complete exact, proj not: {comp_gains}");
    println!("Projection exact, comp not: {proj_better}");
}
