//! Generate an interactive HTML review page for the 22 "correct superset" cases.
//!
//! These are ground-truth cases where our engine returns extra source columns that
//! ALL come from tables NOT in the schema — meaning Snowflake ACCESS_USAGE couldn't
//! trace them and the ground truth is incomplete.
//!
//! Run: `cargo run --release --example superset_review`
//! Output: `/tmp/superset_review.html`

use altimate_core::lineage::column_lineage;
use altimate_core::schema::types::SqlDialect;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fmt::Write as FmtWrite;
use std::io::BufRead;

/// The 22 known "correct superset" case names (all extras from non-schema tables).
const CORRECT_SUPERSET_NAMES: &[&str] = &[
    "sf_insert_e5739d2c5883",
    "sf_insert_08621d4cfd84",
    "sf_insert_56fdbc562949",
    "sf_create_table_as_select_f38c788b1a36",
    "sf_insert_9c8abd4ed0e1",
    "sf_create_table_as_select_5ea36af5a999",
    "sf_create_table_as_select_81d7da9f18ec",
    "sf_create_table_as_select_cf0c9c00fbfd",
    "sf_create_table_as_select_53168677e8a9",
    "sf_insert_59d9e917e7f5",
    "sf_insert_94feb49ccd4e",
    "sf_insert_096646f79ac3",
    "sf_insert_0390b943e1f6",
    "sf_insert_21b86b6b541d",
    "sf_insert_d9cd116a441d",
    "sf_insert_447e8d444b32",
    "sf_insert_b3ccb83cb5cd",
    "sf_insert_3972cb688ba9",
    "sf_insert_17476fd313b2",
    "sf_insert_ad325093e3b5",
    "sf_insert_ef1b6474b9a0",
    "sf_create_table_as_select_ef1ea2ceb5ec",
];

#[derive(serde::Deserialize)]
struct Case {
    name: String,
    sql: String,
    schema: serde_json::Value,
    expected: HashMap<String, Vec<String>>,
}

/// Collect table names from the schema JSON object (uppercased).
fn schema_tables(schema: &serde_json::Value) -> HashSet<String> {
    let mut tables = HashSet::new();
    if let serde_json::Value::Object(map) = schema {
        for k in map.keys() {
            tables.insert(k.to_uppercase());
        }
    }
    tables
}

/// Extract table name from a qualified reference like `"DB"."SCHEMA"."TABLE"."COL"`.
fn extract_table_from_ref(source_ref: &str) -> Option<String> {
    let stripped = source_ref.replace('"', "");
    let parts: Vec<&str> = stripped.split('.').collect();
    if parts.len() >= 2 {
        Some(parts[parts.len() - 2].to_uppercase())
    } else {
        None
    }
}

/// Per-column comparison data for the review UI.
struct ColumnComparison {
    column: String,
    matched: Vec<String>,
    extra: Vec<(String, Option<String>, bool)>, // (source, table_name, in_schema)
    missing: Vec<String>,
}

/// Full case data for the review UI.
struct ReviewCase {
    name: String,
    sql: String,
    schema_table_names: BTreeSet<String>,
    columns: Vec<ColumnComparison>,
    valid_superset: bool,
}

/// Escape HTML special characters.
fn html_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}

