//! Semantic validation rules that detect logically incorrect SQL.
//!
//! Each rule inspects the DataFusion [`LogicalPlan`] and the schema
//! to detect patterns that are syntactically valid but semantically wrong
//! or suspicious (e.g., cartesian products, wrong JOIN keys, duplicate counting).

use super::types::{SemanticFinding, SemanticSeverity};
use crate::schema::types::SchemaDefinition;
use datafusion::logical_expr::{Expr, LogicalPlan};
use std::collections::{HashMap, HashSet};

/// Run all semantic rules against a logical plan and schema.
/// Returns (findings, passed_check_names).
pub fn run_all_rules(
    plan: &LogicalPlan,
    schema: &SchemaDefinition,
) -> (Vec<SemanticFinding>, Vec<String>) {
    type RuleFn = fn(&LogicalPlan, &SchemaDefinition) -> Vec<SemanticFinding>;
    let rules: Vec<(&str, RuleFn)> = vec![
        ("missing_join_condition", check_missing_join_condition),
        ("wrong_join_key", check_wrong_join_key),
        (
            "aggregate_without_group_by",
            check_aggregate_without_group_by,
        ),
        ("where_vs_having", check_where_vs_having),
        ("duplicate_counting", check_duplicate_counting),
        ("unnecessary_distinct", check_unnecessary_distinct),
        ("null_unsafe_comparison", check_null_unsafe_comparison),
        ("filter_order", check_filter_order),
        ("implicit_type_coercion", check_implicit_type_coercion),
        ("self_join_without_alias", check_self_join_without_alias),
    ];

    let mut all_findings = Vec::new();
    let mut passed = Vec::new();

    for (name, check_fn) in rules {
        let findings = check_fn(plan, schema);
        if findings.is_empty() {
            passed.push(name.to_string());
        } else {
            all_findings.extend(findings);
        }
    }

    (all_findings, passed)
}

// ─── Rule 1: Missing JOIN condition (cartesian product) ─────────────

fn check_missing_join_condition(
    plan: &LogicalPlan,
    _schema: &SchemaDefinition,
) -> Vec<SemanticFinding> {
    let mut findings = Vec::new();
    visit_plan(plan, &mut |node| {
        // In DataFusion 46, cross joins are represented as Join with empty on/filter
        if let LogicalPlan::Join(join) = node
            && join.on.is_empty()
            && join.filter.is_none()
        {
            let left = extract_table_name(&join.left).unwrap_or_else(|| "left".to_string());
            let right = extract_table_name(&join.right).unwrap_or_else(|| "right".to_string());
            findings.push(SemanticFinding {
                rule: "missing_join_condition".to_string(),
                severity: SemanticSeverity::Error,
                message: format!("Cartesian product detected between '{left}' and '{right}'"),
                explanation: format!(
                    "A CROSS JOIN (or JOIN without ON) between '{left}' and '{right}' \
                         produces every combination of rows from both tables. This is almost \
                         always unintentional and produces row counts of |{left}| * |{right}|."
                ),
                suggestion: Some(format!(
                    "Add a JOIN condition: JOIN {right} ON {left}.<key> = {right}.<key>"
                )),
                confidence: 0.95,
            });
        }
    });
    findings
}

// ─── Rule 2: Wrong JOIN key ─────────────────────────────────────────

