"""Indirection analysis for Python code.

Detects factory patterns, pass-through wrappers, and unnecessary indirection.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from vibelexity.models import PatternViolation
from vibelexity.parsers.python import collect_functions

if TYPE_CHECKING:
    from tree_sitter import Node


def _has_nested_function_definition(func_node: Node) -> bool:
    """Check if function contains a nested function definition."""
    for child in func_node.children:
        if child.type == "block":
            for stmt in child.children:
                if stmt.type == "function_definition":
                    return True
                for stmt_child in stmt.children:
                    if stmt_child.type == "function_definition":
                        return True
    return False


def _has_lambda_in_return(func_node: Node) -> bool:
    """Check if function's return statement contains a lambda."""
    for child in func_node.children:
        if child.type == "block":
            for stmt in child.children:
                if stmt.type == "return_statement":
                    for ret_child in stmt.children:
                        if ret_child.type == "lambda":
                            return True
    return False


def _get_function_name(node: Node) -> str:
    """Extract function name from a tree-sitter function_definition node."""
    for child in node.children:
        if child.type == "identifier":
            return child.text.decode() if child.text else "<anonymous>"
    return "<anonymous>"


def _is_creating_pattern(name: str) -> bool:
    """Check if function name suggests it's creating/factoring something."""
    return any(
        prefix in name.lower()
        for prefix in ("create", "make", "new", "build", "factory")
    )


def check_factory_functions(root: Node) -> list[PatternViolation]:
    """Detect functions that return other functions (factory/closure pattern).

    Flags functions that:
    1. Have a return statement containing a lambda
    2. Have names suggesting factory pattern (create_, make_, etc.)
    """
    functions = collect_functions(root)
    violations = []

    for func_node in functions:
        has_lambda = _has_lambda_in_return(func_node)
        has_nested_func = _has_nested_function_definition(func_node)

        # Check if it's a factory pattern
        name = _get_function_name(func_node)
        is_factory_name = _is_creating_pattern(name)

        # Flag if it returns a lambda OR has nested function AND suggests factory pattern
        if has_lambda or (has_nested_func and is_factory_name):
            line = func_node.start_point[0] + 1
            if has_lambda:
                desc = f"Factory function returns lambda - consider inlining"
            else:
                desc = f"Factory function returns closure - consider inlining"
            violations.append(
                PatternViolation(
                    type="factory_function",
                    line=line,
                    description=desc,
                    severity="info",
                )
            )

    return violations


def _find_calls_in_function(func_node) -> set[str]:
    """Extract all function calls within a function body."""
    calls = set()
    for child in func_node.children:
        if child.type == "block":
            for stmt in child.children:
                if stmt.type in ("return_statement", "expression_statement"):
                    for stmt_child in stmt.children:
                        if stmt_child.type == "call":
                            for call_child in stmt_child.children:
                                if call_child.type == "identifier":
                                    name = (
                                        call_child.text.decode()
                                        if call_child.text
                                        else ""
                                    )
                                    calls.add(name)
    return calls


def _build_call_map(root: Node) -> tuple[dict[str, set[str]], dict[str, int]]:
    """Build call map: ({function_name: set of callees}, {function_name: line_number})."""
    functions = collect_functions(root)
    call_map: dict[str, set[str]] = {}
    line_map: dict[str, int] = {}

    for func_node in functions:
        name = _get_function_name(func_node)
        call_map[name] = _find_calls_in_function(func_node)
        line_map[name] = func_node.start_point[0] + 1

    return call_map, line_map


def check_high_indirection_depth(root: Node) -> list[PatternViolation]:
    """Detect functions with deep call chains before reaching actual logic.

    A function has high indirection depth if it calls through multiple
    layers (3+) before reaching an implementation function (one with no calls).
    """
    call_map, line_map = _build_call_map(root)
    violations: list[PatternViolation] = []

    for func_name in call_map:
        callees = call_map[func_name]

        if not callees:
            continue

        for callee in callees:
            depth = 1
            current = callee
            visited = {func_name, callee}

            while current in call_map and call_map[current]:
                next_callees = call_map[current] - visited
                if not next_callees:
                    break
                # Pick one next callee to trace (depth-first)
                next_callee = next_callees.pop()
                depth += 1
                visited.add(next_callee)
                current = next_callee

            if depth >= 3:
                violations.append(
                    PatternViolation(
                        type="high_indirection_depth",
                        line=line_map.get(func_name, 0),
                        description=f"Call chain depth of {depth} before reaching implementation",
                        severity="warning",
                    )
                )

    return violations
