//! Python AST walker — visits the tree-sitter tree and dispatches to
//! declaration/call/import handlers. Contains `walk`, `collect_docstring`,
//! and `decorator_role`.

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

use crate::util::text;
use super::Ctx;
use super::decls::emit_class_fields;
use super::calls::emit_calls;
use super::imports::add_python_aliases;

fn name_of(n: Node<'_>, src: &str) -> Option<String> {
    n.child_by_field_name("name")
        .map(|x| text(x, src).to_string())
}

/// Extract `"""docstring"""` from a function/class body.
///
/// M15: refactored control flow — early-return on the first string literal
/// found in the first `expression_statement`. Any other leading statement
/// immediately terminates the search (only the first statement can be a
/// docstring per Python spec).
fn collect_docstring(body: Node<'_>, src: &str) -> String {
    let mut c = body.walk();
    for child in body.children(&mut c) {
        if child.kind() == "expression_statement" {
            // Look for a string literal child inside the expression_statement.
            let mut cc = child.walk();
            let string_node = child
                .children(&mut cc)
                .find(|s| s.kind() == "string");
            if let Some(s) = string_node {
                let raw = text(s, src);
                return raw.trim_matches('"').trim_matches('\'').trim().to_string();
            }
        }
        // Only the very first non-extra child can be a docstring.
        if !child.is_extra() {
            break;
        }
    }
    String::new()
}

/// Recognise role-defining stdlib decorators: `@property`,
/// `@classmethod`, `@staticmethod`, `@cached_property`, `@abstractmethod`,
/// plus the descriptor pair `@x.setter` / `@x.getter` / `@x.deleter`.
/// Returns the role tag stamped into `Symbol.doc`.
pub(crate) fn decorator_role(decorated: Node<'_>, src: &str) -> Option<String> {
    let mut c = decorated.walk();
    for child in decorated.children(&mut c) {
        if child.kind() != "decorator" {
            continue;
        }
        let raw = text(child, src).trim().trim_start_matches('@');
        let head = raw.split(['(', ' ']).next().unwrap_or("").trim();
        // `<ident>.setter` / `.getter` / `.deleter` form.
        if let Some(idx) = head.rfind('.') {
            let suffix = &head[idx + 1..];
            if matches!(suffix, "setter" | "getter" | "deleter") {
                return Some(format!("{}.{suffix}", &head[..idx]));
            }
        }
        let bare = head.split('.').next().unwrap_or("");
        match bare {
            "property" => return Some("property".into()),
            "classmethod" => return Some("classmethod".into()),
            "staticmethod" => return Some("staticmethod".into()),
            "cached_property" => return Some("cached_property".into()),
            "abstractmethod" => return Some("abstractmethod".into()),
            _ => {}
        }
    }
    None
}

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

const MAX_DEPTH: usize = 512;