fn check_wrong_join_key(plan: &LogicalPlan, schema: &SchemaDefinition) -> Vec<SemanticFinding> {
    let mut findings = Vec::new();

    // Build a map of foreign key relationships
    let fk_pairs = build_fk_pairs(schema);

    visit_plan(plan, &mut |node| {
        if let LogicalPlan::Join(join) = node {
            // Use base table names (not aliases) for FK/PK lookup
            let left_table = extract_base_table_name(&join.left);
            let right_table = extract_base_table_name(&join.right);

            if let (Some(ref lt), Some(ref rt)) = (left_table, right_table) {
                // Collect all column pairs from the `on` field
                let mut col_pairs: Vec<(String, String)> = Vec::new();
                for (left_col_expr, right_col_expr) in &join.on {
                    if let (Some(lc), Some(rc)) = (
                        extract_column_name(left_col_expr),
                        extract_column_name(right_col_expr),
                    ) {
                        col_pairs.push((lc, rc));
                    }
                }

                // Also extract column pairs from the filter (DataFusion may put
                // the join condition here instead of `on`)
                if let Some(ref filter) = join.filter {
                    extract_eq_column_pairs(filter, &mut col_pairs);
                }

                for (lc, rc) in &col_pairs {
                    // Check if these columns form a known FK relationship
                    let is_valid_fk = fk_pairs.iter().any(|(ft, fc, rt2, rc2)| {
                        (ft == lt && fc == lc && rt2 == rt && rc2 == rc)
                            || (ft == rt && fc == rc && rt2 == lt && rc2 == lc)
                    });

                    // Check if joining on primary keys of both tables
                    let left_is_pk = is_primary_key(schema, lt, lc);
                    let right_is_pk = is_primary_key(schema, rt, rc);

                    if !is_valid_fk && !left_is_pk && !right_is_pk {
                        // Neither FK nor PK — suspicious join key
                        findings.push(SemanticFinding {
                            rule: "wrong_join_key".to_string(),
                            severity: SemanticSeverity::Warning,
                            message: format!(
                                "JOIN on {lt}.{lc} = {rt}.{rc} does not match any foreign key relationship"
                            ),
                            explanation: format!(
                                "The columns {lt}.{lc} and {rt}.{rc} are not connected by a \
                                 foreign key in the schema. This may produce incorrect results \
                                 if these columns don't represent the same entity."
                            ),
                            suggestion: suggest_correct_join_key(schema, lt, rt),
                            confidence: 0.7,
                        });
                    }
                }
            }
        }
    });

    findings
}

/// Extract equality column pairs from a filter expression (e.g., `a.col = b.col`).
fn extract_eq_column_pairs(expr: &Expr, pairs: &mut Vec<(String, String)>) {
    if let Expr::BinaryExpr(binary) = expr {
        if matches!(binary.op, datafusion::logical_expr::Operator::Eq)
            && let (Some(lc), Some(rc)) = (
                extract_column_name(&binary.left),
                extract_column_name(&binary.right),
            )
        {
            pairs.push((lc, rc));
        }
        // Recurse for AND conditions
        if matches!(binary.op, datafusion::logical_expr::Operator::And) {
            extract_eq_column_pairs(&binary.left, pairs);
            extract_eq_column_pairs(&binary.right, pairs);
        }
    }
}

// ─── Rule 3: Aggregate without GROUP BY ─────────────────────────────

fn check_aggregate_without_group_by(
    plan: &LogicalPlan,
    _schema: &SchemaDefinition,
) -> Vec<SemanticFinding> {
    let mut findings = Vec::new();

    visit_plan(plan, &mut |node| {
        if let LogicalPlan::Projection(proj) = node {
            let has_agg = proj.expr.iter().any(contains_aggregate);
            let has_non_agg = proj.expr.iter().any(is_bare_column);

            if has_agg && has_non_agg {
                // Check if there's a GROUP BY underneath
                let has_group_by = has_aggregate_child(&proj.input);
                if !has_group_by {
                    let bare_cols: Vec<String> = proj
                        .expr
                        .iter()
                        .filter(|e| is_bare_column(e))
                        .map(|e| format!("{e}"))
                        .collect();
                    let agg_fns: Vec<String> = proj
                        .expr
                        .iter()
                        .filter(|e| contains_aggregate(e))
                        .map(|e| format!("{e}"))
                        .collect();

                    findings.push(SemanticFinding {
                        rule: "aggregate_without_group_by".to_string(),
                        severity: SemanticSeverity::Error,
                        message: format!(
                            "Mixing aggregate functions ({}) with non-aggregated columns ({}) without GROUP BY",
                            agg_fns.join(", "),
                            bare_cols.join(", ")
                        ),
                        explanation: "When using aggregate functions like COUNT, SUM, AVG, etc., \
                                     all non-aggregated columns in SELECT must appear in GROUP BY. \
                                     Without GROUP BY, the database may return arbitrary values for \
                                     the non-aggregated columns or raise an error."
                            .to_string(),
                        suggestion: Some(format!(
                            "Add GROUP BY {}",
                            bare_cols.join(", ")
                        )),
                        confidence: 0.85,
                    });
                }
            }
        }
    });

    findings
}

// ─── Rule 4: WHERE vs HAVING confusion ──────────────────────────────

