//! JavaScript/TypeScript call-site detection and body-local type harvesting.

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

use crate::util::text;
use super::Ctx;
use super::named_child;

/// Walk a function body looking for `const/let x: T = …` (annotation)
/// and `const/let x = new T(...)` (constructor) bindings, populating
/// the type map for `x.method()` resolution.
pub(crate) fn collect_body_var_types(
    body: Node<'_>,
    src: &str,
    out: &mut std::collections::HashMap<String, String>,
) {
    let mut cursor = body.walk();
    let mut stack = vec![body];
    while let Some(n) = stack.pop() {
        if matches!(n.kind(), "lexical_declaration" | "variable_declaration") {
            let mut dc = n.walk();
            for declarator in n.children(&mut dc) {
                if declarator.kind() != "variable_declarator" {
                    continue;
                }
                let Some(name_node) = declarator.child_by_field_name("name") else {
                    continue;
                };
                if name_node.kind() != "identifier" {
                    continue;
                }
                let var_name = text(name_node, src).to_string();

                // TS type annotation: `const x: T = …`
                let annotated = declarator
                    .child_by_field_name("type")
                    .or_else(|| named_child(declarator, "type_annotation"))
                    .and_then(|ann| {
                        let mut tc = ann.walk();
                        ann.children(&mut tc)
                            .find(|c| matches!(c.kind(), "type_identifier" | "predefined_type"))
                            .map(|t| text(t, src).to_string())
                    });
                if let Some(ty) = annotated {
                    out.insert(var_name.clone(), ty);
                    continue;
                }

                // `const x = new T(...)` constructor pattern.
                if let Some(value) = declarator.child_by_field_name("value")
                    && value.kind() == "new_expression"
                    && let Some(ctor) = value.named_child(0)
                    && matches!(
                        ctor.kind(),
                        "identifier" | "type_identifier" | "member_expression"
                    )
                {
                    let head = text(ctor, src)
                        .split('.')
                        .next_back()
                        .unwrap_or("")
                        .to_string();
                    if !head.is_empty() {
                        out.insert(var_name, head);
                    }
                }
            }
        }
        for child in n.children(&mut cursor) {
            stack.push(child);
        }
    }
}

/// Emit `Calls` edges from a function body. Tree-sitter-javascript
/// represents calls as `call_expression` with a `function` field
/// (identifier / member_expression / etc.).
///
/// Resolution tiers:
///   1.0  bare-name leaf matches a file-local Symbol
///   0.9  resolves through an import alias
///   0.7  `var.method` where `var` is in the type map (TS annotation,
///        constructor pattern, or typed param). `this.method` when
///        inside a class body.
///   0.5  unresolved bare or member call
///
/// Awaits: when the call sits inside an `await_expression` ancestor
/// within `node`, also emit an `Awaits` edge with the same dst/conf.
///
/// JSX: `<Comp …/>` and `<Comp>…</Comp>` produce a Calls edge from the
/// enclosing function to the component name, file-local resolved at
/// 1.0 if known, else 0.5.
pub(crate) fn emit_calls(ctx: &mut Ctx<'_>, node: Node<'_>, src_id: &str, src: &str) {
    // First pass: harvest `const/let x: T = …` and `const/let x = new T(...)`
    // bindings declared anywhere inside the body. handle_var_decl only fires
    // at the top level, so without this pre-pass body-local typed dispatch
    // would always miss.
    collect_body_var_types(node, src, &mut ctx.types);

    let mut stack: Vec<(Node<'_>, bool)> = vec![(node, false)];
    while let Some((n, in_await)) = stack.pop() {
        let is_await = in_await || n.kind() == "await_expression";
        if n.kind() == "call_expression"
            && let Some(func) = n.child_by_field_name("function")
        {
            let target = text(func, src).to_string();
            let head = target
                .split(['.', ':'])
                .next()
                .unwrap_or(&target)
                .to_string();
            // Type-aware path: receiver is `this` or a typed variable.
            let typed_dst = if head == "this" {
                ctx.this_type
                    .as_deref()
                    .map(|ty| target.replacen("this", ty, 1))
            } else {
                ctx.types
                    .get(&head)
                    .map(|ty| target.replacen(head.as_str(), ty, 1))
            };
            let (dst, conf) = if let Some(typed) = typed_dst {
                (typed, 0.7)
            } else if !target.contains('.')
                && let Some(id) = ctx.name_to_id.get(&head)
            {
                (id.clone(), 1.0)
            } else if let Some(full) = ctx.aliases.get(&head) {
                let suffix = if target.len() > head.len() {
                    &target[head.len()..]
                } else {
                    ""
                };
                (format!("{full}{suffix}"), 0.9)
            } else {
                (target, 0.5)
            };
            ctx.out.edges.push(Edge {
                kind: EdgeKind::Calls,
                src: src_id.to_string(),
                dst: dst.clone(),
                confidence: conf,
            });
            if is_await {
                ctx.out.edges.push(Edge {
                    kind: EdgeKind::Awaits,
                    src: src_id.to_string(),
                    dst,
                    confidence: conf,
                });
            }
        }
        // JSX component usage: `<Comp />` / `<Comp>…</Comp>`.
        // M8: for namespaced components like `<Lib.Comp />`, split on `.`
        // and resolve the head via aliases, then reattach the tail.
        if matches!(n.kind(), "jsx_self_closing_element" | "jsx_opening_element")
            && let Some(name_node) = n.child_by_field_name("name")
        {
            let name = text(name_node, src);
            // Lowercase tags are HTML elements (`<div>`); only treat
            // capitalized identifiers as component calls.
            let head = name.split('.').next().unwrap_or(name);
            if head
                .chars()
                .next()
                .map(|c| c.is_ascii_uppercase())
                .unwrap_or(false)
            {
                let (dst, conf) = if let Some(id) = ctx.name_to_id.get(name) {
                    (id.clone(), 1.0)
                } else if let Some(full) = ctx.aliases.get(name) {
                    (full.clone(), 0.9)
                } else if name.contains('.') && let Some(full) = ctx.aliases.get(head) {
                    // e.g. `<Lib.Comp />`: resolve head to its import alias,
                    // then reattach the dotted suffix.
                    let tail = &name[head.len()..]; // includes leading `.`
                    (format!("{full}{tail}"), 0.8)
                } else {
                    (name.to_string(), 0.5)
                };
                ctx.out.edges.push(Edge {
                    kind: EdgeKind::Calls,
                    src: src_id.to_string(),
                    dst,
                    confidence: conf,
                });
            }
        }
        let mut c = n.walk();
        for child in n.children(&mut c) {
            stack.push((child, is_await));
        }
    }
}