fn walk_inner(ctx: &mut Ctx<'_>, node: Node<'_>, src: &str, scope: &str, depth: usize) {
    if depth >= MAX_DEPTH {
        tracing::warn!(
            "ckg-parse/python: 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_definition" | "decorated_definition" => {
                let inner = if child.kind() == "decorated_definition" {
                    child.child_by_field_name("definition").unwrap_or(child)
                } else {
                    child
                };
                let kind = if inner.kind() == "function_definition" {
                    if scope.is_empty() {
                        Kind::Function
                    } else {
                        Kind::Method
                    }
                } else if inner.kind() == "class_definition" {
                    Kind::Class
                } else {
                    continue;
                };
                let Some(name) = name_of(inner, src) else {
                    continue;
                };
                let qname = if scope.is_empty() {
                    format!("{}.{name}", ctx.module)
                } else {
                    format!("{}.{}.{name}", ctx.module, scope)
                };
                let body = inner.child_by_field_name("body");
                let mut doc = body.map(|b| collect_docstring(b, src)).unwrap_or_default();
                if child.kind() == "decorated_definition"
                    && let Some(role) = decorator_role(child, src)
                {
                    if doc.is_empty() {
                        doc = format!("@{role}");
                    } else {
                        doc = format!("@{role}\n{doc}");
                    }
                }
                if matches!(kind, Kind::Function | Kind::Method)
                    && name.starts_with("test_")
                    && !doc.contains("@test")
                {
                    if doc.is_empty() {
                        doc = "@test".into();
                    } else {
                        doc = format!("@test\n{doc}");
                    }
                }
                let id = ctx.push(inner, src, kind, qname, name.clone(), doc);

                if child.kind() == "decorated_definition" && inner.kind() == "function_definition" {
                    crate::framework::fastapi::detect_python_route(ctx, child, src, &name);
                }

                if inner.kind() == "class_definition" {
                    if let Some(supers) = inner.child_by_field_name("superclasses") {
                        let mut cc = supers.walk();
                        for s in supers.children(&mut cc) {
                            if s.kind() == "identifier" || s.kind() == "attribute" {
                                ctx.out.edges.push(Edge {
                                    kind: EdgeKind::Extends,
                                    src: id.clone(),
                                    dst: text(s, src).to_string(),
                                    confidence: 0.9,
                                });
                            }
                        }
                    }
                    if let Some(body) = body {
                        emit_class_fields(ctx, body, src, &id, &name, scope);
                        let inner_scope = if scope.is_empty() {
                            name.clone()
                        } else {
                            format!("{}.{name}", scope)
                        };
                        walk_inner(ctx, body, src, &inner_scope, depth + 1);
                    }
                } else if let Some(body) = body {
                    emit_calls(ctx, body, src, &id);
                }
            }
            "class_definition" => {
                let Some(name) = name_of(child, src) else {
                    continue;
                };
                let qname = if scope.is_empty() {
                    format!("{}.{name}", ctx.module)
                } else {
                    format!("{}.{}.{name}", ctx.module, scope)
                };
                let body = child.child_by_field_name("body");
                let doc = body.map(|b| collect_docstring(b, src)).unwrap_or_default();
                let id = ctx.push(child, src, Kind::Class, qname, name.clone(), doc);
                if let Some(supers) = child.child_by_field_name("superclasses") {
                    let mut cc = supers.walk();
                    for s in supers.children(&mut cc) {
                        if s.kind() == "identifier" || s.kind() == "attribute" {
                            ctx.out.edges.push(Edge {
                                kind: EdgeKind::Extends,
                                src: id.clone(),
                                dst: text(s, src).to_string(),
                                confidence: 0.9,
                            });
                        }
                    }
                }
                if let Some(body) = body {
                    emit_class_fields(ctx, body, src, &id, &name, scope);
                    let inner_scope = if scope.is_empty() {
                        name.clone()
                    } else {
                        format!("{}.{name}", scope)
                    };
                    walk_inner(ctx, body, src, &inner_scope, depth + 1);
                }
            }
            "import_statement" | "import_from_statement" => {
                let body = text(child, src).trim().trim_end_matches(';').to_string();
                ctx.out.edges.push(Edge {
                    kind: EdgeKind::Imports,
                    src: ctx.module.clone(),
                    dst: body.clone(),
                    confidence: 0.9,
                });
                add_python_aliases(&mut ctx.aliases, child, src, &ctx.module);
            }
            "type_alias_statement" => {
                let alias_name = child
                    .child_by_field_name("left")
                    .map(|n| text(n, src).to_string())
                    .unwrap_or_default();
                if !alias_name.is_empty() {
                    let qname = if scope.is_empty() {
                        format!("{}.{alias_name}", ctx.module)
                    } else {
                        format!("{}.{}.{alias_name}", ctx.module, scope)
                    };
                    ctx.push(
                        child,
                        src,
                        Kind::TypeAlias,
                        qname,
                        alias_name,
                        String::new(),
                    );
                }
            }
            "expression_statement" => {
                let mut sc = child.walk();
                for s in child.children(&mut sc) {
                    if s.kind() == "assignment"
                        && let (Some(left), Some(ty)) =
                            (s.child_by_field_name("left"), s.child_by_field_name("type"))
                        && left.kind() == "identifier"
                        && text(ty, src).contains("TypeAlias")
                    {
                        let alias_name = text(left, src).to_string();
                        let qname = if scope.is_empty() {
                            format!("{}.{alias_name}", ctx.module)
                        } else {
                            format!("{}.{}.{alias_name}", ctx.module, scope)
                        };
                        ctx.push(
                            child,
                            src,
                            Kind::TypeAlias,
                            qname,
                            alias_name,
                            String::new(),
                        );
                    }
                }
                crate::framework::fastapi::detect_router_registration(ctx, child, src);
                walk_inner(ctx, child, src, scope, depth + 1);
            }
            _ => walk_inner(ctx, child, src, scope, depth + 1),
        }
    }
}

