"""Dependency resolution for container groups using topological sort."""

from __future__ import annotations

from collections import deque

from gaol.groups.models import ContainerRef


class CircularDependencyError(Exception):
    """Raised when a circular dependency is detected."""

    def __init__(self, cycle: list[str]) -> None:
        self.cycle = cycle
        cycle_str = " -> ".join(cycle)
        super().__init__(f"Circular dependency detected: {cycle_str}")


class DependencyResolver:
    """Resolve container startup and shutdown order using topological sort.

    Uses Kahn's algorithm for topological sorting, which naturally
    detects cycles and provides deterministic ordering.
    """

    def __init__(self, containers: dict[str, ContainerRef]) -> None:
        """Initialize the resolver.

        Args:
            containers: Mapping of container names to their configurations
        """
        self._containers = containers
        self._build_graph()

    def _build_graph(self) -> None:
        """Build the dependency graph."""
        # adjacency list: name -> list of containers that depend on it
        self._dependents: dict[str, list[str]] = {name: [] for name in self._containers}
        # in-degree: number of dependencies for each container
        self._in_degree: dict[str, int] = {name: 0 for name in self._containers}

        for name, container in self._containers.items():
            for dep in container.depends_on:
                if dep in self._containers:
                    self._dependents[dep].append(name)
                    self._in_degree[name] += 1

    def resolve_start_order(self) -> list[str]:
        """Resolve the order in which containers should be started.

        Containers with no dependencies are started first, followed by
        containers whose dependencies have all been started.

        Returns:
            List of container names in startup order

        Raises:
            CircularDependencyError: If a circular dependency is detected
        """
        # Copy in-degrees so we don't modify the original
        in_degree = self._in_degree.copy()

        # Start with containers that have no dependencies
        queue: deque[str] = deque()
        for name, degree in in_degree.items():
            if degree == 0:
                queue.append(name)

        result: list[str] = []

        while queue:
            # Process containers in sorted order for determinism
            current = queue.popleft()
            result.append(current)

            # Reduce in-degree for dependents
            for dependent in sorted(self._dependents[current]):
                in_degree[dependent] -= 1
                if in_degree[dependent] == 0:
                    queue.append(dependent)

        # Check for cycles
        if len(result) != len(self._containers):
            cycle = self._find_cycle()
            raise CircularDependencyError(cycle)

        return result

    def resolve_stop_order(self) -> list[str]:
        """Resolve the order in which containers should be stopped.

        This is the reverse of the start order - containers that depend
        on others are stopped first.

        Returns:
            List of container names in shutdown order

        Raises:
            CircularDependencyError: If a circular dependency is detected
        """
        return list(reversed(self.resolve_start_order()))

    def _find_cycle(self) -> list[str]:
        """Find a cycle in the dependency graph using DFS.

        Returns:
            List of container names forming a cycle
        """
        visited: set[str] = set()
        rec_stack: set[str] = set()
        path: list[str] = []

        def dfs(node: str) -> list[str] | None:
            visited.add(node)
            rec_stack.add(node)
            path.append(node)

            container = self._containers[node]
            for dep in container.depends_on:
                if dep not in self._containers:
                    continue

                if dep not in visited:
                    result = dfs(dep)
                    if result:
                        return result
                elif dep in rec_stack:
                    # Found cycle - extract the cycle from path
                    cycle_start = path.index(dep)
                    return path[cycle_start:] + [dep]

            path.pop()
            rec_stack.remove(node)
            return None

        for name in self._containers:
            if name not in visited:
                cycle = dfs(name)
                if cycle:
                    return cycle

        return []  # Should not reach here if _find_cycle is called after cycle detection

    def get_dependencies(self, container_name: str) -> list[str]:
        """Get the direct dependencies of a container.

        Args:
            container_name: Name of the container

        Returns:
            List of container names that this container depends on
        """
        if container_name not in self._containers:
            return []
        return list(self._containers[container_name].depends_on)

    def get_dependents(self, container_name: str) -> list[str]:
        """Get the containers that depend on the given container.

        Args:
            container_name: Name of the container

        Returns:
            List of container names that depend on this container
        """
        if container_name not in self._dependents:
            return []
        return list(self._dependents[container_name])

    def get_all_dependencies(self, container_name: str) -> set[str]:
        """Get all transitive dependencies of a container.

        Args:
            container_name: Name of the container

        Returns:
            Set of all container names that this container transitively depends on
        """
        if container_name not in self._containers:
            return set()

        result: set[str] = set()
        queue: deque[str] = deque(self._containers[container_name].depends_on)

        while queue:
            dep = queue.popleft()
            if dep in result or dep not in self._containers:
                continue
            result.add(dep)
            queue.extend(self._containers[dep].depends_on)

        return result


def resolve_start_order(containers: dict[str, ContainerRef]) -> list[str]:
    """Convenience function to resolve start order.

    Args:
        containers: Mapping of container names to their configurations

    Returns:
        List of container names in startup order
    """
    resolver = DependencyResolver(containers)
    return resolver.resolve_start_order()


def resolve_stop_order(containers: dict[str, ContainerRef]) -> list[str]:
    """Convenience function to resolve stop order.

    Args:
        containers: Mapping of container names to their configurations

    Returns:
        List of container names in shutdown order
    """
    resolver = DependencyResolver(containers)
    return resolver.resolve_stop_order()