fn main() {
    let target_names: HashSet<&str> = CORRECT_SUPERSET_NAMES.iter().copied().collect();

    let fixture_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../tests/fixtures/lineage/snowflake_ground_truth.jsonl");
    let file = std::fs::File::open(&fixture_path).expect("cannot open ground truth fixture");
    let reader = std::io::BufReader::with_capacity(1024 * 1024, file);
    let dialect = SqlDialect::Snowflake;

    let mut cases: Vec<ReviewCase> = Vec::new();
    let mut found = 0usize;

    eprintln!(
        "Scanning ground truth for {} target cases...",
        target_names.len()
    );

    for line in reader.lines() {
        let line = line.unwrap();
        if line.trim().is_empty() {
            continue;
        }
        let case: Case = match serde_json::from_str(&line) {
            Ok(c) => c,
            Err(_) => continue,
        };
        if !target_names.contains(case.name.as_str()) {
            continue;
        }
        found += 1;
        eprintln!(
            "  [{found}/{}] Processing: {}",
            target_names.len(),
            case.name
        );

        let schema_table_set = schema_tables(&case.schema);
        let schema_table_names: BTreeSet<String> = schema_table_set.iter().cloned().collect();
        let schema_map: HashMap<String, serde_json::Value> =
            match serde_json::from_value(case.schema.clone()) {
                Ok(s) => s,
                Err(e) => {
                    eprintln!("    SKIP: schema parse error: {e}");
                    continue;
                }
            };

        let result = match column_lineage(&case.sql, dialect, &schema_map, None) {
            Ok(r) => r,
            Err(e) => {
                eprintln!("    SKIP: lineage error: {e}");
                continue;
            }
        };

        // Normalize to BTreeSet for stable comparison
        let actual: HashMap<String, BTreeSet<String>> = result
            .column_dict
            .iter()
            .map(|(k, v)| (k.clone(), v.iter().cloned().collect()))
            .collect();
        let expected: HashMap<String, BTreeSet<String>> = case
            .expected
            .iter()
            .map(|(k, v)| (k.clone(), v.iter().cloned().collect()))
            .collect();

        // Build column comparisons
        let mut all_cols: BTreeSet<String> = BTreeSet::new();
        all_cols.extend(expected.keys().cloned());
        all_cols.extend(actual.keys().cloned());

        let mut columns = Vec::new();
        let mut all_extras_from_non_schema = true;

        for col in &all_cols {
            let exp: BTreeSet<String> = expected.get(col).cloned().unwrap_or_default();
            let act: BTreeSet<String> = actual.get(col).cloned().unwrap_or_default();

            let matched: Vec<String> = exp.intersection(&act).cloned().collect();
            let extra_set: BTreeSet<String> = act.difference(&exp).cloned().collect();
            let missing: Vec<String> = exp.difference(&act).cloned().collect();

            let mut extra = Vec::new();
            for e in &extra_set {
                let (table, in_schema) = if let Some(t) = extract_table_from_ref(e) {
                    let in_s = schema_table_set.contains(&t);
                    if in_s {
                        all_extras_from_non_schema = false;
                    }
                    (Some(t), in_s)
                } else {
                    (None, false)
                };
                extra.push((e.clone(), table, in_schema));
            }

            columns.push(ColumnComparison {
                column: col.clone(),
                matched,
                extra,
                missing,
            });
        }

        cases.push(ReviewCase {
            name: case.name.clone(),
            sql: case.sql,
            schema_table_names,
            columns,
            valid_superset: all_extras_from_non_schema,
        });

        if found == target_names.len() {
            break;
        }
    }

    eprintln!(
        "Found {} / {} cases. Generating HTML...",
        cases.len(),
        target_names.len()
    );

    // Generate HTML
    let valid_count = cases.iter().filter(|c| c.valid_superset).count();
    let total = cases.len();

    let mut html = String::with_capacity(256 * 1024);
    write_html_header(&mut html, total, valid_count);

    for (i, case) in cases.iter().enumerate() {
        write_case_card(&mut html, i, case);
    }

    write_html_footer(&mut html);

    let out_path = "/tmp/superset_review.html";
    std::fs::write(out_path, &html).expect("failed to write HTML");
    eprintln!("Wrote {out_path} ({} bytes, {total} cases)", html.len());
    println!("{out_path}");
}

