use pg_query::NodeEnum;
use text_size::TextRange;

use super::fk;
use super::type_names::{type_name_to_qn, type_name_to_string};
use crate::core::ddl::util::deparse_expr;
use crate::core::schema::ColumnDef;

pub(crate) fn build_column_def(
    col: &pg_query::protobuf::ColumnDef,
    stmt_start: text_size::TextSize,
) -> (String, ColumnDef) {
    let ty = col
        .type_name
        .as_ref()
        .map(type_name_to_string)
        .unwrap_or_default();
    let type_ref = col.type_name.as_ref().and_then(type_name_to_qn);
    let nullable = !is_not_null(col);
    let fk_target = fk::extract_inline_fk(col);
    let default_expr = extract_default_expr(col, &col.colname);
    (
        col.colname.clone(),
        ColumnDef {
            ty,
            nullable,
            range: column_name_range(col, stmt_start),
            doc: None,
            type_ref,
            fk_target,
            default_expr,
        },
    )
}

fn extract_default_expr(col: &pg_query::protobuf::ColumnDef, owner: &str) -> Option<String> {
    use pg_query::protobuf::ConstrType;
    for node in &col.constraints {
        let Some(NodeEnum::Constraint(c)) = node.node.as_ref() else {
            continue;
        };
        if ConstrType::try_from(c.contype) != Ok(ConstrType::ConstrDefault) {
            continue;
        }
        if let Some(raw) = c.raw_expr.as_deref().filter(|n| n.node.is_some()) {
            match deparse_expr(raw) {
                Ok(text) => return Some(text),
                Err(err) => {
                    tracing::warn!(
                        column = owner,
                        %err,
                        "deparse of column default failed, skipping",
                    );
                    continue;
                }
            }
        }
        if !c.cooked_expr.is_empty() {
            return Some(c.cooked_expr.clone());
        }
    }
    None
}

fn column_name_range(
    col: &pg_query::protobuf::ColumnDef,
    stmt_start: text_size::TextSize,
) -> TextRange {
    if col.location < 0 {
        return TextRange::default();
    }
    #[allow(clippy::cast_sign_loss)]
    let start = stmt_start + text_size::TextSize::from(col.location as u32);
    let type_offset = col
        .type_name
        .as_ref()
        .filter(|tn| tn.location >= 0)
        .map(|tn| tn.location);
    let end = match type_offset {
        #[allow(clippy::cast_sign_loss)]
        Some(loc) => stmt_start + text_size::TextSize::from(loc as u32),
        None => start,
    };
    TextRange::new(start, end)
}

fn is_not_null(col: &pg_query::protobuf::ColumnDef) -> bool {
    use pg_query::protobuf::ConstrType;
    if col.is_not_null {
        return true;
    }
    col.constraints.iter().any(|node| {
        matches!(
            node.node.as_ref(),
            Some(NodeEnum::Constraint(c))
                if ConstrType::try_from(c.contype) == Ok(ConstrType::ConstrNotnull)
        )
    })
}

#[cfg(test)]
mod tests {
    use crate::core::schema::{QualifiedName, SchemaModel};
    use crate::parse::split_and_parse;

    fn walk(sql: &str) -> SchemaModel {
        let (stmts, _) = split_and_parse(sql).expect("must parse");
        let mut model = SchemaModel::default();
        crate::core::ddl::walk_ddl(&stmts, &mut model);
        model
    }

    #[test]
    fn not_null_column_is_non_nullable() {
        let model = walk("CREATE TABLE public.t1 (id bigint NOT NULL);");
        assert!(!model.tables[&QualifiedName::new(Some("public"), "t1")].columns["id"].nullable);
    }

    #[test]
    fn nullable_column_has_no_not_null_constraint() {
        let model = walk("CREATE TABLE public.t1 (name text);");
        assert!(model.tables[&QualifiedName::new(Some("public"), "t1")].columns["name"].nullable);
    }

    #[test]
    fn column_with_schema_prefix_sets_type_ref() {
        let model = walk(concat!(
            "CREATE TYPE public.item_color AS ENUM ('red', 'green');",
            "CREATE TABLE public.t1 (id bigint NOT NULL, color public.item_color NOT NULL);",
        ));
        let col = &model.tables[&QualifiedName::new(Some("public"), "t1")].columns["color"];
        let tr = col.type_ref.as_ref().expect("must have type_ref");
        assert_eq!(tr.schema, "public");
        assert_eq!(tr.name, "item_color");
    }

    #[test]
    fn builtin_column_has_no_type_ref() {
        let model = walk("CREATE TABLE public.t1 (id bigint NOT NULL);");
        let col = &model.tables[&QualifiedName::new(Some("public"), "t1")].columns["id"];
        assert!(col.type_ref.is_none());
    }

    #[test]
    fn column_range_start_is_nonzero_for_named_column() {
        let model = walk("CREATE TABLE public.t1 (id bigint NOT NULL);");
        let col = &model.tables[&QualifiedName::new(Some("public"), "t1")].columns["id"];
        assert!(
            col.range.start() > text_size::TextSize::from(0u32),
            "column range start must reflect actual source position, not default zero"
        );
    }

    #[test]
    fn column_range_end_is_after_start() {
        let model = walk("CREATE TABLE public.t1 (id bigint NOT NULL);");
        let col = &model.tables[&QualifiedName::new(Some("public"), "t1")].columns["id"];
        assert!(
            col.range.end() > col.range.start(),
            "column range end must be after start"
        );
    }

    #[test]
    fn second_column_range_is_after_first() {
        let model = walk("CREATE TABLE public.t1 (id bigint NOT NULL, name text);");
        let t = &model.tables[&QualifiedName::new(Some("public"), "t1")];
        let id_range = t.columns["id"].range;
        let name_range = t.columns["name"].range;
        assert!(
            name_range.start() > id_range.start(),
            "second column must start after first column"
        );
    }

    #[test]
    fn column_default_now() {
        let model = walk("CREATE TABLE public.t (created_at timestamptz NOT NULL DEFAULT now());");
        let col = &model.tables[&QualifiedName::new(Some("public"), "t")].columns["created_at"];
        assert_eq!(col.default_expr.as_deref(), Some("now()"));
    }

    #[test]
    fn column_default_gen_random_uuid() {
        let model = walk("CREATE TABLE public.t (id uuid PRIMARY KEY DEFAULT gen_random_uuid());");
        let col = &model.tables[&QualifiedName::new(Some("public"), "t")].columns["id"];
        assert_eq!(col.default_expr.as_deref(), Some("gen_random_uuid()"));
    }

    #[test]
    fn column_default_string_literal() {
        let model = walk("CREATE TABLE public.t (status text NOT NULL DEFAULT 'pending');");
        let col = &model.tables[&QualifiedName::new(Some("public"), "t")].columns["status"];
        assert_eq!(col.default_expr.as_deref(), Some("'pending'"));
    }

    #[test]
    fn column_default_integer_literal() {
        let model = walk("CREATE TABLE public.t (retries integer NOT NULL DEFAULT 0);");
        let col = &model.tables[&QualifiedName::new(Some("public"), "t")].columns["retries"];
        assert_eq!(col.default_expr.as_deref(), Some("0"));
    }

    #[test]
    fn column_without_default() {
        let model = walk("CREATE TABLE public.t (note text);");
        let col = &model.tables[&QualifiedName::new(Some("public"), "t")].columns["note"];
        assert_eq!(col.default_expr, None);
    }
}