fn check_where_vs_having(plan: &LogicalPlan, _schema: &SchemaDefinition) -> Vec<SemanticFinding> {
    let mut findings = Vec::new();

    visit_plan(plan, &mut |node| {
        if let LogicalPlan::Filter(filter) = node {
            // Check if the filter predicate contains aggregate functions
            if contains_aggregate(&filter.predicate) {
                // Check if this filter is ABOVE an aggregate (that's HAVING — correct)
                // vs BELOW (that's WHERE with aggregate — wrong)
                let is_above_aggregate = has_aggregate_child(&filter.input);
                if !is_above_aggregate {
                    findings.push(SemanticFinding {
                        rule: "where_vs_having".to_string(),
                        severity: SemanticSeverity::Error,
                        message: format!(
                            "Aggregate function in WHERE clause: {}",
                            filter.predicate
                        ),
                        explanation: "Aggregate functions (COUNT, SUM, AVG, etc.) cannot be used \
                                     in WHERE clauses. WHERE filters individual rows BEFORE \
                                     aggregation. Use HAVING to filter AFTER aggregation."
                            .to_string(),
                        suggestion: Some(format!(
                            "Move the condition to HAVING: HAVING {}",
                            filter.predicate
                        )),
                        confidence: 0.95,
                    });
                }
            }
        }
    });

    findings
}

// ─── Rule 5: Duplicate counting after one-to-many JOIN ──────────────

fn check_duplicate_counting(plan: &LogicalPlan, schema: &SchemaDefinition) -> Vec<SemanticFinding> {
    let mut findings = Vec::new();

    // Look for patterns: Aggregate(SUM/COUNT) above Join on a one-to-many FK
    visit_plan(plan, &mut |node| {
        if let LogicalPlan::Aggregate(agg) = node {
            // Check for SUM or COUNT in aggregate expressions
            let has_sum_or_count = agg.aggr_expr.iter().any(|e| {
                let s = format!("{e}").to_uppercase();
                s.contains("SUM(") || s.contains("COUNT(")
            });

            if has_sum_or_count {
                // Check if there's a one-to-many join below
                let one_to_many_joins = find_one_to_many_joins(&agg.input, schema);
                for (parent_table, child_table) in one_to_many_joins {
                    // Check if the aggregate is on a column from the parent table
                    for expr in &agg.aggr_expr {
                        let expr_str = format!("{expr}").to_uppercase();
                        if expr_str.contains("SUM(") || expr_str.contains("COUNT(") {
                            findings.push(SemanticFinding {
                                rule: "duplicate_counting".to_string(),
                                severity: SemanticSeverity::Warning,
                                message: format!(
                                    "Potential duplicate counting: aggregate {expr} after \
                                     one-to-many JOIN between '{parent_table}' and '{child_table}'"
                                ),
                                explanation: format!(
                                    "When you JOIN '{parent_table}' with '{child_table}' \
                                     (one-to-many), rows from '{parent_table}' are duplicated \
                                     for each matching row in '{child_table}'. Aggregating \
                                     (SUM/COUNT) on '{parent_table}' columns will produce \
                                     inflated results."
                                ),
                                suggestion: Some(
                                    "Use a subquery or CTE to aggregate the child table \
                                     before joining, or use DISTINCT inside the aggregate."
                                        .to_string(),
                                ),
                                confidence: 0.6,
                            });
                        }
                    }
                }
            }
        }
    });

    findings
}

// ─── Rule 6: Unnecessary DISTINCT ───────────────────────────────────

fn check_unnecessary_distinct(
    plan: &LogicalPlan,
    _schema: &SchemaDefinition,
) -> Vec<SemanticFinding> {
    let mut findings = Vec::new();

    visit_plan(plan, &mut |node| {
        if let LogicalPlan::Distinct(_distinct) = node {
            // Check if DISTINCT is applied above a JOIN
            // In DataFusion 46, Distinct.input() is a method
            let inputs = node.inputs();
            let input = match inputs.first() {
                Some(i) => *i,
                None => return,
            };
            if has_join_below(input) {
                findings.push(SemanticFinding {
                    rule: "unnecessary_distinct".to_string(),
                    severity: SemanticSeverity::Warning,
                    message: "DISTINCT after JOIN may mask duplicate rows from an incorrect join"
                        .to_string(),
                    explanation: "Using DISTINCT to remove duplicates after a JOIN often \
                                 indicates the JOIN is producing unintended duplicate rows. \
                                 Instead of hiding the problem with DISTINCT, fix the JOIN \
                                 condition or use a more specific join type."
                        .to_string(),
                    suggestion: Some(
                        "Review the JOIN condition — the duplicates likely come from \
                         a missing or incorrect ON clause."
                            .to_string(),
                    ),
                    confidence: 0.5,
                });
            }
        }
    });

    findings
}

