"""Process tracer — Phase 5: Execution Flow Detection.

Finds entry points and traces complete call chains through the graph.
Produces Process nodes with pre-computed execution steps.
"""

from __future__ import annotations

from collections import defaultdict
from dataclasses import dataclass, field

from ..models import Edge, EdgeType, NodeType, SymbolNode
from ._console import console

# Patterns that indicate entry points (tuples for faster `any(... in ...)`)
ENTRY_PATTERNS = {
    "python": (
        "main", "__main__", "app", "cli", "run", "start", "execute",
        "handle", "process", "setup", "init",
    ),
    "javascript": (
        "main", "app", "handler", "index", "server", "start",
        "init", "setup", "bootstrap",
    ),
    "typescript": (
        "main", "app", "handler", "index", "server", "start",
        "init", "setup", "bootstrap",
    ),
}

# Decorators / patterns that indicate route handlers (tuple for faster iteration)
ROUTE_INDICATORS = (
    "@app.", "@router.", "app.get", "app.post", "app.put", "app.delete",
    "app.route", "@click.", "@main.",
)

# Verb prefixes that indicate entry-point functions
_ENTRY_VERB_PREFIXES = ("get_", "create_", "update_", "delete_", "list_")


@dataclass
class Process:
    """An execution flow from entry point through call chain."""
    id: str
    name: str
    entry_uid: str
    steps: list[dict] = field(default_factory=list)  # ordered call chain


def _find_entry_points(nodes: list[SymbolNode]) -> list[SymbolNode]:
    """Identify likely entry points in the codebase."""
    entries = []
    func_method_types = frozenset((NodeType.FUNCTION, NodeType.METHOD))

    for node in nodes:
        if node.node_type not in func_method_types:
            continue

        # Check name patterns
        lang_patterns = ENTRY_PATTERNS.get(node.language, ENTRY_PATTERNS["python"])
        name_lower = node.name.lower()
        is_entry = any(p in name_lower for p in lang_patterns)

        # Check for route/CLI decorators in body
        if not is_entry and node.body_text:
            is_entry = any(ind in node.body_text for ind in ROUTE_INDICATORS)

        # Functions named with verbs are often entry points
        if not is_entry and node.node_type == NodeType.FUNCTION:
            if name_lower.startswith(_ENTRY_VERB_PREFIXES):
                is_entry = True

        if is_entry:
            entries.append(node)

    return entries


def _build_call_graph(edges: list[Edge]) -> tuple[dict[str, list[str]], set[str]]:
    """Build caller -> [callees] adjacency and called_uids in a single pass."""
    graph: dict[str, list[str]] = defaultdict(list)
    called_uids: set[str] = set()
    for e in edges:
        if e.edge_type == EdgeType.CALLS:
            graph[e.source_uid].append(e.target_uid)
            called_uids.add(e.target_uid)
    return graph, called_uids


def _trace_chain(
    start_uid: str,
    call_graph: dict[str, list[str]],
    node_map: dict[str, SymbolNode],
    max_depth: int = 10,
) -> list[dict]:
    """Trace a call chain from start, returning ordered steps (iterative DFS)."""
    steps = []
    visited = set()
    # Stack of (uid, depth)
    stack = [(start_uid, 0)]

    while stack:
        uid, depth = stack.pop()
        if uid in visited or depth > max_depth:
            continue
        visited.add(uid)

        node = node_map.get(uid)
        if not node:
            continue

        steps.append({
            "depth": depth,
            "uid": uid,
            "name": node.name,
            "type": node.node_type.value,
            "file": node.file_path,
            "line": node.line_start,
            "signature": node.signature,
        })

        # Push callees in reverse order so first callee is processed first
        callees = call_graph.get(uid, [])
        for callee_uid in reversed(callees):
            if callee_uid not in visited:
                stack.append((callee_uid, depth + 1))

    return steps


def trace_processes(
    nodes: list[SymbolNode], edges: list[Edge]
) -> tuple[list[SymbolNode], list[Edge]]:
    """Phase 5: Detect entry points and trace execution flows.

    Returns:
        (process_nodes, step_edges) — Process nodes with pre-computed steps
    """
    node_map = {n.uid: n for n in nodes}
    call_graph, called_uids = _build_call_graph(edges)

    # Find entry points
    entry_points = _find_entry_points(nodes)

    entry_uids = {ep.uid for ep in entry_points}
    uncalled_funcs = [
        n for n in nodes
        if n.node_type == NodeType.FUNCTION
        and n.uid not in called_uids
        and n.uid in call_graph  # Has outgoing calls
        and n.uid not in entry_uids  # O(1) set lookup instead of O(n) list scan
    ]
    # Add top uncalled functions as additional entry points
    entry_points.extend(uncalled_funcs[:10])

    # Trace each entry point
    process_nodes: list[SymbolNode] = []
    step_edges: list[Edge] = []

    for i, entry in enumerate(entry_points):
        chain = _trace_chain(entry.uid, call_graph, node_map)
        if len(chain) < 2:  # Skip trivial processes
            continue

        process_id = f"process:{i}"
        process_name = f"{entry.name} flow"

        # Create process node
        process_node = SymbolNode(
            uid=process_id,
            name=process_name,
            node_type=NodeType.PROCESS,
            file_path=entry.file_path,
            line_start=entry.line_start,
            line_end=entry.line_end,
            properties={
                "is_process": True,
                "entry_point": entry.uid,
                "steps": chain,
                "depth": max(s["depth"] for s in chain),
                "symbol_count": len(chain),
            },
        )
        process_nodes.append(process_node)

        # Create STEP edges
        for step in chain:
            step_edges.append(Edge(
                source_uid=step["uid"],
                target_uid=process_id,
                edge_type=EdgeType.DEFINED_IN,
                properties={"step_depth": step["depth"], "process": True},
            ))

    console.print(
        f"  [green]✓[/green] Processes: {len(process_nodes)} execution flows "
        f"from {len(entry_points)} entry points"
    )

    return process_nodes, step_edges
