//! JavaScript/TypeScript AST walker — visits the tree-sitter tree and emits
//! Symbols for declarations plus fires call/import handlers.

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

use crate::util::text;
use super::Ctx;
use super::{name_of_field, collect_jsdoc, qualify, named_child, first_named_child, slugify,
            collect_param_types, collect_decorators, collect_implements};
use super::imports::add_import_aliases;
use super::calls::emit_calls;

pub(crate) fn walk(ctx: &mut Ctx<'_>, node: Node<'_>, scope: &str, src: &str) {
    // PARSE-C1: depth-guarded recursive walker. MAX_DEPTH bounds
    // pathological inputs to prevent stack overflow.
    walk_inner(ctx, node, scope, src, 0);
}

const MAX_DEPTH: usize = 512;

fn walk_inner(ctx: &mut Ctx<'_>, node: Node<'_>, scope: &str, src: &str, depth: usize) {
    if depth >= MAX_DEPTH {
        tracing::warn!(
            "ckg-parse/js: hit MAX_DEPTH={MAX_DEPTH} at {}:{}; skipping subtree",
            ctx.relpath,
            node.start_position().row + 1
        );
        return;
    }

    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        match child.kind() {
            "function_declaration" | "generator_function_declaration" => {
                if let Some(name) = name_of_field(child, src) {
                    let qname = qualify(&ctx.module, scope, &name);
                    let doc = collect_jsdoc(child, src);
                    let id = ctx.push(child, src, Kind::Function, qname, name.clone(), doc);
                    if let Some(params) = child.child_by_field_name("parameters") {
                        collect_param_types(params, src, &mut ctx.types);
                    }
                    if let Some(body) = child.child_by_field_name("body") {
                        emit_calls(ctx, body, &id, src);
                    }
                }
            }
            "class_declaration" => {
                if let Some(name) = name_of_field(child, src) {
                    let qname = qualify(&ctx.module, scope, &name);
                    let mut doc = collect_jsdoc(child, src);
                    let decos = collect_decorators(child, src);
                    if !decos.is_empty() {
                        let tag = decos
                            .iter()
                            .map(|d| format!("@{d}"))
                            .collect::<Vec<_>>()
                            .join(" ");
                        doc = if doc.is_empty() {
                            tag
                        } else {
                            format!("{tag}\n{doc}")
                        };
                    }
                    let id = ctx.push(child, src, Kind::Class, qname, name.clone(), doc);
                    let mut implements_ifaces: Vec<String> = Vec::new();
                    if let Some(heritage) = child
                        .child_by_field_name("class_heritage")
                        .or_else(|| named_child(child, "class_heritage"))
                    {
                        emit_extends(ctx, heritage, &id, src);
                        collect_implements(heritage, src, &mut implements_ifaces);
                    }
                    if let Some(body) = child.child_by_field_name("body") {
                        let inner = if scope.is_empty() {
                            name.clone()
                        } else {
                            format!("{}.{name}", scope)
                        };
                        let prev_this = ctx.this_type.take();
                        ctx.this_type = Some(name.clone());
                        walk_class_body(ctx, body, &inner, &id, src, &implements_ifaces);
                        ctx.this_type = prev_this;
                    }
                }
            }
            "expression_statement" => {
                if let Some(call) = first_named_child(child, "call_expression") {
                    let handled = handle_test_wrapper(ctx, call, scope, src);
                    if !handled {
                        walk_inner(ctx, child, scope, src, depth + 1);
                    }
                }
            }
            "interface_declaration" => {
                if let Some(name) = name_of_field(child, src) {
                    let qname = qualify(&ctx.module, scope, &name);
                    let doc = collect_jsdoc(child, src);
                    ctx.push(child, src, Kind::Interface, qname, name, doc);
                }
            }
            "type_alias_declaration" => {
                if let Some(name) = name_of_field(child, src) {
                    let qname = qualify(&ctx.module, scope, &name);
                    let doc = collect_jsdoc(child, src);
                    ctx.push(child, src, Kind::TypeAlias, qname, name, doc);
                }
            }
            "enum_declaration" => {
                if let Some(name) = name_of_field(child, src) {
                    let qname = qualify(&ctx.module, scope, &name);
                    let doc = collect_jsdoc(child, src);
                    ctx.push(child, src, Kind::Enum, qname, name, doc);
                }
            }
            "import_statement" => {
                add_import_aliases(ctx, child, src);
            }
            "lexical_declaration" | "variable_declaration" => {
                handle_var_decl(ctx, child, scope, src);
            }
            "export_statement" | _ => {
                walk_inner(ctx, child, scope, src, depth + 1);
            }
        }
    }
}