// ─── Rule 7: NULL-unsafe comparison ─────────────────────────────────

fn check_null_unsafe_comparison(
    plan: &LogicalPlan,
    _schema: &SchemaDefinition,
) -> Vec<SemanticFinding> {
    let mut findings = Vec::new();

    visit_plan(plan, &mut |node| {
        if let LogicalPlan::Filter(filter) = node {
            check_null_in_expr(&filter.predicate, &mut findings);
        }
        if let LogicalPlan::Join(join) = node
            && let Some(ref filter) = join.filter
        {
            check_null_in_expr(filter, &mut findings);
        }
    });

    findings
}

/// Check for `column = NULL` patterns in an expression tree.
fn check_null_in_expr(expr: &Expr, findings: &mut Vec<SemanticFinding>) {
    match expr {
        Expr::BinaryExpr(binary) => {
            let is_eq_or_neq = matches!(
                binary.op,
                datafusion::logical_expr::Operator::Eq | datafusion::logical_expr::Operator::NotEq
            );
            let left_is_null = matches!(
                &*binary.left,
                Expr::Literal(datafusion::common::ScalarValue::Null)
            );
            let right_is_null = matches!(
                &*binary.right,
                Expr::Literal(datafusion::common::ScalarValue::Null)
            );

            if is_eq_or_neq && (left_is_null || right_is_null) {
                let non_null_side = if left_is_null {
                    format!("{}", binary.right)
                } else {
                    format!("{}", binary.left)
                };
                let op_str = if matches!(binary.op, datafusion::logical_expr::Operator::Eq) {
                    "="
                } else {
                    "!="
                };
                findings.push(SemanticFinding {
                    rule: "null_unsafe_comparison".to_string(),
                    severity: SemanticSeverity::Error,
                    message: format!(
                        "Comparing with NULL using '{op_str}' — this always evaluates to NULL/UNKNOWN"
                    ),
                    explanation: format!(
                        "In SQL, NULL {op_str} NULL is NULL (not TRUE). The expression \
                         '{non_null_side} {op_str} NULL' will never match any rows. \
                         Use IS NULL or IS NOT NULL instead."
                    ),
                    suggestion: Some(if op_str == "=" {
                        format!("{non_null_side} IS NULL")
                    } else {
                        format!("{non_null_side} IS NOT NULL")
                    }),
                    confidence: 0.99,
                });
            }

            // Recurse
            check_null_in_expr(&binary.left, findings);
            check_null_in_expr(&binary.right, findings);
        }
        Expr::Not(inner) => check_null_in_expr(inner, findings),
        Expr::IsNull(_) | Expr::IsNotNull(_) => {} // Correct usage
        _ => {}
    }
}

// ─── Rule 8: Filter order (pre-filter optimization) ─────────────────

fn check_filter_order(plan: &LogicalPlan, _schema: &SchemaDefinition) -> Vec<SemanticFinding> {
    let mut findings = Vec::new();

    visit_plan(plan, &mut |node| {
        if let LogicalPlan::Filter(filter) = node {
            // Check if the filter is directly above a join AND the filter
            // references columns from only one side of the join
            if let LogicalPlan::Join(join) = &*filter.input {
                let filter_cols = extract_column_refs(&filter.predicate);
                let left_cols = collect_table_columns(&join.left);
                let right_cols = collect_table_columns(&join.right);

                let refs_left = filter_cols.iter().any(|c| left_cols.contains(c));
                let refs_right = filter_cols.iter().any(|c| right_cols.contains(c));

                if refs_left && !refs_right {
                    findings.push(SemanticFinding {
                        rule: "filter_order".to_string(),
                        severity: SemanticSeverity::Info,
                        message: "Filter applied after JOIN could be pushed down to left side"
                            .to_string(),
                        explanation: format!(
                            "The filter '{}' only references columns from the left side of \
                             the JOIN. Moving it before the JOIN (as a subquery or pre-filter) \
                             reduces the number of rows in the join, improving performance.",
                            filter.predicate
                        ),
                        suggestion: Some(
                            "Consider moving the WHERE filter into a subquery or CTE \
                             applied before the JOIN."
                                .to_string(),
                        ),
                        confidence: 0.6,
                    });
                } else if refs_right && !refs_left {
                    findings.push(SemanticFinding {
                        rule: "filter_order".to_string(),
                        severity: SemanticSeverity::Info,
                        message: "Filter applied after JOIN could be pushed down to right side"
                            .to_string(),
                        explanation: format!(
                            "The filter '{}' only references columns from the right side of \
                             the JOIN. Moving it before the JOIN reduces the number of rows \
                             in the join, improving performance.",
                            filter.predicate
                        ),
                        suggestion: Some(
                            "Consider moving the WHERE filter into a subquery or CTE \
                             applied before the JOIN."
                                .to_string(),
                        ),
                        confidence: 0.6,
                    });
                }
            }
        }
    });

    findings
}

