"""Graph algorithms for pathway execution.

This module provides graph analysis utilities used by the VM:
- Topological sorting for execution order
- Reachability analysis for streaming execution
"""

from collections import defaultdict, deque

from core.pathways.nodes.base import NodeBase

# Hoisted imports (Rule 1: No lazy imports)
from core.pathways.pathway import Connection, Pathway, PathwayMode
from core.pathways.validation import find_cycle


def topological_sort(pathway: Pathway) -> list[str]:
    """Get execution order via topological sort.

    Considers all real nodes. Pseudo-nodes ('input'/'output') are only skipped
    if they don't exist as actual nodes in pathway.nodes.

    Args:
        pathway: The pathway to sort

    Returns
    -------
        List of node IDs in execution order

    Raises
    ------
        ValueError: If pathway contains cycles (in DAG mode)
    """
    # Using hoisted imports: PathwayMode, find_cycle
    deps: dict[str, set[str]] = defaultdict(set)
    nodes = pathway.nodes or {}
    for conn in pathway.connections:
        # Skip connections to/from non-existent nodes
        # This naturally handles pseudo-nodes that aren't real nodes
        if conn.from_node not in nodes or conn.to_node not in nodes:
            continue
        deps[conn.to_node].add(conn.from_node)

    in_degree = {node_id: len(deps.get(node_id, set())) for node_id in pathway.nodes}
    queue = deque([node_id for node_id, degree in in_degree.items() if degree == 0])
    result = []

    while queue:
        node_id = queue.popleft()
        result.append(node_id)

        for other_id, other_deps in deps.items():
            if node_id in other_deps:
                in_degree[other_id] -= 1
                if in_degree[other_id] == 0:
                    queue.append(other_id)

    if len(result) != len(pathway.nodes):
        # Cycles are allowed in STATEFUL mode - skip validation
        if pathway.mode == PathwayMode.STATEFUL:
            # In stateful mode, return nodes in definition order for cycle-aware execution
            return list(pathway.nodes.keys())

        try:
            # Using hoisted import: find_cycle
            cycle = find_cycle(pathway)
        except Exception:
            cycle = None
        if cycle:
            raise ValueError(f"Pathway contains cycles! Cycle: {' -> '.join(cycle)}")
        raise ValueError("Pathway contains cycles!")

    return result


def reachable_from(
    start: str,
    connections: list[Connection],
    all_nodes: dict[str, NodeBase],
) -> set[str]:
    """Find all nodes reachable from a starting node.

    Args:
        start: Starting node ID
        connections: List of Connection objects
        all_nodes: Dict of all nodes in the pathway

    Returns
    -------
        Set of node IDs reachable from start
    """
    # Build adjacency list
    outgoing: dict[str, set[str]] = {}
    for c in connections:
        if c.from_node in all_nodes and c.to_node in all_nodes:
            outgoing.setdefault(c.from_node, set()).add(c.to_node)

    seen: set[str] = set()
    stack = [start]
    while stack:
        cur = stack.pop()
        for nxt in outgoing.get(cur, set()):
            if nxt not in seen:
                seen.add(nxt)
                stack.append(nxt)
    return seen
