use pg_query::protobuf::Node;
use pg_query::{NodeEnum, protobuf::ObjectType};
use text_size::TextRange;

use super::util::{deparse_expr, parse_name};

use crate::core::schema::{
    IndexDef, IndexTarget, PolicyDef, QualifiedName, SchemaModel, TriggerDef,
};

use super::tables::range_var_to_key;

pub(super) fn handle_comment(stmt: &pg_query::protobuf::CommentStmt, model: &mut SchemaModel) {
    let Ok(objtype) = ObjectType::try_from(stmt.objtype) else {
        tracing::debug!(
            objtype = stmt.objtype,
            "COMMENT ON unknown object type, skipping"
        );
        return;
    };
    let comment = stmt.comment.clone();
    let object = stmt.object.as_ref().and_then(|b| b.node.as_ref());

    match objtype {
        ObjectType::ObjectTable => attach_table_comment(object, comment, model),
        ObjectType::ObjectColumn => attach_column_comment(object, comment, model),
        ObjectType::ObjectFunction | ObjectType::ObjectProcedure => {
            attach_function_comment(object, comment, model);
        }
        ObjectType::ObjectView => attach_view_comment(object, comment, model),
        ObjectType::ObjectType => attach_type_comment(object, comment, model),
        other => {
            tracing::debug!(?other, "COMMENT ON unhandled object type, skipping");
        }
    }
}

pub(super) fn handle_create_trigger(
    stmt: &pg_query::protobuf::CreateTrigStmt,
    range: TextRange,
    model: &mut SchemaModel,
) {
    let Some(rv) = stmt.relation.as_ref() else {
        tracing::warn!("CreateTrigStmt has no relation, skipping");
        return;
    };
    let target = range_var_to_key(rv);
    let key = QualifiedName::new(Some(target.schema.as_str()), &stmt.trigname);
    let func = extract_trigger_func(&stmt.funcname);
    model.triggers.insert(
        key,
        TriggerDef {
            target,
            range,
            func,
        },
    );
}

// funcname items are String sval entries only — no location data
fn extract_trigger_func(funcname: &[pg_query::protobuf::Node]) -> Option<QualifiedName> {
    use pg_query::NodeEnum;
    let parts: Vec<&str> = funcname
        .iter()
        .filter_map(|n| n.node.as_ref())
        .filter_map(|n| {
            if let NodeEnum::String(s) = n {
                Some(s.sval.as_str())
            } else {
                None
            }
        })
        .collect();
    match parts.as_slice() {
        [schema, name] => Some(QualifiedName::new(Some(schema), name)),
        [name] => Some(QualifiedName::new(None, name)),
        _ => None,
    }
}

pub(super) fn handle_create_policy(
    stmt: &pg_query::protobuf::CreatePolicyStmt,
    range: TextRange,
    model: &mut SchemaModel,
) {
    let Some(rv) = stmt.table.as_ref() else {
        tracing::warn!("CreatePolicyStmt has no table, skipping");
        return;
    };
    let target = range_var_to_key(rv);
    let key = QualifiedName::new(Some(target.schema.as_str()), &stmt.policy_name);
    model.policies.insert(key, PolicyDef { target, range });
}

pub(super) fn handle_create_index(
    stmt: &pg_query::protobuf::IndexStmt,
    range: TextRange,
    model: &mut SchemaModel,
) {
    let Some(rv) = stmt.relation.as_ref() else {
        tracing::warn!("IndexStmt has no relation, skipping");
        return;
    };
    let target = range_var_to_key(rv);
    let schema = target.schema.clone();
    let key = QualifiedName::new(Some(&schema), &stmt.idxname);
    let targets = extract_index_targets(&stmt.index_params, &stmt.idxname);
    let predicate = extract_index_predicate(stmt.where_clause.as_deref(), &stmt.idxname);
    model.indexes.insert(
        key,
        IndexDef {
            target,
            range,
            method: stmt.access_method.clone(),
            targets,
            unique: stmt.unique,
            predicate,
        },
    );
}

fn extract_index_targets(params: &[Node], idx_name: &str) -> Vec<IndexTarget> {
    params
        .iter()
        .filter_map(|n| index_target_from_node(n, idx_name))
        .collect()
}

