//! ActiveRecord / ActionController callback macros —
//! `before_save`, `after_save`, `before_create`, `after_create`,
//! `before_update`, `after_update`, `before_destroy`, `after_destroy`,
//! `before_validation`, `after_validation`, `before_action`,
//! `after_action`, `around_action`.
//!
//! Emits `Calls` edges from the class (and, when `only:`/`except:` are
//! given, from each scoped action method) to each named callback target.

use ckg_core::{Edge, EdgeKind, Kind, Symbol, node_id};
use tree_sitter::Node;

use crate::lang::ruby::{Ctx, strip_quotes, text};

/// Walk up from a callback call to find the enclosing `class` body, then
/// harvest every `method` name defined directly on that class.
/// Skips `singleton_method`s (those are class methods, not actions).
fn enclosing_class_actions(call: Node<'_>, src: &str) -> Vec<String> {
    let mut cur = call;
    let class_node = loop {
        let Some(parent) = cur.parent() else {
            return Vec::new();
        };
        if parent.kind() == "class" {
            break parent;
        }
        cur = parent;
    };
    let Some(body) = class_node.child_by_field_name("body") else {
        return Vec::new();
    };
    let mut names = Vec::new();
    let mut c = body.walk();
    for stmt in body.children(&mut c) {
        if stmt.kind() != "method" {
            continue;
        }
        let Some(name_node) = stmt.child_by_field_name("name") else {
            continue;
        };
        let n = text(name_node, src).trim_end_matches('=').to_string();
        // Skip lifecycle helpers + private utility methods (those leading
        // with `_` are conventionally not actions).
        if n.starts_with('_')
            || matches!(n.as_str(), "initialize" | "to_s" | "to_param" | "inspect")
        {
            continue;
        }
        names.push(n);
    }
    names
}