// ─── Rule 9: Implicit type coercion ─────────────────────────────────

fn check_implicit_type_coercion(
    plan: &LogicalPlan,
    _schema: &SchemaDefinition,
) -> Vec<SemanticFinding> {
    let mut findings = Vec::new();

    visit_plan(plan, &mut |node| {
        if let LogicalPlan::Filter(filter) = node {
            check_coercion_in_expr(&filter.predicate, &mut findings);
        }
        if let LogicalPlan::Join(join) = node {
            for (left, right) in &join.on {
                check_coercion_between(left, right, &mut findings);
            }
        }
    });

    findings
}

fn check_coercion_in_expr(expr: &Expr, findings: &mut Vec<SemanticFinding>) {
    if let Expr::BinaryExpr(binary) = expr {
        check_coercion_between(&binary.left, &binary.right, findings);
        check_coercion_in_expr(&binary.left, findings);
        check_coercion_in_expr(&binary.right, findings);
    }
}

fn check_coercion_between(left: &Expr, right: &Expr, findings: &mut Vec<SemanticFinding>) {
    // Detect CAST wrapping that DataFusion adds for implicit coercion
    let left_has_cast = matches!(left, Expr::Cast(_) | Expr::TryCast(_));
    let right_has_cast = matches!(right, Expr::Cast(_) | Expr::TryCast(_));

    if left_has_cast || right_has_cast {
        // This is an implicit coercion added by DataFusion
        let left_str = format!("{left}");
        let right_str = format!("{right}");

        // Only flag if one side is a literal and the other is a column
        // (DataFusion typically casts the literal to match the column type)
        if (is_literal(left) && is_column(right)) || (is_literal(right) && is_column(left)) {
            findings.push(SemanticFinding {
                rule: "implicit_type_coercion".to_string(),
                severity: SemanticSeverity::Info,
                message: format!("Implicit type coercion between {left_str} and {right_str}"),
                explanation: "The database will implicitly cast one value to match the other's \
                             type. While this usually works, it can cause unexpected behavior \
                             with edge cases (e.g., string '123abc' cast to integer)."
                    .to_string(),
                suggestion: Some(
                    "Add an explicit CAST to make the type conversion clear.".to_string(),
                ),
                confidence: 0.4,
            });
        }
    }
}

// ─── Rule 10: Self-join without alias ───────────────────────────────

fn check_self_join_without_alias(
    plan: &LogicalPlan,
    _schema: &SchemaDefinition,
) -> Vec<SemanticFinding> {
    let mut findings = Vec::new();

    // Collect all table names referenced in the plan
    let mut table_refs: Vec<String> = Vec::new();
    collect_table_refs(plan, &mut table_refs);

    // Check for duplicate table references
    let mut seen: HashMap<String, usize> = HashMap::new();
    for table in &table_refs {
        *seen.entry(table.clone()).or_insert(0) += 1;
    }

    for (table, count) in &seen {
        if *count > 1 {
            // Check if there are aliases (SubqueryAlias nodes)
            let alias_count = count_aliases_for_table(plan, table);
            if alias_count < *count {
                findings.push(SemanticFinding {
                    rule: "self_join_without_alias".to_string(),
                    severity: SemanticSeverity::Warning,
                    message: format!(
                        "Table '{table}' is referenced {count} times without sufficient aliases"
                    ),
                    explanation: format!(
                        "When the same table '{table}' appears multiple times in a query \
                         (self-join), each reference needs a unique alias for disambiguation. \
                         Without aliases, column references become ambiguous."
                    ),
                    suggestion: Some(format!("Add aliases: {table} AS t1 JOIN {table} AS t2")),
                    confidence: 0.7,
                });
            }
        }
    }

    findings
}

