"""Call graph builder using parsed syntax trees."""

from __future__ import annotations

import ast
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Iterator

from ieum.analysis.treesitter.parser import TreeSitterParser, ParsedFile


@dataclass
class FunctionCall:
    """Represents a function call."""

    caller: str  # Function that makes the call
    callee: str  # Function being called
    file_path: str
    line: int
    is_method: bool = False
    receiver: str | None = None  # Object/class the method is called on


@dataclass
class CallGraphNode:
    """Node in the call graph."""

    name: str
    file_path: str
    line: int
    calls: list[str] = field(default_factory=list)  # Functions this node calls
    called_by: list[str] = field(default_factory=list)  # Functions that call this node


@dataclass
class CallGraph:
    """Complete call graph for a codebase."""

    nodes: dict[str, CallGraphNode]  # name -> node
    edges: list[FunctionCall]
    entry_points: list[str]  # Functions not called by anyone
    leaf_nodes: list[str]  # Functions that don't call anything

    def get_callers(self, function_name: str) -> list[str]:
        """Get all functions that call the given function."""
        if function_name in self.nodes:
            return self.nodes[function_name].called_by
        return []

    def get_callees(self, function_name: str) -> list[str]:
        """Get all functions called by the given function."""
        if function_name in self.nodes:
            return self.nodes[function_name].calls
        return []

    def get_call_chain(self, function_name: str, max_depth: int = 10) -> dict:
        """Get the call chain starting from a function."""
        visited = set()

        def _get_chain(name: str, depth: int) -> dict | None:
            if depth > max_depth or name in visited:
                return None
            visited.add(name)

            if name not in self.nodes:
                return {"name": name, "calls": []}

            node = self.nodes[name]
            return {
                "name": name,
                "file": node.file_path,
                "line": node.line,
                "calls": [
                    _get_chain(callee, depth + 1)
                    for callee in node.calls
                    if callee not in visited
                ],
            }

        return _get_chain(function_name, 0) or {"name": function_name, "calls": []}

    def get_reverse_chain(self, function_name: str, max_depth: int = 10) -> dict:
        """Get all callers of a function recursively."""
        visited = set()

        def _get_chain(name: str, depth: int) -> dict | None:
            if depth > max_depth or name in visited:
                return None
            visited.add(name)

            if name not in self.nodes:
                return {"name": name, "called_by": []}

            node = self.nodes[name]
            return {
                "name": name,
                "file": node.file_path,
                "line": node.line,
                "called_by": [
                    _get_chain(caller, depth + 1)
                    for caller in node.called_by
                    if caller not in visited
                ],
            }

        return _get_chain(function_name, 0) or {"name": function_name, "called_by": []}

    def to_mermaid(self, focus_function: str | None = None, max_nodes: int = 50) -> str:
        """Export call graph to Mermaid diagram format."""
        lines = ["graph TD"]

        if focus_function and focus_function in self.nodes:
            # Show only related nodes
            related = {focus_function}
            node = self.nodes[focus_function]
            related.update(node.calls)
            related.update(node.called_by)

            for name in list(related)[:max_nodes]:
                safe_name = name.replace(".", "_").replace("<", "_").replace(">", "_")
                lines.append(f"    {safe_name}[{name}]")

            for edge in self.edges:
                if edge.caller in related and edge.callee in related:
                    caller = edge.caller.replace(".", "_").replace("<", "_").replace(">", "_")
                    callee = edge.callee.replace(".", "_").replace("<", "_").replace(">", "_")
                    lines.append(f"    {caller} --> {callee}")
        else:
            # Show all nodes up to max
            shown_nodes = list(self.nodes.keys())[:max_nodes]
            for name in shown_nodes:
                safe_name = name.replace(".", "_").replace("<", "_").replace(">", "_")
                lines.append(f"    {safe_name}[{name}]")

            for edge in self.edges:
                if edge.caller in shown_nodes and edge.callee in shown_nodes:
                    caller = edge.caller.replace(".", "_").replace("<", "_").replace(">", "_")
                    callee = edge.callee.replace(".", "_").replace("<", "_").replace(">", "_")
                    lines.append(f"    {caller} --> {callee}")

        return "\n".join(lines)

    def to_dot(self, focus_function: str | None = None) -> str:
        """Export call graph to DOT format for Graphviz."""
        lines = ["digraph CallGraph {", "    rankdir=LR;"]

        nodes_to_show = set(self.nodes.keys())
        if focus_function and focus_function in self.nodes:
            nodes_to_show = {focus_function}
            node = self.nodes[focus_function]
            nodes_to_show.update(node.calls)
            nodes_to_show.update(node.called_by)

        for name in nodes_to_show:
            safe_name = name.replace(".", "_").replace("<", "_").replace(">", "_")
            label = name
            if name in self.entry_points:
                lines.append(f'    {safe_name} [label="{label}" style=filled fillcolor=lightgreen];')
            elif name in self.leaf_nodes:
                lines.append(f'    {safe_name} [label="{label}" style=filled fillcolor=lightyellow];')
            else:
                lines.append(f'    {safe_name} [label="{label}"];')

        for edge in self.edges:
            if edge.caller in nodes_to_show and edge.callee in nodes_to_show:
                caller = edge.caller.replace(".", "_").replace("<", "_").replace(">", "_")
                callee = edge.callee.replace(".", "_").replace("<", "_").replace(">", "_")
                lines.append(f"    {caller} -> {callee};")

        lines.append("}")
        return "\n".join(lines)


