use pg_query::NodeEnum;
use text_size::{TextRange, TextSize};

use crate::core::ddl::util::parse_name;

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

use super::walker::{walk_all_nodes, walk_node};

pub(super) fn emit_stmt(
    stmt: &ParsedStatement,
    stack: &scope::ScopeStack,
    model: &SchemaModel,
    out: &mut Vec<ResolvedRef>,
) {
    walk_all_nodes(&stmt.root, &mut |node| {
        emit_inner(node, stmt.range, stack, model, out);
    });
}

// stmt_range is the enclosing statement's absolute byte range in the source file
pub(super) fn emit_node(
    node: &pg_query::protobuf::Node,
    stmt_range: TextRange,
    scope: &scope::ScopeStack,
    model: &SchemaModel,
    out: &mut Vec<ResolvedRef>,
) {
    walk_node(node, &mut |inner| {
        emit_inner(inner, stmt_range, scope, model, out);
    });
}

fn emit_inner(
    node: &NodeEnum,
    stmt_range: TextRange,
    scope: &scope::ScopeStack,
    model: &SchemaModel,
    out: &mut Vec<ResolvedRef>,
) {
    match node {
        NodeEnum::RangeVar(rv) => emit_range_var(rv, stmt_range, scope, model, out),
        NodeEnum::ColumnRef(cr) => emit_column_ref(cr, stmt_range, scope, model, out),
        NodeEnum::FuncCall(fc) => emit_func_call(fc, stmt_range, model, out),
        NodeEnum::TypeCast(tc) => {
            if let Some(tn) = tc.type_name.as_ref() {
                emit_type_name(tn, stmt_range, model, out);
            }
        }
        _ => {}
    }
}

pub(super) fn loc_to_size(location: i32) -> TextSize {
    TextSize::from(u32::try_from(location.max(0)).unwrap_or(0))
}

pub(super) fn len_to_size(len: usize) -> TextSize {
    TextSize::from(u32::try_from(len).unwrap_or(0))
}

fn emit_range_var(
    rv: &pg_query::protobuf::RangeVar,
    stmt_range: TextRange,
    stack: &scope::ScopeStack,
    model: &SchemaModel,
    out: &mut Vec<ResolvedRef>,
) {
    if rv.relname.is_empty() || stack.ctes.contains(&rv.relname) {
        return;
    }
    let Some(qn) = scope::resolve_range_var(rv, model) else {
        return;
    };
    let abs_start = stmt_range.start() + loc_to_size(rv.location);
    let abs_end = abs_start + len_to_size(rv.relname.len());
    out.push(ResolvedRef {
        symbol: qn.to_string(),
        range: TextRange::new(abs_start, abs_end),
        statement_range: stmt_range,
    });
}

fn emit_column_ref(
    cr: &pg_query::protobuf::ColumnRef,
    stmt_range: TextRange,
    stack: &scope::ScopeStack,
    model: &SchemaModel,
    out: &mut Vec<ResolvedRef>,
) {
    let names = extract_col_field_names(&cr.fields);
    let resolved = match names.as_slice() {
        [table_or_alias, col] => scope::resolve_qualified_col(stack, table_or_alias, col, model),
        [col] => scope::resolve_bare_col(stack, col, model),
        _ => {
            tracing::debug!("ColumnRef has unexpected field count, skipping");
            return;
        }
    };
    let Some((qn, col)) = resolved else { return };
    let col_name = names.last().expect("names is non-empty; checked above");
    let abs_start = stmt_range.start() + loc_to_size(cr.location);
    let abs_end = abs_start + len_to_size(col_name.len());
    out.push(ResolvedRef {
        symbol: format!("{qn}#{col}"),
        range: TextRange::new(abs_start, abs_end),
        statement_range: stmt_range,
    });
}

fn extract_col_field_names(fields: &[pg_query::protobuf::Node]) -> Vec<String> {
    fields
        .iter()
        .filter_map(|n| n.node.as_ref())
        .filter_map(|n| {
            if let NodeEnum::String(s) = n {
                Some(s.sval.clone())
            } else {
                None
            }
        })
        .collect()
}

fn emit_func_call(
    fc: &pg_query::protobuf::FuncCall,
    stmt_range: TextRange,
    model: &SchemaModel,
    out: &mut Vec<ResolvedRef>,
) {
    let Some((schema, name)) = parse_name(&fc.funcname) else {
        tracing::debug!("FuncCall has no parseable name, skipping");
        return;
    };
    let qn = crate::core::schema::QualifiedName::new(schema.as_deref(), &name);
    if !model.functions.contains_key(&qn) {
        tracing::debug!(func = %qn, "FuncCall not found in schema, skipping");
        return;
    }
    let abs_start = stmt_range.start() + loc_to_size(fc.location);
    let abs_end = abs_start + len_to_size(name.len());
    out.push(ResolvedRef {
        symbol: qn.to_string(),
        range: TextRange::new(abs_start, abs_end),
        statement_range: stmt_range,
    });
}

/// Built-in `PostgreSQL` type names that should not generate references.
const BUILTIN_TYPE_NAMES: &[&str] = &[
    "int2",
    "int4",
    "int8",
    "float4",
    "float8",
    "bool",
    "text",
    "bytea",
    "char",
    "bpchar",
    "varchar",
    "numeric",
    "date",
    "time",
    "timetz",
    "timestamp",
    "timestamptz",
    "interval",
    "json",
    "jsonb",
    "uuid",
    "oid",
    "name",
    "void",
    "record",
    "any",
    "anyarray",
    "anyelement",
    "trigger",
];

fn emit_type_name(
    tn: &pg_query::protobuf::TypeName,
    stmt_range: TextRange,
    model: &SchemaModel,
    out: &mut Vec<ResolvedRef>,
) {
    let Some((schema, name)) = parse_name(&tn.names) else {
        tracing::debug!("TypeName has no parseable name, skipping");
        return;
    };
    if schema.as_deref() == Some("pg_catalog") || BUILTIN_TYPE_NAMES.contains(&name.as_str()) {
        return;
    }
    let qn = crate::core::schema::QualifiedName::new(schema.as_deref(), &name);
    if !model.types.contains_key(&qn) {
        tracing::debug!(ty = %qn, "TypeName not found in schema types, skipping");
        return;
    }
    let abs_start = stmt_range.start() + loc_to_size(tn.location);
    let abs_end = abs_start + len_to_size(name.len());
    out.push(ResolvedRef {
        symbol: qn.to_string(),
        range: TextRange::new(abs_start, abs_end),
        statement_range: stmt_range,
    });
}

// not via iterator — used for column types and function return types in emit_ddl
pub(super) fn emit_type_name_direct(
    tn: &pg_query::protobuf::TypeName,
    stmt_range: TextRange,
    model: &SchemaModel,
    out: &mut Vec<ResolvedRef>,
) {
    emit_type_name(tn, stmt_range, model, out);
}

// not via iterator — used for FK targets, trigger targets, policy targets in emit_ddl
pub(super) fn emit_range_var_direct(
    rv: &pg_query::protobuf::RangeVar,
    stmt_range: TextRange,
    model: &SchemaModel,
    out: &mut Vec<ResolvedRef>,
) {
    emit_range_var(
        rv,
        stmt_range,
        &super::scope::ScopeStack::default(),
        model,
        out,
    );
}