fn write_html_header(html: &mut String, total: usize, valid: usize) {
    let _ = write!(
        html,
        r##"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Superset Review &mdash; Column Lineage Ground Truth</title>
<style>
  :root {{
    --bg: #f8f9fa;
    --card-bg: #ffffff;
    --border: #e2e8f0;
    --text: #1a202c;
    --muted: #718096;
    --green: #38a169;
    --green-bg: #c6f6d5;
    --green-dark: #276749;
    --red: #e53e3e;
    --red-bg: #fed7d7;
    --red-dark: #9b2c2c;
    --blue: #3182ce;
    --blue-bg: #bee3f8;
    --purple: #805ad5;
    --purple-bg: #e9d8fd;
    --shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
    --shadow-lg: 0 4px 6px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.06);
    --radius: 8px;
    --mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace;
  }}
  * {{ box-sizing: border-box; margin: 0; padding: 0; }}
  body {{
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
    background: var(--bg);
    color: var(--text);
    line-height: 1.6;
    padding: 2rem;
  }}
  .header {{
    max-width: 1200px;
    margin: 0 auto 2rem;
    text-align: center;
  }}
  .header h1 {{
    font-size: 1.75rem;
    font-weight: 700;
    margin-bottom: 0.5rem;
  }}
  .header .subtitle {{
    color: var(--muted);
    font-size: 0.95rem;
    margin-bottom: 1rem;
  }}
  .stats {{
    display: flex;
    justify-content: center;
    gap: 1.5rem;
    flex-wrap: wrap;
  }}
  .stat {{
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.75rem 1.5rem;
    box-shadow: var(--shadow);
  }}
  .stat .num {{
    font-size: 1.5rem;
    font-weight: 700;
  }}
  .stat .label {{
    font-size: 0.8rem;
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }}
  .stat.green .num {{ color: var(--green); }}
  .stat.blue .num {{ color: var(--blue); }}

  .cards {{
    max-width: 1200px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
  }}
  .card {{
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    overflow: hidden;
  }}
  .card-header {{
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1rem 1.25rem;
    border-bottom: 1px solid var(--border);
    background: #fdfdfe;
  }}
  .card-header h2 {{
    font-size: 1rem;
    font-weight: 600;
    font-family: var(--mono);
  }}
  .card-num {{
    color: var(--muted);
    font-size: 0.8rem;
    font-weight: 500;
    margin-right: 0.75rem;
  }}
  .badge {{
    display: inline-block;
    padding: 0.2rem 0.6rem;
    border-radius: 999px;
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.03em;
  }}
  .badge.valid {{
    background: var(--green-bg);
    color: var(--green-dark);
  }}
  .badge.invalid {{
    background: var(--red-bg);
    color: var(--red-dark);
  }}
  .card-body {{
    padding: 1.25rem;
  }}
  .section-label {{
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
    margin-bottom: 0.5rem;
  }}
  .pills {{
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-bottom: 1.25rem;
  }}
  .pill {{
    display: inline-block;
    padding: 0.2rem 0.55rem;
    border-radius: 4px;
    font-size: 0.8rem;
    font-family: var(--mono);
    font-weight: 500;
    background: var(--blue-bg);
    color: #2a4365;
  }}
  details {{
    margin-bottom: 1.25rem;
  }}
  details summary {{
    cursor: pointer;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--blue);
    padding: 0.4rem 0;
    user-select: none;
  }}
  details summary:hover {{
    text-decoration: underline;
  }}
  .sql-block {{
    background: #1e1e2e;
    color: #cdd6f4;
    border-radius: 6px;
    padding: 1rem;
    overflow-x: auto;
    margin-top: 0.5rem;
    font-family: var(--mono);
    font-size: 0.82rem;
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 400px;
    overflow-y: auto;
  }}
  .col-table {{
    width: 100%;
    border-collapse: collapse;
    font-size: 0.85rem;
  }}
  .col-table th {{
    text-align: left;
    padding: 0.5rem 0.75rem;
    background: #f7fafc;
    border-bottom: 2px solid var(--border);
    font-weight: 600;
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
  }}
  .col-table td {{
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
    font-family: var(--mono);
    font-size: 0.82rem;
  }}
  .col-table tr:last-child td {{
    border-bottom: none;
  }}
  .col-name {{
    font-weight: 600;
    white-space: nowrap;
  }}
  .source-line {{
    display: block;
    padding: 1px 0;
    word-break: break-all;
  }}
  .extra-tag {{
    display: inline-block;
    background: var(--green-bg);
    color: var(--green-dark);
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 0.72rem;
    font-weight: 700;
    margin-left: 4px;
    vertical-align: middle;
  }}
  .extra-source {{
    background: #f0fff4;
    border-radius: 3px;
    padding: 0 3px;
  }}
  .missing-tag {{
    display: inline-block;
    background: var(--red-bg);
    color: var(--red-dark);
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 0.72rem;
    font-weight: 700;
    margin-left: 4px;
    vertical-align: middle;
  }}
  .missing-source {{
    background: #fff5f5;
    border-radius: 3px;
    padding: 0 3px;
  }}
  .table-info {{
    font-size: 0.72rem;
    color: var(--muted);
    margin-left: 4px;
  }}
  .table-info.not-in-schema {{
    color: var(--green);
  }}
  .table-info.in-schema {{
    color: var(--red);
  }}
  .review-controls {{
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.75rem 1.25rem;
    border-top: 1px solid var(--border);
    background: #fafbfc;
  }}
  .review-controls label {{
    display: flex;
    align-items: center;
    gap: 0.4rem;
    cursor: pointer;
    font-size: 0.85rem;
    font-weight: 500;
  }}
  .review-controls input[type="radio"] {{
    accent-color: var(--green);
    width: 16px;
    height: 16px;
  }}
  .review-controls .approve {{ color: var(--green-dark); }}
  .review-controls .reject {{ color: var(--red-dark); }}
