//! Diagnose SELECT * expansion failures.
//!
//! Run: cargo run --example star_diagnostic --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)]
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()
}

fn has_select_star(sql: &str) -> bool {
    let upper = sql.to_uppercase();
    upper.contains("SELECT *") || upper.contains("SELECT\n*") || upper.contains("SELECT\r\n*")
}

/// Count how many levels of wrapping (subqueries/CTEs with SELECT *) the query has.
fn count_star_depth(sql: &str) -> usize {
    let upper = sql.to_uppercase();
    upper.matches("SELECT *").count()
}

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;

    // Categories for missing columns in SELECT * queries
    let mut star_depth_1_missing = 0u64; // SELECT * (1 level)
    let mut star_depth_2_missing = 0u64; // SELECT * nested 2+
    let mut star_actual_has_star_key = 0u64; // actual output has "*" key
    let mut star_actual_empty = 0u64; // actual output completely empty
    let mut star_actual_has_some = 0u64; // actual has some columns but not all

    // Track: does the outermost query have SELECT *?
    let mut outer_star_missing = 0u64;
    let mut inner_star_only_missing = 0u64;

    // Samples
    let mut samples_star1: Vec<String> = Vec::new();
    let mut samples_star2: Vec<String> = Vec::new();
    let mut samples_has_star_key: Vec<String> = Vec::new();

    // Track queries with missing cols that have SELECT *
    let mut queries_with_star_missing = 0u64;
    let mut total_queries_with_star = 0u64;

    // Track actual column_dict keys for queries with missing
    let mut actual_has_cols_missing_in_expected = 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,
        };

        if !has_select_star(&case.sql) {
            continue;
        }
        total_queries_with_star += 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);

        // Find missing columns
        let mut query_has_missing = false;
        for (col, _exp_sources) in &expected {
            if !actual.contains_key(col) {
                query_has_missing = true;
                let depth = count_star_depth(&case.sql);

                if depth == 1 {
                    star_depth_1_missing += 1;
                } else {
                    star_depth_2_missing += 1;
                }

                // Check if actual has "*" key
                if actual.contains_key("*") || actual.contains_key("\"*\"") {
                    star_actual_has_star_key += 1;
                }

                // Check if the outermost SELECT is *
                let trimmed = case.sql.trim();
                let upper = trimmed.to_uppercase();
                let is_outer_star = upper.starts_with("SELECT *")
                    || upper.starts_with("SELECT\n*")
                    || upper.starts_with("SELECT\r\n*")
                    || (upper.starts_with("WITH ") && {
                        // For CTE queries, check the final SELECT
                        // Simple heuristic: last SELECT * in the query
                        let last_select = upper.rfind("SELECT");
                        if let Some(pos) = last_select {
                            let after_select = &upper[pos + 6..].trim_start();
                            after_select.starts_with('*')
                        } else {
                            false
                        }
                    });

                if is_outer_star {
                    outer_star_missing += 1;
                } else {
                    inner_star_only_missing += 1;
                }
            }
        }

        if query_has_missing {
            queries_with_star_missing += 1;

            if actual.is_empty() {
                star_actual_empty += 1;
                if samples_star1.len() < 5 {
                    samples_star1.push(format!(
                        "EMPTY: {} | sql: {} | expected_cols: {}",
                        case.name,
                        &case.sql[..case.sql.len().min(100)],
                        expected.len()
                    ));
                }
            } else {
                star_actual_has_some += 1;
                let actual_cols: Vec<_> = actual.keys().collect();
                let expected_cols: Vec<_> = expected.keys().collect();
                let missing: Vec<_> = expected_cols
                    .iter()
                    .filter(|c| !actual.contains_key(**c))
                    .collect();
                if samples_star2.len() < 10 {
                    samples_star2.push(format!(
                        "PARTIAL: {} | actual_cols={} expected_cols={} missing={} | actual_keys={:?} | missing={:?} | sql: {}",
                        case.name,
                        actual_cols.len(),
                        expected_cols.len(),
                        missing.len(),
                        &actual_cols[..actual_cols.len().min(5)],
                        &missing[..missing.len().min(5)],
                        &case.sql[..case.sql.len().min(100)]
                    ));
                }
            }

            if actual.contains_key("*") || actual.keys().any(|k| k.ends_with(".*")) {
                actual_has_cols_missing_in_expected += 1;
                if samples_has_star_key.len() < 5 {
                    let star_val = actual.get("*").or_else(|| {
                        actual
                            .iter()
                            .find(|(k, _)| k.ends_with(".*"))
                            .map(|(_, v)| v)
                    });
                    samples_has_star_key.push(format!(
                        "HAS_STAR: {} | star_sources={:?} | sql: {}",
                        case.name,
                        star_val,
                        &case.sql[..case.sql.len().min(100)]
                    ));
                }
            }
        }
    }

    println!("\n=== SELECT * Diagnostic ===");
    println!("Total queries with SELECT *:     {total_queries_with_star}");
    println!("Queries with missing columns:    {queries_with_star_missing}");
    println!("  actual completely empty:       {star_actual_empty}");
    println!("  actual has some columns:       {star_actual_has_some}");
    println!("  actual still has '*' key:      {actual_has_cols_missing_in_expected}");
    println!("\nMissing column breakdown:");
    println!("  Single SELECT * depth:         {star_depth_1_missing}");
    println!("  Nested SELECT * (2+):          {star_depth_2_missing}");
    println!("  Outer SELECT * missing:        {outer_star_missing}");
    println!("  Inner SELECT * only missing:   {inner_star_only_missing}");
    println!("  actual has '*' key:            {star_actual_has_star_key}");

    println!("\n=== Samples: Empty Actual ===");
    for s in &samples_star1 {
        println!("  {s}");
    }

    println!("\n=== Samples: Partial Actual ===");
    for s in &samples_star2 {
        println!("  {s}");
    }

    println!("\n=== Samples: Still has '*' key ===");
    for s in &samples_has_star_key {
        println!("  {s}");
    }
}