class CallGraphBuilder:
    """Builds call graphs from parsed source files."""

    def __init__(self) -> None:
        """Initialize the call graph builder."""
        self.parser = TreeSitterParser()

    def build_from_file(self, file_path: str | Path) -> CallGraph:
        """Build call graph from a single file."""
        parsed = self.parser.parse_file(file_path)
        return self._build_graph([parsed])

    def build_from_directory(
        self,
        directory: str | Path,
        extensions: list[str] | None = None,
    ) -> CallGraph:
        """Build call graph from all files in a directory."""
        parsed_files = list(self.parser.parse_directory(directory, extensions))
        return self._build_graph(parsed_files)

    def build_from_files(self, file_paths: list[str | Path]) -> CallGraph:
        """Build call graph from a list of files."""
        parsed_files = [self.parser.parse_file(f) for f in file_paths]
        return self._build_graph(parsed_files)

    def _build_graph(self, parsed_files: list[ParsedFile]) -> CallGraph:
        """Build call graph from parsed files."""
        nodes: dict[str, CallGraphNode] = {}
        edges: list[FunctionCall] = []

        # First pass: collect all function definitions
        for parsed in parsed_files:
            for node in parsed.nodes:
                if node.type == "function":
                    full_name = self._get_full_name(node, parsed.file_path)
                    nodes[full_name] = CallGraphNode(
                        name=full_name,
                        file_path=parsed.file_path,
                        line=node.start_line,
                    )

        # Second pass: find function calls
        for parsed in parsed_files:
            if parsed.language == "python":
                file_edges = self._extract_python_calls(parsed)
            elif parsed.language in ("javascript", "typescript", "tsx"):
                file_edges = self._extract_js_calls(parsed)
            elif parsed.language in ("c", "cpp"):
                file_edges = self._extract_cpp_calls(parsed)
            else:
                file_edges = []

            edges.extend(file_edges)

        # Build adjacency lists
        for edge in edges:
            if edge.caller in nodes:
                if edge.callee not in nodes[edge.caller].calls:
                    nodes[edge.caller].calls.append(edge.callee)

            if edge.callee in nodes:
                if edge.caller not in nodes[edge.callee].called_by:
                    nodes[edge.callee].called_by.append(edge.caller)

        # Find entry points (not called by anyone)
        all_callees = {e.callee for e in edges}
        entry_points = [name for name in nodes if name not in all_callees]

        # Find leaf nodes (don't call anything)
        leaf_nodes = [name for name, node in nodes.items() if not node.calls]

        return CallGraph(
            nodes=nodes,
            edges=edges,
            entry_points=entry_points,
            leaf_nodes=leaf_nodes,
        )

    def _get_full_name(self, node, file_path: str) -> str:
        """Get fully qualified name for a node."""
        # Use file basename as module name
        module = Path(file_path).stem
        if node.parent_name:
            return f"{module}.{node.parent_name}.{node.name}"
        return f"{module}.{node.name}"

    def _extract_python_calls(self, parsed: ParsedFile) -> list[FunctionCall]:
        """Extract function calls from Python source."""
        edges: list[FunctionCall] = []

        try:
            source = Path(parsed.file_path).read_text(encoding="utf-8", errors="replace")
            tree = ast.parse(source)
        except Exception:
            return edges

        # Map line numbers to containing functions
        function_ranges: list[tuple[str, int, int]] = []
        for node in parsed.nodes:
            if node.type == "function":
                full_name = self._get_full_name(node, parsed.file_path)
                function_ranges.append((full_name, node.start_line, node.end_line))

        def get_containing_function(line: int) -> str | None:
            for name, start, end in function_ranges:
                if start <= line <= end:
                    return name
            return None

        # Find all calls
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                line = node.lineno
                caller = get_containing_function(line)
                if not caller:
                    continue

                # Get callee name
                callee = None
                is_method = False
                receiver = None

                if isinstance(node.func, ast.Name):
                    callee = node.func.id
                elif isinstance(node.func, ast.Attribute):
                    callee = node.func.attr
                    is_method = True
                    if isinstance(node.func.value, ast.Name):
                        receiver = node.func.value.id

                if callee:
                    edges.append(FunctionCall(
                        caller=caller,
                        callee=callee,
                        file_path=parsed.file_path,
                        line=line,
                        is_method=is_method,
                        receiver=receiver,
                    ))

        return edges

    def _extract_js_calls(self, parsed: ParsedFile) -> list[FunctionCall]:
        """Extract function calls from JavaScript/TypeScript source."""
        edges: list[FunctionCall] = []

        try:
            source = Path(parsed.file_path).read_text(encoding="utf-8", errors="replace")
        except Exception:
            return edges

        lines = source.split("\n")

        # Map line numbers to containing functions
        function_ranges: list[tuple[str, int, int]] = []
        for node in parsed.nodes:
            if node.type == "function":
                full_name = self._get_full_name(node, parsed.file_path)
                # Estimate end line (simplified)
                function_ranges.append((full_name, node.start_line, node.end_line + 50))

        def get_containing_function(line: int) -> str | None:
            for name, start, end in function_ranges:
                if start <= line <= end:
                    return name
            return None

        # Simple regex for function calls
        call_pattern = re.compile(r"(\w+)\s*\(")

        for i, line in enumerate(lines, 1):
            caller = get_containing_function(i)
            if not caller:
                continue

            for match in call_pattern.finditer(line):
                callee = match.group(1)
                # Skip keywords
                if callee not in ("if", "for", "while", "switch", "catch", "function", "return"):
                    edges.append(FunctionCall(
                        caller=caller,
                        callee=callee,
                        file_path=parsed.file_path,
                        line=i,
                    ))

        return edges

    def _extract_cpp_calls(self, parsed: ParsedFile) -> list[FunctionCall]:
        """Extract function calls from C/C++ source."""
        edges: list[FunctionCall] = []

        try:
            source = Path(parsed.file_path).read_text(encoding="utf-8", errors="replace")
        except Exception:
            return edges

        lines = source.split("\n")

        # Map line numbers to containing functions
        function_ranges: list[tuple[str, int, int]] = []
        for node in parsed.nodes:
            if node.type == "function":
                full_name = self._get_full_name(node, parsed.file_path)
                function_ranges.append((full_name, node.start_line, node.end_line + 100))

        def get_containing_function(line: int) -> str | None:
            for name, start, end in function_ranges:
                if start <= line <= end:
                    return name
            return None

        # Simple regex for function calls
        call_pattern = re.compile(r"(\w+)\s*\(")

        for i, line in enumerate(lines, 1):
            # Skip preprocessor
            if line.strip().startswith("#"):
                continue

            caller = get_containing_function(i)
            if not caller:
                continue

            for match in call_pattern.finditer(line):
                callee = match.group(1)
                # Skip keywords and types
                if callee not in ("if", "for", "while", "switch", "catch", "return",
                                  "sizeof", "typeof", "static_cast", "dynamic_cast",
                                  "const_cast", "reinterpret_cast"):
                    edges.append(FunctionCall(
                        caller=caller,
                        callee=callee,
                        file_path=parsed.file_path,
                        line=i,
                    ))

        return edges
