//! Detect SELECT * usage.

use crate::tagger::types::{TagReport, TagResult};
use crate::tagger::walk::find_all_selects;
use polyglot_sql::dialects::DialectType;
use polyglot_sql::expressions::Expression;

pub const TAG_NAME: &str = "select_star";

/// Check if an expression wraps a Star as its primary child.
fn inner_is_star(expr: &Expression) -> bool {
    match expr {
        Expression::Count(c) => c.star || matches!(&c.this, Some(Expression::Star(_))),
        Expression::Sum(a)
        | Expression::Avg(a)
        | Expression::Min(a)
        | Expression::Max(a)
        | Expression::ArrayAgg(a)
        | Expression::CountIf(a)
        | Expression::Stddev(a)
        | Expression::StddevPop(a)
        | Expression::StddevSamp(a)
        | Expression::Variance(a)
        | Expression::VarPop(a)
        | Expression::VarSamp(a)
        | Expression::Median(a)
        | Expression::Mode(a)
        | Expression::First(a)
        | Expression::Last(a)
        | Expression::AnyValue(a)
        | Expression::ApproxDistinct(a)
        | Expression::ApproxCountDistinct(a)
        | Expression::LogicalAnd(a)
        | Expression::LogicalOr(a)
        | Expression::Skewness(a) => {
            matches!(&a.this, Expression::Star(_))
        }
        Expression::Function(f) => f
            .args
            .first()
            .is_some_and(|a| matches!(a, Expression::Star(_))),
        Expression::AggregateFunction(f) => f
            .args
            .first()
            .is_some_and(|a| matches!(a, Expression::Star(_))),
        _ => false,
    }
}

fn build_query(columns: &[String], position: usize, table: &str) -> String {
    let max_len = 1000;
    let mut query_span = String::from("SELECT ");
    let mut star_position = 0;

    for (i, col) in columns.iter().enumerate() {
        if i == position {
            star_position = query_span.len();
        }
        if i == 0 {
            query_span.push_str(col);
        } else {
            query_span.push_str(", ");
            query_span.push_str(col);
        }
    }

    query_span.push(' ');
    query_span.push_str(table);

    if query_span.len() > max_len {
        let half = max_len / 2;
        let mut start = star_position.saturating_sub(half);
        let mut end = (star_position + half).min(query_span.len());
        while start > 0 && !query_span.is_char_boundary(start) {
            start -= 1;
        }
        while end < query_span.len() && !query_span.is_char_boundary(end) {
            end += 1;
        }
        query_span = query_span[start..end].to_string();
    }

    query_span
}

pub fn check(expr: &Expression, dialect: DialectType) -> TagResult {
    let selects = find_all_selects(expr);
    let mut reports = Vec::new();

    for select_node in selects {
        let sel = match select_node {
            Expression::Select(s) => s,
            _ => continue,
        };

        let mut contains_star = false;
        let mut columns: Vec<String> = Vec::new();
        let mut first_star_pos: Option<usize> = None;

        for (i, col_expr) in sel.expressions.iter().enumerate() {
            match col_expr {
                Expression::Star(star) => {
                    contains_star = true;
                    if first_star_pos.is_none() {
                        first_star_pos = Some(i);
                    }
                    if let Some(ref tbl) = star.table {
                        columns.push(format!("{}.*", tbl.name));
                    } else {
                        columns.push("*".to_string());
                    }
                }
                Expression::Alias(alias) if matches!(&alias.this, Expression::Star(_)) => {
                    contains_star = true;
                    if first_star_pos.is_none() {
                        first_star_pos = Some(i);
                    }
                    if let Expression::Star(star) = &alias.this {
                        if let Some(ref tbl) = star.table {
                            columns.push(format!("{}.*", tbl.name));
                        } else {
                            columns.push("*".to_string());
                        }
                    }
                }
                other if inner_is_star(other) => {
                    contains_star = true;
                    if first_star_pos.is_none() {
                        first_star_pos = Some(i);
                    }
                    columns.push("*".to_string());
                }
                _ => {
                    columns.push(col_expr.sql_for(dialect));
                }
            }
        }

        if contains_star {
            let pos = first_star_pos.unwrap_or(0);
            let table_str = match &sel.from {
                Some(from) => Expression::From(Box::new(from.clone())).sql_for(dialect),
                None => String::new(),
            };

            let query_span = build_query(&columns, pos, &table_str);
            reports.push(TagReport {
                query_span,
                functions: None,
                description: None,
                metadata: None,
            });
        }
    }

    if reports.is_empty() {
        TagResult::not_triggered(TAG_NAME)
    } else {
        TagResult {
            tag_name: TAG_NAME.to_string(),
            triggered: true,
            reports,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn with_large_stack<F: FnOnce() + Send + 'static>(f: F) {
        std::thread::Builder::new()
            .stack_size(32 * 1024 * 1024)
            .spawn(f)
            .unwrap()
            .join()
            .unwrap();
    }

    #[test]
    fn triggered_on_select_star() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one("SELECT * FROM t", DialectType::Snowflake).unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(r.triggered);
            assert_eq!(r.reports.len(), 1);
            assert!(r.reports[0].query_span.contains('*'));
        });
    }

    #[test]
    fn not_triggered_with_explicit_columns() {
        with_large_stack(|| {
            let expr =
                polyglot_sql::parse_one("SELECT a, b FROM t", DialectType::Snowflake).unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(!r.triggered);
        });
    }

    #[test]
    fn triggered_in_nested_subquery() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one(
                "SELECT a FROM (SELECT * FROM t2) sub",
                DialectType::Snowflake,
            )
            .unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(r.triggered);
        });
    }

    #[test]
    fn triggered_on_count_star() {
        with_large_stack(|| {
            let expr =
                polyglot_sql::parse_one("SELECT COUNT(*) FROM t", DialectType::Snowflake).unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(r.triggered);
        });
    }

    #[test]
    fn triggered_in_create_table_as_select() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one(
                "CREATE TABLE x AS SELECT * FROM t",
                DialectType::Snowflake,
            )
            .unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(r.triggered);
        });
    }
}