</style>
</head>
<body>
<div class="header">
  <h1>Superset Review</h1>
  <p class="subtitle">Column lineage cases where our engine finds extra sources from tables absent in the schema.<br>
  Ground truth (Snowflake ACCESS_USAGE) cannot trace through non-schema tables, so these extras are likely correct.</p>
  <div class="stats">
    <div class="stat blue">
      <div class="num">{total}</div>
      <div class="label">Total Cases</div>
    </div>
    <div class="stat green">
      <div class="num">{valid}</div>
      <div class="label">Valid Supersets</div>
    </div>
  </div>
</div>
<div class="cards">
"##
    );
}

fn write_case_card(html: &mut String, index: usize, case: &ReviewCase) {
    let badge_class = if case.valid_superset {
        "valid"
    } else {
        "invalid"
    };
    let badge_text = if case.valid_superset {
        "VALID &mdash; extra from non-schema table"
    } else {
        "NEEDS REVIEW &mdash; extra from schema table"
    };

    let _ = write!(
        html,
        r##"<div class="card" id="case-{index}">
  <div class="card-header">
    <div style="display:flex;align-items:center">
      <span class="card-num">#{num}</span>
      <h2>{name}</h2>
    </div>
    <span class="badge {badge_class}">{badge_text}</span>
  </div>
  <div class="card-body">
    <div class="section-label">Schema Tables</div>
    <div class="pills">
"##,
        num = index + 1,
        name = html_escape(&case.name),
    );

    for table in &case.schema_table_names {
        let _ = write!(
            html,
            "      <span class=\"pill\">{}</span>\n",
            html_escape(table)
        );
    }

    let _ = write!(
        html,
        r##"    </div>
    <details>
      <summary>Show SQL Query</summary>
      <div class="sql-block">{sql}</div>
    </details>
    <div class="section-label">Column Comparison</div>
    <table class="col-table">
      <thead>
        <tr>
          <th style="width:20%">Column</th>
          <th style="width:40%">Expected Sources</th>
          <th style="width:40%">Actual Sources</th>
        </tr>
      </thead>
      <tbody>
"##,
        sql = html_escape(&case.sql),
    );

    for col in &case.columns {
        let has_diff = !col.extra.is_empty() || !col.missing.is_empty();
        let _ = write!(
            html,
            "        <tr{}>\n          <td class=\"col-name\">{}</td>\n",
            if has_diff {
                " style=\"background:#fffdf5\""
            } else {
                ""
            },
            html_escape(&col.column),
        );

        // Expected sources column
        let _ = write!(html, "          <td>");
        for src in &col.matched {
            let _ = write!(
                html,
                "<span class=\"source-line\">{}</span>",
                html_escape(src)
            );
        }
        for src in &col.missing {
            let _ = write!(
                html,
                "<span class=\"source-line missing-source\">{}<span class=\"missing-tag\">- MISSING</span></span>",
                html_escape(src)
            );
        }
        let _ = write!(html, "</td>\n");

        // Actual sources column
        let _ = write!(html, "          <td>");
        for src in &col.matched {
            let _ = write!(
                html,
                "<span class=\"source-line\">{}</span>",
                html_escape(src)
            );
        }
        for (src, table, in_schema) in &col.extra {
            let table_label = if let Some(t) = table {
                if *in_schema {
                    format!(
                        "<span class=\"table-info in-schema\">(table: {} - IN schema)</span>",
                        html_escape(t)
                    )
                } else {
                    format!(
                        "<span class=\"table-info not-in-schema\">(table: {} - not in schema)</span>",
                        html_escape(t)
                    )
                }
            } else {
                String::new()
            };
            let _ = write!(
                html,
                "<span class=\"source-line extra-source\">{}<span class=\"extra-tag\">+ EXTRA</span>{}</span>",
                html_escape(src),
                table_label,
            );
        }
        let _ = write!(html, "</td>\n");

        let _ = write!(html, "        </tr>\n");
    }

    let _ = write!(
        html,
        r##"      </tbody>
    </table>
  </div>
  <div class="review-controls">
    <label class="approve">
      <input type="radio" name="review-{index}" value="approve"> Approve
    </label>
    <label class="reject">
      <input type="radio" name="review-{index}" value="reject"> Reject
    </label>
  </div>
</div>
"##,
    );
}

fn write_html_footer(html: &mut String) {
    let _ = write!(
        html,
        r##"</div>
<script>
// Purely visual: highlight card border on approve/reject
document.querySelectorAll('.review-controls input[type="radio"]').forEach(radio => {{
  radio.addEventListener('change', e => {{
    const card = e.target.closest('.card');
    card.style.borderColor = e.target.value === 'approve' ? 'var(--green)' : 'var(--red)';
    card.style.borderWidth = '2px';
  }});
}});
</script>
</body>
</html>"##
    );
}