// ─── Helper functions ───────────────────────────────────────────────

/// Visit every node in a LogicalPlan tree.
fn visit_plan<F>(plan: &LogicalPlan, visitor: &mut F)
where
    F: FnMut(&LogicalPlan),
{
    visitor(plan);
    for child in plan.inputs() {
        visit_plan(child, visitor);
    }
}

/// Extract a table name from a plan node (returns alias name if present).
fn extract_table_name(plan: &LogicalPlan) -> Option<String> {
    match plan {
        LogicalPlan::TableScan(scan) => Some(scan.table_name.table().to_string()),
        LogicalPlan::SubqueryAlias(alias) => Some(alias.alias.table().to_string()),
        _ => {
            for child in plan.inputs() {
                if let Some(name) = extract_table_name(child) {
                    return Some(name);
                }
            }
            None
        }
    }
}

/// Extract the underlying base table name from a plan node, skipping aliases.
/// Returns the actual TableScan table name regardless of aliases.
fn extract_base_table_name(plan: &LogicalPlan) -> Option<String> {
    match plan {
        LogicalPlan::TableScan(scan) => Some(scan.table_name.table().to_string()),
        LogicalPlan::SubqueryAlias(alias) => extract_base_table_name(&alias.input),
        _ => {
            for child in plan.inputs() {
                if let Some(name) = extract_base_table_name(child) {
                    return Some(name);
                }
            }
            None
        }
    }
}

/// Extract column name from an expression (if it's a simple Column reference).
fn extract_column_name(expr: &Expr) -> Option<String> {
    match expr {
        Expr::Column(col) => Some(col.name.clone()),
        Expr::Cast(cast) => extract_column_name(&cast.expr),
        _ => None,
    }
}

/// Build a list of (from_table, from_col, to_table, to_col) from foreign keys.
fn build_fk_pairs(schema: &SchemaDefinition) -> Vec<(String, String, String, String)> {
    let mut pairs = Vec::new();
    for (table_name, table_def) in &schema.tables {
        for fk in &table_def.foreign_keys {
            for (i, col) in fk.columns.iter().enumerate() {
                if let Some(ref_col) = fk.references.columns.get(i) {
                    pairs.push((
                        table_name.clone(),
                        col.clone(),
                        fk.references.table.clone(),
                        ref_col.clone(),
                    ));
                }
            }
        }
    }
    pairs
}

/// Check if a column is a primary key in the schema.
fn is_primary_key(schema: &SchemaDefinition, table: &str, column: &str) -> bool {
    schema
        .tables
        .get(table)
        .and_then(|t| t.primary_key.as_ref())
        .map(|pk| pk.iter().any(|c| c == column))
        .unwrap_or(false)
}

/// Suggest the correct join key between two tables based on FK relationships.
fn suggest_correct_join_key(
    schema: &SchemaDefinition,
    table_a: &str,
    table_b: &str,
) -> Option<String> {
    let fk_pairs = build_fk_pairs(schema);
    for (ft, fc, rt, rc) in &fk_pairs {
        if (ft == table_a && rt == table_b) || (ft == table_b && rt == table_a) {
            return Some(format!("Use the FK relationship: {ft}.{fc} = {rt}.{rc}"));
        }
    }
    None
}

/// Check if an expression contains an aggregate function.
fn contains_aggregate(expr: &Expr) -> bool {
    match expr {
        Expr::AggregateFunction(_) => true,
        Expr::Alias(alias) => contains_aggregate(&alias.expr),
        Expr::BinaryExpr(b) => contains_aggregate(&b.left) || contains_aggregate(&b.right),
        Expr::Cast(c) => contains_aggregate(&c.expr),
        _ => false,
    }
}

/// Check if an expression is a bare column reference (not wrapped in aggregate).
fn is_bare_column(expr: &Expr) -> bool {
    match expr {
        Expr::Column(_) => true,
        Expr::Alias(alias) => is_bare_column(&alias.expr),
        _ => false,
    }
}

