//! Ruby call-site detection — emits Calls edges from method bodies.

use ckg_core::{Edge, EdgeKind};
use tree_sitter::Node;

use crate::util::text;
use super::{Ctx, strip_quotes};

/// Extract block param names from a `block_parameters` node.
/// `do |x, y, *rest, &blk| …` → `["x", "y", "rest", "blk"]`.
/// Used by `emit_calls` to populate a per-block scope so type-aware
/// dispatch (when added) can shadow outer bindings.
pub(super) fn collect_block_param_names(params: Node<'_>, src: &str) -> Vec<String> {
    let mut names = Vec::new();
    let mut c = params.walk();
    for p in params.children(&mut c) {
        match p.kind() {
            "identifier" => names.push(text(p, src).to_string()),
            "splat_parameter" | "hash_splat_parameter" | "block_parameter" => {
                let mut sc = p.walk();
                if let Some(id) = p.children(&mut sc).find(|x| x.kind() == "identifier") {
                    names.push(text(id, src).to_string());
                }
            }
            _ => {}
        }
    }
    names
}

/// Harvest `var = Foo.new(…)` and `@var = Foo.new(…)` assignments from
/// the method body, plus instance-variable assignments inside `initialize`
/// when reachable. Best-effort: only records when the RHS is a `Foo.new`
/// or a constant `Foo` reference.
pub(super) fn collect_ruby_types(body: Node<'_>, src: &str) -> std::collections::HashMap<String, String> {
    let mut out: std::collections::HashMap<String, String> = std::collections::HashMap::new();
    let mut cursor = body.walk();
    let mut stack = vec![body];
    while let Some(n) = stack.pop() {
        if n.kind() == "assignment"
            && let (Some(left), Some(right)) = (
                n.child_by_field_name("left"),
                n.child_by_field_name("right"),
            )
            && matches!(left.kind(), "identifier" | "instance_variable")
        {
            let var = text(left, src).to_string();
            // RHS = `Foo.new` / `Foo.new(args)` → `Foo`.
            // RHS = bare `Foo` (constant) → `Foo`.
            let ty = match right.kind() {
                "call" => right
                    .child_by_field_name("receiver")
                    .filter(|r| r.kind() == "constant" || r.kind() == "scope_resolution")
                    .filter(|_| {
                        right
                            .child_by_field_name("method")
                            .map(|m| text(m, src) == "new")
                            .unwrap_or(false)
                    })
                    .map(|r| text(r, src).to_string()),
                "constant" => Some(text(right, src).to_string()),
                _ => None,
            };
            if let Some(t) = ty {
                out.insert(var, t);
            }
        }
        for child in n.children(&mut cursor) {
            stack.push(child);
        }
    }
    out
}

pub(crate) fn emit_calls(ctx: &mut Ctx<'_>, node: Node<'_>, src: &str, src_id: &str) {
    // Type-aware dispatch: harvest `var = Foo.new` from the body so calls
    // like `var.method` resolve to `Foo.method` at confidence 0.7.
    let types = collect_ruby_types(node, src);
    // Per-block scope: each entry is the set of block params visible at
    // some ancestor `do_block` / `block`. A receiver matching any of these
    // is a closure-local variable, not a class — drop the call's
    // confidence to reflect the dispatch ambiguity.
    let mut block_scope: Vec<Vec<String>> = Vec::new();
    let mut stack: Vec<(Node<'_>, usize)> = vec![(node, 0)];
    while let Some((n, scope_depth)) = stack.pop() {
        while block_scope.len() > scope_depth {
            block_scope.pop();
        }
        let pushed_scope = if matches!(n.kind(), "do_block" | "block")
            && let Some(params) = n.child_by_field_name("parameters")
        {
            block_scope.push(collect_block_param_names(params, src));
            true
        } else {
            false
        };
        let here_depth = if pushed_scope {
            scope_depth + 1
        } else {
            scope_depth
        };

        if n.kind() == "call"
            && let Some(method) = n.child_by_field_name("method")
        {
            let method_name = text(method, src).to_string();
            let receiver = n.child_by_field_name("receiver");
            let receiver_text = receiver
                .filter(|r| matches!(r.kind(), "identifier" | "instance_variable"))
                .map(|r| text(r, src).to_string());
            let receiver_is_block_local = receiver_text
                .as_deref()
                .map(|name| block_scope.iter().any(|s| s.iter().any(|p| p == name)))
                .unwrap_or(false);
            // Type-aware upgrade: receiver bound to a known class →
            // `Foo.method` at 0.7. Block-local takes precedence (it's
            // shadowing whatever outer binding existed).
            let typed_dst = if receiver_is_block_local {
                None
            } else {
                receiver_text
                    .as_deref()
                    .and_then(|name| types.get(name))
                    .map(|cls| format!("{cls}.{method_name}"))
            };
            let (dst, confidence) = match typed_dst {
                Some(d) => (d, 0.7),
                None if receiver_is_block_local => (method_name, 0.4),
                None => (method_name, 0.5),
            };
            ctx.out.edges.push(Edge {
                kind: EdgeKind::Calls,
                src: src_id.to_string(),
                dst,
                confidence,
            });
        }
        // `arr.map(&:upcase)` — `block_argument` containing a `simple_symbol`.
        if n.kind() == "block_argument" {
            let mut c = n.walk();
            for child in n.children(&mut c) {
                if child.kind() == "simple_symbol" {
                    let method_name = strip_quotes(text(child, src)).to_string();
                    ctx.out.edges.push(Edge {
                        kind: EdgeKind::Calls,
                        src: src_id.to_string(),
                        dst: method_name,
                        confidence: 0.7,
                    });
                }
            }
        }
        let mut c = n.walk();
        for child in n.children(&mut c) {
            stack.push((child, here_depth));
        }
    }
}
