//! Eyeball debugger: pick one ground truth case, print SQL + expected + actual.
//!
//! Usage:
//!   cargo run --example eyeball --release                     # first failing case
//!   cargo run --example eyeball --release -- <case_name>      # specific case by name
//!   cargo run --example eyeball --release -- --category wrong_sources  # first of category
//!   cargo run --example eyeball --release -- --index 5        # 5th failing case

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

#[derive(Debug, Deserialize)]
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, BTreeSet<String>> {
    map.iter()
        .map(|(k, v)| {
            let set: BTreeSet<String> = v.iter().cloned().collect();
            (k.clone(), set)
        })
        .collect()
}

fn classify(
    actual: &HashMap<String, BTreeSet<String>>,
    expected: &HashMap<String, BTreeSet<String>>,
) -> &'static str {
    let has_extra_cols = actual.keys().any(|k| !expected.contains_key(k));
    let has_missing_cols = expected.keys().any(|k| !actual.contains_key(k));
    let has_wrong_sources = expected.keys().any(|k| {
        actual.get(k).map_or(false, |a| {
            let e = expected.get(k).unwrap();
            a != e
        })
    });

    match (has_extra_cols, has_missing_cols, has_wrong_sources) {
        (true, false, false) => "only_extra_output_cols",
        (false, true, false) => "only_missing_output_cols",
        (false, false, true) => "wrong_sources",
        (true, false, true) => "extra_cols+wrong_sources",
        (false, true, true) => "missing_cols+wrong_sources",
        (true, true, _) => "mixed",
        _ => "unknown",
    }
}

fn main() {
    let args: Vec<String> = std::env::args().collect();

    // Parse arguments
    let mut target_name: Option<String> = None;
    let mut target_category: Option<String> = None;
    let mut target_index: usize = 0;
    let mut i = 1;
    while i < args.len() {
        match args[i].as_str() {
            "--category" => {
                target_category = Some(args[i + 1].clone());
                i += 2;
            }
            "--index" => {
                target_index = args[i + 1].parse().unwrap_or(0);
                i += 2;
            }
            name => {
                target_name = Some(name.to_string());
                i += 1;
            }
        }
    }

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

    let mut fail_count = 0usize;

    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,
        };

        let schema = schema_from_json(&case.schema);
        let result = match column_lineage(&case.sql, dialect, &schema, None) {
            Ok(r) => r,
            Err(e) => {
                if target_name.as_deref() == Some(&case.name) {
                    println!("=== PARSE ERROR: {} ===", case.name);
                    println!("Error: {e}");
                    println!("\nSQL:\n{}\n", case.sql);
                    return;
                }
                continue;
            }
        };

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

        if actual == expected {
            continue;
        }

        let category = classify(&actual, &expected);

        // Check if this is the case we want
        let is_target = if let Some(ref name) = target_name {
            case.name == *name
        } else if let Some(ref cat) = target_category {
            category == cat.as_str() && {
                let matches = fail_count == target_index;
                if !matches {
                    fail_count += 1;
                }
                matches
            }
        } else {
            let matches = fail_count == target_index;
            if !matches {
                fail_count += 1;
            }
            matches
        };

        if !is_target {
            continue;
        }

        // Found our case — print everything
        println!("╔══════════════════════════════════════════════════════════════╗");
        println!("║  Case: {:<53}║", case.name);
        println!("║  Category: {:<49}║", category);
        println!("╚══════════════════════════════════════════════════════════════╝");

        println!("\n━━━ SQL ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        println!("{}", case.sql);

        println!("\n━━━ SCHEMA ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        for (table, cols) in &schema {
            if let Some(obj) = cols.as_object() {
                let col_names: Vec<&str> = obj.keys().map(|k| k.as_str()).collect();
                println!("  {} → [{}]", table, col_names.join(", "));
            }
        }

        println!("\n━━━ EXPECTED (ground truth) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        let mut exp_keys: Vec<&String> = expected.keys().collect();
        exp_keys.sort();
        for key in &exp_keys {
            let sources = &expected[*key];
            println!("  {} ←", key);
            for s in sources {
                println!("    {}", s);
            }
        }

        println!("\n━━━ ACTUAL (our engine) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        let mut act_keys: Vec<&String> = actual.keys().collect();
        act_keys.sort();
        for key in &act_keys {
            let sources = &actual[*key];
            let marker = if !expected.contains_key(*key) {
                " [EXTRA COL]"
            } else if expected[*key] != *sources {
                " [WRONG SOURCES]"
            } else {
                " ✓"
            };
            println!("  {}{}", key, marker);
            for s in sources {
                let s_marker = if let Some(exp) = expected.get(*key) {
                    if !exp.contains(s) { " ← EXTRA" } else { "" }
                } else {
                    ""
                };
                println!("    {}{}", s, s_marker);
            }
        }

        // Show missing output columns
        let missing_cols: Vec<&String> = expected
            .keys()
            .filter(|k| !actual.contains_key(*k))
            .collect();
        if !missing_cols.is_empty() {
            println!("\n  MISSING OUTPUT COLUMNS:");
            for col in &missing_cols {
                println!("    {} ← {:?}", col, expected[*col]);
            }
        }

        // Per-column diff for wrong_sources
        println!("\n━━━ DIFF (per column) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        for key in &exp_keys {
            if let Some(act) = actual.get(*key) {
                let exp = &expected[*key];
                if act == exp {
                    println!("  {} → MATCH", key);
                } else {
                    let missing: Vec<&String> = exp.difference(act).collect();
                    let extra: Vec<&String> = act.difference(exp).collect();
                    println!("  {} →", key);
                    for m in &missing {
                        println!("    MISSING: {}", m);
                    }
                    for e in &extra {
                        println!("    EXTRA:   {}", e);
                    }
                }
            } else {
                println!("  {} → NOT IN ACTUAL", key);
            }
        }

        // Show polyglot raw edges
        println!("\n━━━ RAW POLYGLOT EDGES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        match altimate_core::lineage::polyglot_column_lineage(&case.sql, dialect) {
            Ok(raw) => {
                if raw.edges.is_empty() {
                    println!("  (no edges)");
                }
                for edge in &raw.edges {
                    println!(
                        "  {} → {} (source: {}.{}, kind: {:?})",
                        edge.target_column,
                        edge.source_column,
                        edge.source_table,
                        edge.source_column,
                        edge.terminal_kind,
                    );
                }
                if !raw.errors.is_empty() {
                    println!("\n  Errors:");
                    for e in &raw.errors {
                        println!("    {}", e.message);
                    }
                }
            }
            Err(e) => println!("  polyglot error: {}", e),
        }

        return;
    }

    println!("No matching case found.");
    if let Some(name) = target_name {
        println!("  Searched for name: {}", name);
    }
}