fn index_target_from_node(node: &Node, idx_name: &str) -> Option<IndexTarget> {
    let Some(NodeEnum::IndexElem(elem)) = node.node.as_ref() else {
        tracing::warn!(idx = idx_name, "index_params entry not IndexElem, skipping");
        return None;
    };
    if !elem.name.is_empty() {
        return Some(IndexTarget::Column(elem.name.clone()));
    }
    let Some(expr) = elem.expr.as_deref().filter(|n| n.node.is_some()) else {
        tracing::warn!(
            idx = idx_name,
            "IndexElem has neither name nor expr, skipping",
        );
        return None;
    };
    match deparse_expr(expr) {
        Ok(text) => Some(IndexTarget::Expr(text)),
        Err(err) => {
            tracing::warn!(idx = idx_name, %err, "deparse of index expression failed, skipping target");
            None
        }
    }
}

fn extract_index_predicate(where_clause: Option<&Node>, idx_name: &str) -> Option<String> {
    let node = where_clause?;
    node.node.as_ref()?;
    match deparse_expr(node) {
        Ok(text) => Some(text),
        Err(err) => {
            tracing::warn!(idx = idx_name, %err, "deparse of index predicate failed, skipping");
            None
        }
    }
}

fn attach_table_comment(object: Option<&NodeEnum>, comment: String, model: &mut SchemaModel) {
    let Some(NodeEnum::List(list)) = object else {
        return;
    };
    let Some((schema, name)) = parse_name(&list.items) else {
        return;
    };
    let key = QualifiedName::new(schema.as_deref(), &name);
    if let Some(def) = model.tables.get_mut(&key) {
        def.doc = Some(comment);
    }
}

fn attach_column_comment(object: Option<&NodeEnum>, comment: String, model: &mut SchemaModel) {
    let Some(NodeEnum::List(list)) = object else {
        return;
    };
    let parts: Vec<&str> = list
        .items
        .iter()
        .filter_map(|n| n.node.as_ref())
        .filter_map(|n| {
            if let NodeEnum::String(s) = n {
                Some(s.sval.as_str())
            } else {
                None
            }
        })
        .collect();
    let (schema, table, col) = match parts.as_slice() {
        [schema, table, col] => (Some(*schema), *table, *col),
        [table, col] => (None, *table, *col),
        _ => return,
    };
    let key = QualifiedName::new(schema, table);
    if let Some(tdef) = model.tables.get_mut(&key) {
        if let Some(cdef) = tdef.columns.get_mut(col) {
            cdef.doc = Some(comment);
        }
    }
}

fn attach_function_comment(object: Option<&NodeEnum>, comment: String, model: &mut SchemaModel) {
    let Some(NodeEnum::ObjectWithArgs(owa)) = object else {
        return;
    };
    let Some((schema, name)) = parse_name(&owa.objname) else {
        return;
    };
    let key = QualifiedName::new(schema.as_deref(), &name);
    if let Some(def) = model.functions.get_mut(&key) {
        def.doc = Some(comment);
    }
}

fn attach_view_comment(object: Option<&NodeEnum>, comment: String, model: &mut SchemaModel) {
    let Some(NodeEnum::List(list)) = object else {
        return;
    };
    let Some((schema, name)) = parse_name(&list.items) else {
        return;
    };
    let key = QualifiedName::new(schema.as_deref(), &name);
    if let Some(def) = model.views.get_mut(&key) {
        def.doc = Some(comment);
    }
}