pub(crate) fn handle_callback(
    ctx: &mut Ctx<'_>,
    n: Node<'_>,
    src: &str,
    kind: &str,
    class_id: &str,
    class_scope: &str,
) {
    let Some(args) = n.child_by_field_name("arguments") else {
        return;
    };
    let mut c = args.walk();
    let mut targets: Vec<String> = Vec::new();
    let mut only: Vec<String> = Vec::new();
    let mut except: Vec<String> = Vec::new();

    let harvest_filter_list = |node: Node<'_>, into: &mut Vec<String>| {
        let mut nc = node.walk();
        for child in node.children(&mut nc) {
            if child.kind() == "simple_symbol" {
                into.push(strip_quotes(text(child, src)).to_string());
            } else if child.kind() == "array" {
                let mut ac = child.walk();
                for a in child.children(&mut ac) {
                    if a.kind() == "simple_symbol" {
                        into.push(strip_quotes(text(a, src)).to_string());
                    }
                }
            }
        }
    };

    for arg in args.children(&mut c) {
        match arg.kind() {
            "simple_symbol" => {
                targets.push(strip_quotes(text(arg, src)).to_string());
            }
            "pair" | "hash" => {
                // Collect filter scope: only:/except: → list of action symbols.
                let extract_pair =
                    |kv: Node<'_>, only: &mut Vec<String>, except: &mut Vec<String>| {
                        if kv.kind() != "pair" {
                            return;
                        }
                        let key = kv.child_by_field_name("key").map(|k| text(k, src));
                        let val = kv.child_by_field_name("value");
                        let Some(k) = key else { return };
                        let k_clean = strip_quotes(k.trim_end_matches(':'));
                        if let Some(v) = val {
                            match k_clean {
                                "only" => harvest_filter_list(v, only),
                                "except" => harvest_filter_list(v, except),
                                _ => {}
                            }
                        }
                    };
                if arg.kind() == "pair" {
                    extract_pair(arg, &mut only, &mut except);
                } else {
                    let mut hc = arg.walk();
                    for kv in arg.children(&mut hc) {
                        extract_pair(kv, &mut only, &mut except);
                    }
                }
            }
            _ => {}
        }
    }

    let scope_doc = match (only.is_empty(), except.is_empty()) {
        (true, true) => String::new(),
        (false, _) => format!(" only=[{}]", only.join(",")),
        (_, false) => format!(" except=[{}]", except.join(",")),
    };

    // Per-action expansion (Rails 7-style `before_action :foo, only: …`):
    // emit a Calls edge from each affected action method → target.
    //
    // For `except:`, harvest the actual `def` names from the enclosing
    // class body — covers custom actions like `archive` / `restore` that
    // aren't in the REST 7. Falls back to REST 7 if the class body can't
    // be located. For `only:`, the symbol list is already exact.
    const REST_ACTIONS: &[&str] = &[
        "index", "show", "new", "create", "edit", "update", "destroy",
    ];
    let action_kind_is_action_filter =
        matches!(kind, "before_action" | "after_action" | "around_action");
    let class_actions: Vec<String> = enclosing_class_actions(n, src);
    let except_pool: Vec<String> = if class_actions.is_empty() {
        REST_ACTIONS.iter().map(|s| (*s).to_string()).collect()
    } else {
        class_actions
    };
    let scoped_actions: Vec<String> = if !only.is_empty() {
        only.clone()
    } else if !except.is_empty() {
        except_pool
            .into_iter()
            .filter(|a| !except.iter().any(|e| e == a))
            .collect()
    } else {
        Vec::new()
    };

    for target in targets {
        // Per-action Calls: `<class>.<action>` → target. Lets queries
        // like "what filters run before #show?" work via graph traversal
        // instead of having to parse the only:/except: clause.
        if action_kind_is_action_filter && !scoped_actions.is_empty() {
            for action in &scoped_actions {
                let action_qname = if class_scope.is_empty() {
                    format!("{}.{action}", ctx.module)
                } else {
                    format!("{}::{class_scope}.{action}", ctx.module)
                };
                let action_id = node_id(
                    ctx.repo_id,
                    "ruby",
                    &ctx.relpath,
                    &action_qname,
                    Kind::Method,
                );
                ctx.out.edges.push(Edge {
                    kind: EdgeKind::Calls,
                    src: action_id,
                    dst: target.clone(),
                    confidence: 0.8,
                });
            }
        }
        let mut conf = 0.9;
        // Doc-tagging the edge isn't supported; instead encode scope in dst's
        // doc via a synthetic Symbol if filter scope was given.
        if !scope_doc.is_empty() {
            // Edge doc isn't a Symbol field, so we attach it to a marker Symbol.
            // PARSE-H6: include class scope so two controllers in the same
            // file don't collide on the synthetic marker Symbol.
            let qname = if class_scope.is_empty() {
                format!("{}.before_action_{target}", ctx.module)
            } else {
                format!("{}::{class_scope}.before_action_{target}", ctx.module)
            };
            let id = node_id(ctx.repo_id, "ruby", &ctx.relpath, &qname, Kind::Method);
            ctx.out.symbols.push(Symbol {
                id: id.clone(),
                qname,
                name: format!("before_action_{target}"),
                kind: Kind::Method,
                file: ctx.relpath.clone(),
                line: n.start_position().row as u32 + 1,
                col: n.start_position().column as u32,
                is_public: true,
                doc: format!("filter target={target}{scope_doc}"),
                hash: blake3::hash(text(n, src).as_bytes()).to_hex()[..16].to_string(),
            });
            ctx.out.edges.push(Edge {
                kind: EdgeKind::Defines,
                src: class_id.to_string(),
                dst: id,
                confidence: 1.0,
            });
            conf = 1.0;
        }
        ctx.out.edges.push(Edge {
            kind: EdgeKind::Calls,
            src: class_id.to_string(),
            dst: target,
            confidence: conf,
        });
    }
}
