"""Dependency closure and DAG execution ordering for large projects.

Provides :class:`DependencyClosureComputer` which operates on a
:class:`DependencyGraph` to compute bounded transitive closures,
topological (DAG) execution orderings, and cycle detection.

Memoization is applied to the closure computation so that graphs
with 10 000+ files remain tractable.

Based on Forgejo issue #205.
"""

from __future__ import annotations

from collections import deque

from cleveragents.application.services.decomposition_models import (
    DependencyGraph,
)

# ---------------------------------------------------------------------------
# Dependency closure computer
# ---------------------------------------------------------------------------


class DependencyClosureComputer:
    """Computes dependency closures and DAG orderings on a file graph.

    All methods are stateless with respect to the graph itself; the
    memoization cache is instance-scoped and can be reused across
    multiple calls on the same graph.
    """

    def __init__(self) -> None:
        self._closure_cache: dict[str, frozenset[str]] = {}

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def compute_closure(
        self,
        graph: DependencyGraph,
        root_files: list[str],
        cutoff: int = 0,
    ) -> frozenset[str]:
        """Return the bounded transitive closure of *root_files*.

        Starting from every file in *root_files*, follows forward edges
        in *graph* (breadth-first) collecting all reachable nodes.
        When *cutoff* > 0 the traversal stops once the closure reaches
        *cutoff* nodes.

        Results are memoised per root file so repeated queries are O(1).

        Args:
            graph: The dependency graph to traverse.
            root_files: Starting file paths.
            cutoff: Maximum closure size.  ``0`` means unlimited.

        Returns:
            Frozenset of all reachable file paths (including roots).
        """
        result: set[str] = set()
        for root in root_files:
            if root in self._closure_cache:
                result.update(self._closure_cache[root])
                continue

            visited: set[str] = set()
            queue: deque[str] = deque()
            if root in graph.file_nodes:
                queue.append(root)

            while queue:
                if cutoff > 0 and len(visited) >= cutoff:
                    break
                node = queue.popleft()
                if node in visited:
                    continue
                visited.add(node)
                for succ in graph.successors(node):
                    if succ not in visited:
                        queue.append(succ)

            frozen = frozenset(visited)
            self._closure_cache[root] = frozen
            result.update(frozen)

        if cutoff > 0 and len(result) > cutoff:
            # Deterministic trimming — keep the roots plus
            # lexicographically first nodes up to *cutoff*.
            root_set = set(root_files)
            extras = sorted(result - root_set)
            kept = root_set | set(extras[: cutoff - len(root_set)])
            return frozenset(kept)
        return frozenset(result)

    def topological_sort(
        self,
        graph: DependencyGraph,
    ) -> list[str]:
        """Return a topological (Kahn) ordering of *graph*.

        If the graph contains cycles the returned list will be
        shorter than the number of nodes — call :meth:`detect_cycles`
        to diagnose.

        Returns:
            List of file paths in dependency-safe execution order
            (nodes with no incoming edges first).
        """
        in_degree: dict[str, int] = dict(graph.in_degree)
        for node in graph.file_nodes:
            in_degree.setdefault(node, 0)

        queue: deque[str] = deque(sorted(n for n, d in in_degree.items() if d == 0))
        result: list[str] = []

        while queue:
            node = queue.popleft()
            result.append(node)
            for succ in sorted(graph.successors(node)):
                in_degree[succ] -= 1
                if in_degree[succ] == 0:
                    queue.append(succ)

        return result

    def detect_cycles(
        self,
        graph: DependencyGraph,
    ) -> list[list[str]]:
        """Return all elementary cycles in *graph*.

        Uses iterative DFS colouring (white/grey/black).  Each time a
        back-edge is found, the cycle path is extracted from the
        recursion stack.

        Returns:
            List of cycles, each cycle being an ordered list of file
            paths forming the loop.  Empty when the graph is a DAG.
        """
        white: set[str] = set(graph.file_nodes)
        grey: set[str] = set()
        black: set[str] = set()
        parent_map: dict[str, str | None] = {}
        cycles: list[list[str]] = []

        def _dfs(node: str) -> None:
            white.discard(node)
            grey.add(node)
            for succ in sorted(graph.successors(node)):
                if succ in grey:
                    # Back-edge found — extract cycle
                    cycle = [succ, node]
                    cur = node
                    while cur != succ:
                        p = parent_map.get(cur)
                        if p is None or p == succ:
                            break
                        cycle.append(p)
                        cur = p
                    cycle.reverse()
                    cycles.append(cycle)
                elif succ in white:
                    parent_map[succ] = node
                    _dfs(succ)
            grey.discard(node)
            black.add(node)

        for start in sorted(graph.file_nodes):
            if start in white:
                parent_map[start] = None
                _dfs(start)

        return cycles

    def clear_cache(self) -> None:
        """Drop all memoised closure results."""
        self._closure_cache.clear()


__all__: list[str] = [
    "DependencyClosureComputer",
]
