//! Calibrate any uncalibrated JSONL fixture through our column lineage engine.
//!
//! Reads `*_uncalibrated.jsonl` files, runs each SQL through `column_lineage()`,
//! and writes calibrated output with actual engine results as expected values.
//!
//! Usage:
//!   # Calibrate a specific file:
//!   cargo run --example calibrate_all -p altimate-core --release -- input.jsonl output.jsonl
//!
//!   # Calibrate all uncalibrated files in the lineage fixtures dir:
//!   cargo run --example calibrate_all -p altimate-core --release

use altimate_core::lineage::column_lineage;
use altimate_core::schema::types::SqlDialect;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};

#[derive(Debug, Deserialize)]
struct UncalibratedCase {
    name: String,
    sql: String,
    schema: serde_json::Value,
    dialect: String,
    #[serde(default = "default_mode")]
    mode: String,
    #[allow(dead_code)]
    expected: serde_json::Value,
}

fn default_mode() -> String {
    "projection".to_string()
}

#[derive(Debug, Serialize)]
struct CalibratedCase {
    name: String,
    sql: String,
    schema: serde_json::Value,
    dialect: String,
    mode: String,
    expected: HashMap<String, Vec<String>>,
}

fn parse_dialect(s: &str) -> SqlDialect {
    match s {
        "generic" => SqlDialect::Generic,
        "snowflake" => SqlDialect::Snowflake,
        "postgres" => SqlDialect::Postgres,
        "bigquery" => SqlDialect::BigQuery,
        "mysql" => SqlDialect::MySQL,
        "clickhouse" => SqlDialect::ClickHouse,
        "tsql" | "mssql" => SqlDialect::TSQL,
        "oracle" => SqlDialect::Oracle,
        "redshift" => SqlDialect::Redshift,
        "databricks" => SqlDialect::Databricks,
        "duckdb" => SqlDialect::DuckDB,
        "spark" => SqlDialect::Spark,
        "hive" => SqlDialect::Hive,
        "trino" => SqlDialect::Trino,
        "presto" => SqlDialect::Presto,
        "sqlite" => SqlDialect::SQLite,
        "athena" => SqlDialect::Athena,
        "teradata" => SqlDialect::Teradata,
        "dremio" => SqlDialect::Dremio,
        _ => SqlDialect::Generic,
    }
}

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 calibrate_file(input_path: &Path, output_path: &Path) {
    eprintln!(
        "Calibrating: {} → {}",
        input_path.display(),
        output_path.display()
    );

    let file = fs::File::open(input_path).expect("failed to open input file");
    let reader = BufReader::new(file);
    let mut output = fs::File::create(output_path).expect("failed to create output file");

    let mut total = 0;
    let mut ok = 0;
    let mut engine_errors = 0;
    let mut parse_errors = 0;
    let mut empty_results = 0;
    let mut dialect_stats: HashMap<String, (usize, usize)> = HashMap::new();

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

        let case: UncalibratedCase = match serde_json::from_str(&line) {
            Ok(c) => c,
            Err(e) => {
                eprintln!("  SKIP parse error on line: {e}");
                parse_errors += 1;
                continue;
            }
        };
        total += 1;

        let dialect = parse_dialect(&case.dialect);
        let schema = schema_from_json(&case.schema);
        let entry = dialect_stats.entry(case.dialect.clone()).or_insert((0, 0));
        entry.0 += 1;

        let result = match column_lineage(&case.sql, dialect, &schema, None) {
            Ok(r) => r,
            Err(e) => {
                engine_errors += 1;
                eprintln!("  ERR [{}]: {e}", case.name);
                continue;
            }
        };

        if result.column_dict.is_empty() {
            empty_results += 1;
            continue;
        }

        let mut expected: HashMap<String, Vec<String>> = HashMap::new();
        for (col, sources) in &result.column_dict {
            let mut sorted = sources.clone();
            sorted.sort();
            sorted.dedup();
            expected.insert(col.clone(), sorted);
        }

        let calibrated = CalibratedCase {
            name: case.name,
            sql: case.sql,
            schema: case.schema,
            dialect: case.dialect,
            mode: case.mode,
            expected,
        };

        let json_line = serde_json::to_string(&calibrated).expect("failed to serialize");
        writeln!(output, "{json_line}").expect("failed to write");
        ok += 1;
        entry.1 += 1;
    }

    eprintln!("  Input:  {total} cases (+ {parse_errors} JSON parse errors)");
    eprintln!("  Output: {ok} calibrated");
    eprintln!("  Empty:  {empty_results} | Engine errors: {engine_errors}");

    let mut dialects: Vec<_> = dialect_stats.iter().collect();
    dialects.sort_by_key(|(name, _)| (*name).clone());
    for (name, (total_d, pass_d)) in &dialects {
        eprintln!("    {name}: {pass_d}/{total_d}");
    }
    eprintln!();
}

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

    if args.len() == 3 {
        // Explicit input/output paths
        let input = PathBuf::from(&args[1]);
        let output = PathBuf::from(&args[2]);
        calibrate_file(&input, &output);
        return;
    }

    // Auto-discover all *_uncalibrated.jsonl files
    let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/lineage");

    let mut found = 0;
    for entry in fs::read_dir(&base).expect("failed to read lineage dir") {
        let entry = entry.expect("failed to read dir entry");
        let path = entry.path();
        let name = path.file_name().unwrap().to_string_lossy().to_string();

        if name.ends_with("_uncalibrated.jsonl") {
            let output_name = name.replace("_uncalibrated.jsonl", "_calibrated.jsonl");
            let output_path = base.join(&output_name);
            calibrate_file(&path, &output_path);
            found += 1;
        }
    }

    if found == 0 {
        eprintln!("No *_uncalibrated.jsonl files found in {}", base.display());
        eprintln!(
            "Usage: cargo run --example calibrate_all -p altimate-core --release -- [input.jsonl output.jsonl]"
        );
    } else {
        eprintln!("Calibrated {found} files total.");
    }
}