/// Check if a plan node is or contains an Aggregate node.
fn has_aggregate_child(plan: &LogicalPlan) -> bool {
    match plan {
        LogicalPlan::Aggregate(_) => true,
        _ => plan.inputs().iter().any(|child| has_aggregate_child(child)),
    }
}

/// Check if there's a Join node below this plan node.
fn has_join_below(plan: &LogicalPlan) -> bool {
    match plan {
        LogicalPlan::Join(_) => true,
        _ => plan.inputs().iter().any(|child| has_join_below(child)),
    }
}

/// Find one-to-many joins by checking FK relationships in the plan.
fn find_one_to_many_joins(plan: &LogicalPlan, schema: &SchemaDefinition) -> Vec<(String, String)> {
    let mut results = Vec::new();
    let fk_pairs = build_fk_pairs(schema);

    visit_plan(plan, &mut |node| {
        if let LogicalPlan::Join(join) = node {
            // Use base table names (not aliases) for FK lookup
            let left_table = extract_base_table_name(&join.left);
            let right_table = extract_base_table_name(&join.right);

            if let (Some(ref lt), Some(ref rt)) = (left_table, right_table) {
                // Check if there's a FK from right -> left (right is the "many" side)
                let right_refs_left = fk_pairs
                    .iter()
                    .any(|(ft, _, reft, _)| ft == rt && reft == lt);
                if right_refs_left {
                    results.push((lt.clone(), rt.clone()));
                }

                // Check if there's a FK from left -> right (left is the "many" side)
                let left_refs_right = fk_pairs
                    .iter()
                    .any(|(ft, _, reft, _)| ft == lt && reft == rt);
                if left_refs_right {
                    results.push((rt.clone(), lt.clone()));
                }
            }
        }
    });

    results
}

/// Extract column references from an expression.
fn extract_column_refs(expr: &Expr) -> HashSet<String> {
    let mut refs = HashSet::new();
    collect_columns(expr, &mut refs);
    refs
}

fn collect_columns(expr: &Expr, out: &mut HashSet<String>) {
    match expr {
        Expr::Column(col) => {
            // Use qualified name if available
            if let Some(ref relation) = col.relation {
                out.insert(format!("{}.{}", relation, col.name));
            } else {
                out.insert(col.name.clone());
            }
        }
        Expr::BinaryExpr(b) => {
            collect_columns(&b.left, out);
            collect_columns(&b.right, out);
        }
        Expr::Not(inner) => collect_columns(inner, out),
        Expr::IsNull(inner) | Expr::IsNotNull(inner) => collect_columns(inner, out),
        Expr::Alias(alias) => collect_columns(&alias.expr, out),
        Expr::Cast(cast) => collect_columns(&cast.expr, out),
        _ => {}
    }
}

/// Collect all output column names (qualified) from a plan.
fn collect_table_columns(plan: &LogicalPlan) -> HashSet<String> {
    let mut cols = HashSet::new();
    let schema = plan.schema();
    for (qualifier, field) in schema.iter() {
        if let Some(q) = qualifier {
            cols.insert(format!("{q}.{}", field.name()));
        }
        cols.insert(field.name().to_string());
    }
    cols
}

/// Collect all table scan references in a plan.
fn collect_table_refs(plan: &LogicalPlan, refs: &mut Vec<String>) {
    if let LogicalPlan::TableScan(scan) = plan {
        refs.push(scan.table_name.table().to_string());
    }
    for child in plan.inputs() {
        collect_table_refs(child, refs);
    }
}

/// Count SubqueryAlias nodes that reference a specific table.
fn count_aliases_for_table(plan: &LogicalPlan, _table: &str) -> usize {
    let mut count = 0;
    visit_plan(plan, &mut |node| {
        if let LogicalPlan::SubqueryAlias(_) = node {
            count += 1;
        }
    });
    count
}

/// Check if an expression is a literal value.
fn is_literal(expr: &Expr) -> bool {
    matches!(expr, Expr::Literal(_))
}

/// Check if an expression is or wraps a column reference.
fn is_column(expr: &Expr) -> bool {
    match expr {
        Expr::Column(_) => true,
        Expr::Cast(c) => is_column(&c.expr),
        Expr::TryCast(c) => is_column(&c.expr),
        _ => false,
    }
}
