//! Ruby AST walker — visits the tree-sitter tree, emits Symbols for every
//! declaration kind, and fires call/import/mixin handlers.
//!
//! Submodules:
//!   - `helpers` — `handle_mixin`, `handle_attr`, `handle_define_method`,
//!                 `tag_variadic` (extracted to stay under 200L guideline)
//!
//! ## Qname Separator Policy (PARSE-H10)
//!
//! Ruby uses a **mixed separator** convention intentionally:
//! - `::` between modules and classes (e.g. `ActiveRecord::Base::Connection`)
//! - `.` for class-method dispatch (e.g. `User.find_by`)
//! - `#` for instance-method-of-class declaration site (convention only,
//!   not emitted in qname — kept in docs for cross-reference)
//!
//! This mirrors Ruby community conventions (YARD, ri, RDoc) and is
//! documented here as the authoritative rule. Other languages use a
//! single separator per-language (Rust: `::`, Python: `.`, JS: `.`).

mod helpers;

pub(super) use helpers::tag_variadic;

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

use crate::util::text;
use super::{Ctx, name_of, strip_quotes};
use super::calls::emit_calls;
use helpers::{handle_mixin, handle_define_method, handle_attr};

pub(crate) fn walk(ctx: &mut Ctx<'_>, node: Node<'_>, src: &str, scope: &str) {
    walk_with_class(ctx, node, src, scope, None, true)
}

/// PARSE-C1: depth-guarded recursive walker.
///
/// Structural recursion (class/module/refine/included bodies) is bounded
/// by real Ruby nesting which is always shallow. The `MAX_DEPTH` guard
/// catches pathological generated/minified inputs that would otherwise
/// overflow the call stack via the `_ =>` catch-all arm.
///
/// Implementation note: this walker is **depth-bounded recursive**, not
/// iterativized. Each tree node is handled by a direct recursive call to
/// `walk_inner` with `depth + 1`. The guard truncates the subtree when
/// `MAX_DEPTH` is reached rather than converting to an explicit stack.
/// Iterativizing would eliminate the hard depth limit at the cost of
/// significant structural complexity; the current limit is generous enough
/// for any real Ruby codebase (PARSE-C1 ticket terminology: "iterativized"
/// referred to that alternative design, which was not adopted).
const MAX_DEPTH: usize = 512;

pub(crate) fn walk_with_class(
    ctx: &mut Ctx<'_>,
    node: Node<'_>,
    src: &str,
    scope: &str,
    class_id: Option<&str>,
    is_public: bool,
) {
    walk_inner(ctx, node, src, scope, class_id, is_public, 0);
}

