//! Schema-free metadata extraction from SQL using polyglot-sql.
//!
//! Extracts tables, columns, functions, and structural signals from SQL
//! without requiring a schema definition. This enables the "millions of tables"
//! use case where you want to know what a query references before loading schemas.
//!
//! Note: polyglot-sql's AST traversal functions use deep recursion that can overflow
//! the default 8MB thread stack. All extraction functions run on a thread with a
//! larger stack (32MB) to avoid this.

use super::dialect_map::to_polyglot_dialect;
use super::types::ExtractResult;
use crate::schema::types::SqlDialect;
use polyglot_sql::ast_transforms;
use polyglot_sql::expressions::Expression;

/// Stack size for AST traversal threads (32MB).
/// polyglot-sql's recursive AST walkers can overflow the default 8MB stack.
const AST_THREAD_STACK_SIZE: usize = 32 * 1024 * 1024;

/// Extract full metadata from a SQL query: tables, columns, functions, and structural signals.
///
/// This does NOT require a schema — it parses the SQL and walks the AST to extract
/// all referenced identifiers and structural properties.
pub fn extract_metadata(sql: &str, dialect: SqlDialect) -> Result<ExtractResult, String> {
    let sql = sql.to_string();
    std::thread::Builder::new()
        .stack_size(AST_THREAD_STACK_SIZE)
        .spawn(move || extract_metadata_inner(&sql, dialect))
        .map_err(|e| format!("failed to spawn extraction thread: {e}"))?
        .join()
        .map_err(|_| "extraction thread panicked".to_string())?
}

/// Inner implementation of extract_metadata, runs on a thread with a larger stack.
fn extract_metadata_inner(sql: &str, dialect: SqlDialect) -> Result<ExtractResult, String> {
    let pg_dialect = to_polyglot_dialect(dialect);
    let expressions = polyglot_sql::parse(sql, pg_dialect).map_err(|e| e.to_string())?;

    let mut all_tables = Vec::new();
    let mut all_columns = Vec::new();
    let mut all_functions = Vec::new();
    let mut has_subqueries = false;
    let mut has_aggregation = false;
    let mut has_window_functions = false;
    let mut total_nodes = 0;

    for expr in &expressions {
        let tables = ast_transforms::get_table_names(expr);
        let columns = ast_transforms::get_column_names(expr);
        let functions = ast_transforms::get_functions(expr);
        let subqueries = ast_transforms::get_subqueries(expr);
        let agg_functions = ast_transforms::get_aggregate_functions(expr);
        let window_functions = ast_transforms::get_window_functions(expr);
        let nodes = ast_transforms::node_count(expr);

        all_tables.extend(tables);
        all_columns.extend(columns);
        all_functions.extend(functions.iter().map(|f| format!("{f:?}")));
        has_subqueries = has_subqueries || !subqueries.is_empty();
        has_aggregation = has_aggregation || !agg_functions.is_empty();
        has_window_functions = has_window_functions || !window_functions.is_empty();
        total_nodes += nodes;
    }

    // Deduplicate
    all_tables.sort();
    all_tables.dedup();
    all_columns.sort();
    all_columns.dedup();
    all_functions.sort();
    all_functions.dedup();

    Ok(ExtractResult {
        tables: all_tables,
        columns: all_columns,
        functions: all_functions,
        has_subqueries,
        has_aggregation,
        has_window_functions,
        node_count: total_nodes,
    })
}

/// Extract only table names from a SQL query (no schema required).
pub fn extract_tables(sql: &str, dialect: SqlDialect) -> Result<Vec<String>, String> {
    let sql = sql.to_string();
    std::thread::Builder::new()
        .stack_size(AST_THREAD_STACK_SIZE)
        .spawn(move || extract_tables_inner(&sql, dialect))
        .map_err(|e| format!("failed to spawn extraction thread: {e}"))?
        .join()
        .map_err(|_| "extraction thread panicked".to_string())?
}

/// Inner implementation of extract_tables.
fn extract_tables_inner(sql: &str, dialect: SqlDialect) -> Result<Vec<String>, String> {
    let pg_dialect = to_polyglot_dialect(dialect);
    let expressions = polyglot_sql::parse(sql, pg_dialect).map_err(|e| e.to_string())?;

    let mut tables = Vec::new();
    for expr in &expressions {
        tables.extend(ast_transforms::get_table_names(expr));
    }
    tables.sort();
    tables.dedup();
    Ok(tables)
}

/// Extract the output column names from a SQL SELECT statement.
///
/// Returns the alias or column name for each item in the SELECT list, preserving
/// order. Unlike [`extract_columns`] which returns all *referenced* columns across
/// the entire query, this returns only the *output projection* names.
///
/// Errors if the query contains `SELECT *` or expressions without a deterministic
/// output name (e.g. bare literals or function calls without an alias).
pub fn extract_output_columns(sql: &str, dialect: SqlDialect) -> Result<Vec<String>, String> {
    let sql = sql.to_string();
    std::thread::Builder::new()
        .stack_size(AST_THREAD_STACK_SIZE)
        .spawn(move || extract_output_columns_inner(&sql, dialect))
        .map_err(|e| format!("failed to spawn extraction thread: {e}"))?
        .join()
        .map_err(|_| "extraction thread panicked".to_string())?
}

/// Inner implementation of extract_output_columns.
fn extract_output_columns_inner(sql: &str, dialect: SqlDialect) -> Result<Vec<String>, String> {
    let pg_dialect = to_polyglot_dialect(dialect);
    let expressions = polyglot_sql::parse(sql, pg_dialect).map_err(|e| e.to_string())?;

    let select = expressions
        .iter()
        .find_map(|expr| expr.as_select())
        .ok_or_else(|| "no SELECT statement found in SQL".to_string())?;

    let mut columns = Vec::new();
    for expr in &select.expressions {
        match expr {
            Expression::Alias(alias) => {
                columns.push(alias.alias.name.clone());
            }
            Expression::Column(col) => {
                columns.push(col.name.name.clone());
            }
            Expression::Identifier(id) => {
                columns.push(id.name.clone());
            }
            Expression::Star(_) => {
                return Err(format!(
                    "unable to extract output columns due to star: {expr}"
                ));
            }
            other => {
                return Err(format!(
                    "unable to determine output column name for expression: {other}"
                ));
            }
        }
    }

    Ok(columns)
}

/// Extract only column names from a SQL query (no schema required).
pub fn extract_columns(sql: &str, dialect: SqlDialect) -> Result<Vec<String>, String> {
    let sql = sql.to_string();
    std::thread::Builder::new()
        .stack_size(AST_THREAD_STACK_SIZE)
        .spawn(move || extract_columns_inner(&sql, dialect))
        .map_err(|e| format!("failed to spawn extraction thread: {e}"))?
        .join()
        .map_err(|_| "extraction thread panicked".to_string())?
}

/// Inner implementation of extract_columns.
fn extract_columns_inner(sql: &str, dialect: SqlDialect) -> Result<Vec<String>, String> {
    let pg_dialect = to_polyglot_dialect(dialect);
    let expressions = polyglot_sql::parse(sql, pg_dialect).map_err(|e| e.to_string())?;

    let mut columns = Vec::new();
    for expr in &expressions {
        columns.extend(ast_transforms::get_column_names(expr));
    }
    columns.sort();
    columns.dedup();
    Ok(columns)
}