pub(crate) fn walk_class_body(
    ctx: &mut Ctx<'_>,
    body: Node<'_>,
    scope: &str,
    class_id: &str,
    src: &str,
    implements_ifaces: &[String],
) {
    let mut cursor = body.walk();
    for child in body.children(&mut cursor) {
        match child.kind() {
            "method_definition" => {
                if let Some(name) = name_of_field(child, src) {
                    let qname = qualify(&ctx.module, scope, &name);
                    let doc = collect_jsdoc(child, src);
                    // `class X { constructor() {} }` → Kind::Constructor.
                    // `get y()` / `set y(v)` are still Methods but we
                    // include them as-is (their `name` already strips
                    // the get/set keyword via name_of_field).
                    let kind = if name == "constructor" {
                        Kind::Constructor
                    } else {
                        Kind::Method
                    };
                    let id = ctx.push(child, src, kind, qname, name.clone(), doc);
                    ctx.out.edges.push(Edge {
                        kind: EdgeKind::Defines,
                        src: class_id.to_string(),
                        dst: id.clone(),
                        confidence: 1.0,
                    });
                    // Interface back-edges: for each `implements I`,
                    // emit `I.<method>` → impl-method-id at conf 0.6.
                    // Lets a `Box<dyn I>`-style call reach the impl.
                    for iface in implements_ifaces {
                        ctx.out.edges.push(Edge {
                            kind: EdgeKind::Calls,
                            src: format!("{iface}.{name}"),
                            dst: id.clone(),
                            confidence: 0.6,
                        });
                    }
                    if let Some(params) = child.child_by_field_name("parameters") {
                        collect_param_types(params, src, &mut ctx.types);
                    }
                    if let Some(body) = child.child_by_field_name("body") {
                        emit_calls(ctx, body, &id, src);
                    }
                }
            }
            "public_field_definition" | "field_definition" => {
                if let Some(name) = name_of_field(child, src) {
                    let qname = qualify(&ctx.module, scope, &name);
                    let id = ctx.push(child, src, Kind::Constant, qname, name, String::new());
                    ctx.out.edges.push(Edge {
                        kind: EdgeKind::Defines,
                        src: class_id.to_string(),
                        dst: id,
                        confidence: 1.0,
                    });
                }
            }
            _ => {}
        }
    }
}

/// `const foo = () => …` / `const Foo = class { … }` / `const x: T = …`
/// / `const x = require('…')`. Three jobs: emit a Symbol if the value
/// is a callable / class, harvest a type binding for `x.method` typed
/// dispatch, and resolve CommonJS `require()` as an Imports edge +
/// alias.
pub(crate) fn handle_var_decl(ctx: &mut Ctx<'_>, n: Node<'_>, scope: &str, src: &str) {
    let mut c = n.walk();
    for declarator in n.children(&mut c) {
        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();

        // 1. Type harvesting (independent of whether we emit a Symbol).
        //    Priority: explicit `: T` annotation, then RHS shape.
        let annotated_type = 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)
                    // M2: also accept generic_type (e.g. `Promise<T>`) and
                    // union_type (e.g. `string | number`) like collect_param_types.
                    .find(|c| matches!(c.kind(), "type_identifier" | "predefined_type" | "generic_type" | "union_type"))
                    .map(|t| text(t, src).to_string())
            });
        if let Some(ty) = annotated_type {
            ctx.types.insert(var_name.clone(), ty);
        }

        let value = declarator.child_by_field_name("value");

        // 2. CommonJS `require('lib')` — emit Imports + alias.
        if let Some(v) = value
            && v.kind() == "call_expression"
            && let Some(func) = v.child_by_field_name("function")
            && text(func, src) == "require"
            && let Some(args) = v.child_by_field_name("arguments")
        {
            let raw = text(args, src);
            let path = raw.trim_matches(['(', ')', ' ', '"', '\'']).to_string();
            if !path.is_empty() {
                ctx.out.edges.push(Edge {
                    kind: EdgeKind::Imports,
                    src: ctx.module.clone(),
                    dst: path.clone(),
                    confidence: 1.0,
                });
                ctx.aliases.insert(var_name.clone(), path);
            }
        }

        let Some(value) = value else { continue };

        // 3. `new T(...)` — bind var to T (constructor pattern).
        if 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() {
                ctx.types.insert(var_name.clone(), head);
            }
        }

        // 4. Emit Symbol when the value is a callable / class binding.
        let kind = match value.kind() {
            "arrow_function" | "function_expression" | "function" => Some(Kind::Function),
            "class_expression" | "class" => Some(Kind::Class),
            _ => None,
        };
        let Some(kind) = kind else { continue };
        let qname = qualify(&ctx.module, scope, &var_name);
        let doc = collect_jsdoc(n, src);
        let id = ctx.push(declarator, src, kind, qname, var_name, doc);
        if kind == Kind::Function {
            if let Some(params) = value.child_by_field_name("parameters") {
                collect_param_types(params, src, &mut ctx.types);
            }
            if let Some(body) = value.child_by_field_name("body") {
                emit_calls(ctx, body, &id, src);
            }
        }
    }
}