fn attach_type_comment(object: Option<&NodeEnum>, comment: String, model: &mut SchemaModel) {
    let Some(NodeEnum::TypeName(tn)) = object else {
        return;
    };
    let Some((schema, name)) = parse_name(&tn.names) else {
        return;
    };
    let key = QualifiedName::new(schema.as_deref(), &name);
    if let Some(def) = model.types.get_mut(&key) {
        def.doc = Some(comment);
    }
}

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

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

    #[test]
    fn create_trigger_populates_model() {
        let model = walk(concat!(
            "CREATE TABLE public.items (id bigint NOT NULL);",
            "CREATE TRIGGER trg_audit AFTER INSERT ON public.items FOR EACH ROW EXECUTE FUNCTION public.noop();",
        ));
        let key = QualifiedName::new(Some("public"), "trg_audit");
        let trig = model.triggers.get(&key).expect("trigger must exist");
        assert_eq!(trig.target.name, "items");
    }

    #[test]
    fn create_trigger_stores_qualified_func() {
        let model = walk(concat!(
            "CREATE TABLE public.items (id bigint NOT NULL);",
            "CREATE TRIGGER trg_set BEFORE UPDATE ON public.items FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();",
        ));
        let key = QualifiedName::new(Some("public"), "trg_set");
        let trig = model.triggers.get(&key).expect("trigger must exist");
        let func = trig.func.as_ref().expect("func must be populated");
        assert_eq!(func.schema, "public");
        assert_eq!(func.name, "set_updated_at");
    }

    #[test]
    fn create_trigger_stores_unqualified_func() {
        let model = walk(concat!(
            "CREATE TABLE public.items (id bigint NOT NULL);",
            "CREATE TRIGGER trg_bare AFTER INSERT ON public.items FOR EACH ROW EXECUTE FUNCTION bare_func();",
        ));
        let key = QualifiedName::new(Some("public"), "trg_bare");
        let trig = model.triggers.get(&key).expect("trigger must exist");
        let func = trig.func.as_ref().expect("func must be populated");
        assert_eq!(func.schema, "public");
        assert_eq!(func.name, "bare_func");
    }

    #[test]
    fn create_policy_populates_model() {
        let model = walk(concat!(
            "CREATE TABLE public.items (id bigint NOT NULL);",
            "CREATE POLICY pol_user ON public.items USING (id > 0);",
        ));
        let key = QualifiedName::new(Some("public"), "pol_user");
        let pol = model.policies.get(&key).expect("policy must exist");
        assert_eq!(pol.target.name, "items");
    }

    #[test]
    fn create_index_populates_model() {
        let model = walk(concat!(
            "CREATE TABLE public.items (id bigint NOT NULL, name text);",
            "CREATE INDEX idx_name ON public.items (name);",
        ));
        let key = QualifiedName::new(Some("public"), "idx_name");
        let idx = model.indexes.get(&key).expect("index must exist");
        assert_eq!(idx.target.name, "items");
    }

    #[test]
    fn multi_column_index_preserves_declaration_order() {
        use crate::core::schema::IndexTarget;
        let model = walk(concat!(
            "CREATE TABLE public.items (a int, b int, c int);",
            "CREATE INDEX idx_abc ON public.items (a, b, c);",
        ));
        let key = QualifiedName::new(Some("public"), "idx_abc");
        let idx = model.indexes.get(&key).expect("idx_abc must exist");
        assert_eq!(
            idx.targets,
            vec![
                IndexTarget::Column("a".into()),
                IndexTarget::Column("b".into()),
                IndexTarget::Column("c".into()),
            ],
        );
    }

    #[test]
    fn mixed_column_and_expression_index_targets() {
        use crate::core::schema::IndexTarget;
        let model = walk(concat!(
            "CREATE TABLE public.items (a int, b text);",
            "CREATE INDEX idx_mix ON public.items (a, lower(b));",
        ));
        let key = QualifiedName::new(Some("public"), "idx_mix");
        let idx = model.indexes.get(&key).expect("idx_mix must exist");
        assert_eq!(
            idx.targets,
            vec![
                IndexTarget::Column("a".into()),
                IndexTarget::Expr("lower(b)".into()),
            ],
        );
    }

    #[test]
    fn malformed_index_elem_is_skipped_without_breaking_index() {
        // Hand-built malformed IndexElem (empty name + expr=None) is unreachable
        // from real SQL — pg_query rejects it at parse time. This test exercises
        // the defensive log-and-skip path on the helper directly. Subsequent
        // valid IndexElems must still be captured.
        use pg_query::NodeEnum;
        use pg_query::protobuf::{IndexElem, Node};

        let malformed = Node {
            node: Some(NodeEnum::IndexElem(Box::new(IndexElem {
                name: String::new(),
                expr: None,
                ..Default::default()
            }))),
        };
        let valid = Node {
            node: Some(NodeEnum::IndexElem(Box::new(IndexElem {
                name: "a".to_owned(),
                ..Default::default()
            }))),
        };
        let targets = super::extract_index_targets(&[malformed, valid], "idx_test");
        assert_eq!(
            targets,
            vec![crate::core::schema::IndexTarget::Column("a".into())],
        );
    }
}
