//! Resolves SQL identifiers against a [`SchemaModel`]. Each statement gets its own [`ScopeStack`].
mod body;
mod emit;
mod emit_ddl;
mod lex;
mod scope;
mod walker;

use text_size::TextRange;

use crate::{core::schema::SchemaModel, parse::ParsedStatement};

pub use scope::ScopeStack;

/// A resolved reference to a schema symbol with its file location.
#[derive(Clone, Debug)]
pub struct ResolvedRef {
    /// Canonical symbol string, e.g. `"public/users"` or `"public/users#email"`.
    pub symbol: String,
    /// Byte range of the reference identifier in the source file.
    pub range: TextRange,
    /// Byte range of the enclosing SQL statement that contains this reference.
    pub statement_range: TextRange,
}

/// Resolves every identifier in `stmts` against `model`. `source` is needed for
/// lexer-backed lookups (index columns, FK attrs). Unresolved refs are logged at DEBUG and skipped.
#[must_use]
pub fn walk_references(
    stmts: &[ParsedStatement],
    source: &str,
    model: &SchemaModel,
) -> Vec<ResolvedRef> {
    let mut out = Vec::new();
    for stmt in stmts {
        // DDL pass: FK, column types, function return types/bodies, trigger/policy targets
        emit_ddl::emit_ddl_stmt(stmt, source, model, &mut out);
        // generic pass: iterator-visible Vec<Node> fields
        let scope = scope::build_scope(&stmt.root, model);
        emit::emit_stmt(stmt, &scope, model, &mut out);
    }
    out
}

#[cfg(test)]
mod tests {
    use indexmap::IndexMap;

    use super::*;
    use crate::core::schema::{ColumnDef, QualifiedName, SchemaModel, TableDef};

    fn make_model_with_table(schema: &str, name: &str, cols: &[&str]) -> SchemaModel {
        let mut model = SchemaModel::default();
        let key = QualifiedName::new(Some(schema), name);
        let columns: IndexMap<String, ColumnDef> = cols
            .iter()
            .map(|c| {
                (
                    (*c).to_owned(),
                    ColumnDef {
                        ty: "text".into(),
                        nullable: true,
                        range: TextRange::default(),
                        doc: None,
                        type_ref: None,
                        fk_target: None,
                        default_expr: None,
                    },
                )
            })
            .collect();
        model.tables.insert(
            key,
            TableDef {
                columns,
                range: TextRange::default(),
                doc: None,
            },
        );
        model
    }

    #[test]
    fn scope_stack_registers_alias() {
        let mut stack = ScopeStack::default();
        let qn = QualifiedName::new(Some("public"), "orders");
        stack.aliases.insert("o".to_owned(), qn.clone());
        assert_eq!(stack.aliases.get("o"), Some(&qn));
    }

    #[test]
    fn cte_name_shadows_table() {
        let mut stack = ScopeStack::default();
        stack.ctes.insert("recent".to_owned());
        assert!(stack.ctes.contains("recent"));
    }

    #[test]
    fn bare_column_single_table_resolves() {
        let model = make_model_with_table("public", "users", &["email"]);
        let qn = QualifiedName::new(Some("public"), "users");
        let mut stack = ScopeStack::default();
        stack.from_scope.push(qn.clone());

        let result = scope::resolve_bare_col(&stack, "email", &model);
        assert_eq!(result, Some((qn, "email".to_owned())));
    }

    #[test]
    fn bare_column_multiple_tables_skipped() {
        let model = make_model_with_table("public", "users", &["id"]);
        let mut stack = ScopeStack::default();
        stack
            .from_scope
            .push(QualifiedName::new(Some("public"), "users"));
        stack
            .from_scope
            .push(QualifiedName::new(Some("public"), "orders"));

        let result = scope::resolve_bare_col(&stack, "id", &model);
        assert_eq!(result, None);
    }

    #[test]
    fn walk_references_does_not_panic_on_empty_stmts() {
        let model = SchemaModel::default();
        let refs = walk_references(&[], "", &model);
        assert!(refs.is_empty());
    }
}