pub(crate) fn emit_extends(ctx: &mut Ctx<'_>, heritage: Node<'_>, class_id: &str, src: &str) {
    // `class_heritage` wraps either `extends_clause` or
    // `implements_clause` (TS) — recurse one level to find the
    // identifier(s) that name the parent type.
    let mut stack = vec![heritage];
    while let Some(n) = stack.pop() {
        if matches!(
            n.kind(),
            "identifier" | "type_identifier" | "member_expression"
        ) {
            ctx.out.edges.push(Edge {
                kind: EdgeKind::Extends,
                src: class_id.to_string(),
                dst: text(n, src).to_string(),
                confidence: 0.9,
            });
            continue;
        }
        if matches!(
            n.kind(),
            "extends_clause" | "implements_clause" | "class_heritage"
        ) {
            let mut c = n.walk();
            for child in n.children(&mut c) {
                stack.push(child);
            }
        }
    }
}

/// Jest / Vitest / Mocha test wrappers: `describe('…', () => {…})`,
/// `it('…', () => {…})`, `test('…', () => {…})`. Synthesize a Symbol
/// per test/describe block (named after the description), tag with
/// `@test`, and recurse so nested it/test inside describe also fire.
/// Returns true when the call was a recognized test wrapper (caller
/// should NOT recurse further).
pub(crate) fn handle_test_wrapper(ctx: &mut Ctx<'_>, call: Node<'_>, scope: &str, src: &str) -> bool {
    let Some(func) = call.child_by_field_name("function") else {
        return false;
    };
    if func.kind() != "identifier" {
        return false;
    }
    let fname = text(func, src);
    let is_test_block = matches!(fname, "describe" | "it" | "test" | "context" | "suite");
    let is_hook = matches!(
        fname,
        "beforeAll" | "beforeEach" | "afterAll" | "afterEach" | "before" | "after"
    );
    if !is_test_block && !is_hook {
        return false;
    }
    let Some(args) = call.child_by_field_name("arguments") else {
        return false;
    };

    // For test blocks, first arg is the description string; for hooks
    // there's no description and the arrow is the only arg.
    let mut ac = args.walk();
    let kids: Vec<_> = args.children(&mut ac).filter(|c| c.is_named()).collect();
    let (desc_slug, body_arrow) = if is_test_block {
        let desc = kids
            .first()
            .filter(|c| matches!(c.kind(), "string" | "template_string"))
            .map(|c| {
                let raw = text(*c, src);
                slugify(raw.trim_matches(['"', '\'', '`']))
            })
            .unwrap_or_else(|| fname.to_string());
        let arrow = kids
            .iter()
            .find(|c| {
                matches!(
                    c.kind(),
                    "arrow_function" | "function_expression" | "function"
                )
            })
            .copied();
        (desc, arrow)
    } else {
        (
            fname.to_string(),
            kids.iter()
                .find(|c| {
                    matches!(
                        c.kind(),
                        "arrow_function" | "function_expression" | "function"
                    )
                })
                .copied(),
        )
    };

    // M9: include the 1-based line number so two `describe('name', ...)` at
    // different locations in the same file produce distinct Symbol ids.
    let line = call.start_position().row + 1;
    let synth_name = format!("{fname}_{desc_slug}_{line}");
    let qname = qualify(&ctx.module, scope, &synth_name);
    let id = ctx.push(
        call,
        src,
        Kind::Method,
        qname,
        synth_name.clone(),
        "@test".into(),
    );

    if let Some(arrow) = body_arrow {
        // Recurse: emit calls in the body AND descend so nested
        // describe/it/test wrappers also produce Symbols.
        if let Some(body) = arrow.child_by_field_name("body") {
            emit_calls(ctx, body, &id, src);
            // Walk inner statements for nested test wrappers.
            let inner_scope = if scope.is_empty() {
                synth_name.clone()
            } else {
                format!("{scope}.{synth_name}")
            };
            walk(ctx, body, &inner_scope, src);
        }
    }
    true
}