fn walk_inner(
    ctx: &mut Ctx<'_>,
    node: Node<'_>,
    src: &str,
    scope: &str,
    class_id: Option<&str>,
    mut is_public: bool,
    depth: usize,
) {
    if depth >= MAX_DEPTH {
        tracing::warn!(
            "ckg-parse/ruby: 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() {
            "class" | "module" => {
                let Some(name) = name_of(child, src) else {
                    walk_inner(ctx, child, src, scope, class_id, is_public, depth + 1);
                    continue;
                };
                let kind = if child.kind() == "class" {
                    Kind::Class
                } else {
                    Kind::Module
                };
                let qname = if scope.is_empty() {
                    format!("{}::{name}", ctx.module)
                } else {
                    format!("{}::{scope}::{name}", ctx.module)
                };
                let id = ctx.push(child, src, kind, qname, name.clone());

                let superclass_text: String = child
                    .child_by_field_name("superclass")
                    .map(|s| text(s, src).trim_start_matches('<').trim().to_string())
                    .unwrap_or_default();
                if !superclass_text.is_empty() {
                    ctx.out.edges.push(Edge {
                        kind: EdgeKind::Extends,
                        src: id.clone(),
                        dst: superclass_text.clone(),
                        confidence: 0.9,
                    });
                }
                let inner_scope = if scope.is_empty() {
                    name.clone()
                } else {
                    format!("{scope}::{name}")
                };
                if let Some(body) = child.child_by_field_name("body") {
                    walk_inner(ctx, body, src, &inner_scope, Some(&id), true, depth + 1);
                    crate::framework::rails::class_hooks(
                        ctx,
                        &name,
                        &id,
                        &superclass_text,
                        body,
                        src,
                    );
                }
            }
            "method" | "singleton_method" => {
                let Some(name) = name_of(child, src) else {
                    continue;
                };
                let qname = if scope.is_empty() {
                    format!("{}.{name}", ctx.module)
                } else {
                    format!("{}::{scope}.{name}", ctx.module)
                };
                let is_test = name.starts_with("test_");
                let id =
                    ctx.push_with_visibility(child, src, Kind::Method, qname, name, is_public);
                if is_test && let Some(sym) = ctx.symbol_by_id_mut(&id) {
                    if sym.doc.is_empty() {
                        sym.doc = "@test".into();
                    } else if !sym.doc.contains("@test") {
                        sym.doc = format!("@test\n{}", sym.doc);
                    }
                }
                if let Some(pid) = class_id {
                    ctx.out.edges.push(Edge {
                        kind: EdgeKind::Defines,
                        src: pid.to_string(),
                        dst: id.clone(),
                        confidence: 1.0,
                    });
                }
                if let Some(target) = ctx.refinement_target.clone()
                    && let Some(sym) = ctx.symbol_by_id_mut(&id)
                {
                    sym.doc = format!("@refinement {target}");
                }
                tag_variadic(ctx, child, &id);
                if let Some(body) = child.child_by_field_name("body") {
                    emit_calls(ctx, body, src, &id);
                }
            }
            "assignment" => {
                if let Some(left) = child.child_by_field_name("left")
                    && left.kind() == "constant"
                {
                    let name = text(left, src).to_string();
                    let qname = if scope.is_empty() {
                        format!("{}.{name}", ctx.module)
                    } else {
                        format!("{}::{scope}.{name}", ctx.module)
                    };
                    let const_id =
                        ctx.push(child, src, Kind::Constant, qname.clone(), name.clone());
                    if let Some(pid) = class_id {
                        ctx.out.edges.push(Edge {
                            kind: EdgeKind::Defines,
                            src: pid.to_string(),
                            dst: const_id.clone(),
                            confidence: 1.0,
                        });
                    }
                    if let Some(right) = child.child_by_field_name("right")
                        && right.kind() == "call"
                    {
                        let recv_text = right
                            .child_by_field_name("receiver")
                            .map(|r| text(r, src))
                            .unwrap_or("");
                        let m = right.child_by_field_name("method").map(|m| text(m, src));
                        let is_struct = (recv_text == "Struct" && m == Some("new"))
                            || (recv_text == "Data" && m == Some("define"));
                        if is_struct
                            && let Some(args) = right.child_by_field_name("arguments")
                        {
                            let with_setter = recv_text == "Struct";
                            let mut ac = args.walk();
                            for arg in args.children(&mut ac) {
                                if arg.kind() != "simple_symbol" {
                                    continue;
                                }
                                let base = strip_quotes(text(arg, src)).to_string();
                                let mut accessors = vec![base.clone()];
                                if with_setter {
                                    accessors.push(format!("{base}="));
                                }
                                for a_name in accessors {
                                    let a_qname = format!("{qname}.{a_name}");
                                    let a_id = ctx.push_with_visibility(
                                        arg,
                                        src,
                                        Kind::Method,
                                        a_qname,
                                        a_name,
                                        true,
                                    );
                                    ctx.out.edges.push(Edge {
                                        kind: EdgeKind::Defines,
                                        src: const_id.clone(),
                                        dst: a_id,
                                        confidence: 1.0,
                                    });
                                }
                            }
                        }
                    }
                }
            }
            "singleton_class" => {
                if let Some(body) = child.child_by_field_name("body") {
                    walk_inner(ctx, body, src, scope, class_id, is_public, depth + 1);
                }
            }
            "identifier" if class_id.is_some() => {
                let t = text(child, src);
                if matches!(t, "private" | "protected" | "public") {
                    is_public = t == "public";
                }
            }
            "call" => {
                let method = child.child_by_field_name("method").map(|m| text(m, src));
                if matches!(method, Some("private") | Some("protected") | Some("public"))
                    && child
                        .child_by_field_name("arguments")
                        .map(|a| a.named_child_count() == 0)
                        .unwrap_or(true)
                {
                    is_public = method == Some("public");
                    continue;
                }
                if matches!(method, Some("require") | Some("require_relative")) {
                    // PARSE-H5: extract path-only dst (not the whole
                    // `require "foo/bar"` text). Look for the string
                    // argument child node; fall back to text-strip.
                    let raw = text(child, src).trim().to_string();
                    let dst = if let Some(arg) = child
                        .child_by_field_name("arguments")
                        .and_then(|a| a.named_child(0))
                    {
                        text(arg, src)
                            .trim()
                            .trim_matches('"')
                            .trim_matches('\'')
                            .to_string()
                    } else {
                        // Fallback: strip verb + quotes from raw text.
                        raw.strip_prefix("require_relative")
                            .or_else(|| raw.strip_prefix("require"))
                            .unwrap_or(&raw)
                            .trim()
                            .trim_matches('"')
                            .trim_matches('\'')
                            .to_string()
                    };
                    ctx.out.edges.push(Edge {
                        kind: EdgeKind::Imports,
                        src: ctx.module.clone(),
                        dst,
                        confidence: 0.9,
                    });
                } else if matches!(
                    method,
                    Some("include") | Some("extend") | Some("prepend")
                ) && let Some(pid) = class_id
                {
                    handle_mixin(ctx, child, src, pid);
                } else if let Some(m) = method
                    && matches!(m, "attr_accessor" | "attr_reader" | "attr_writer")
                {
                    handle_attr(ctx, child, src, m, scope, class_id, is_public);
                } else if method == Some("alias_method")
                    && let Some(args) = child.child_by_field_name("arguments")
                {
                    let mut ac = args.walk();
                    let syms: Vec<&str> = args
                        .children(&mut ac)
                        .filter(|a| a.kind() == "simple_symbol")
                        .map(|a| crate::util::text(a, src))
                        .collect();
                    if syms.len() == 2 {
                        let new_name = strip_quotes(syms[0]).to_string();
                        let old_name = strip_quotes(syms[1]).to_string();
                        let qname = if scope.is_empty() {
                            format!("{}.{new_name}", ctx.module)
                        } else {
                            format!("{}::{scope}.{new_name}", ctx.module)
                        };
                        let id = ctx.push_with_visibility(
                            child,
                            src,
                            Kind::Method,
                            qname,
                            new_name,
                            is_public,
                        );
                        if let Some(pid) = class_id {
                            ctx.out.edges.push(Edge {
                                kind: EdgeKind::Defines,
                                src: pid.to_string(),
                                dst: id.clone(),
                                confidence: 1.0,
                            });
                        }
                        ctx.out.edges.push(Edge {
                            kind: EdgeKind::Calls,
                            src: id,
                            dst: old_name,
                            confidence: 1.0,
                        });
                    }
                } else if method == Some("refine") {
                    let target = child.child_by_field_name("arguments").and_then(|a| {
                        let mut c = a.walk();
                        a.children(&mut c)
                            .find(|n| matches!(n.kind(), "constant" | "scope_resolution"))
                            .map(|n| text(n, src).to_string())
                    });
                    if let Some(t) = target {
                        let prev = ctx.refinement_target.replace(t);
                        if let Some(block) =
                            child.child_by_field_name("block").or_else(|| {
                                let mut bc = child.walk();
                                child.children(&mut bc).find(|n| {
                                    n.kind() == "do_block" || n.kind() == "block"
                                })
                            })
                        {
                            walk_inner(ctx, block, src, scope, class_id, is_public, depth + 1);
                        }
                        ctx.refinement_target = prev;
                    }
                } else if let Some(pid) = class_id
                    && method == Some("define_method")
                {
                    handle_define_method(ctx, child, src, scope, pid, is_public);
                } else if class_id.is_some() && method == Some("included") {
                    if let Some(block) = child.child_by_field_name("block").or_else(|| {
                        let mut bc = child.walk();
                        child
                            .children(&mut bc)
                            .find(|n| n.kind() == "do_block" || n.kind() == "block")
                    }) {
                        walk_inner(ctx, block, src, scope, class_id, is_public, depth + 1);
                    }
                } else if crate::framework::rails::dispatch_call(
                    ctx, child, src, method, class_id, scope, is_public,
                ) {
                    // Rails consumed it.
                } else {
                    walk_inner(ctx, child, src, scope, class_id, is_public, depth + 1);
                }
            }
            _ => walk_inner(ctx, child, src, scope, class_id, is_public, depth + 1),
        }
    }
}
