from __future__ import annotations

from enum import Enum

from pydantic import BaseModel


class LoopStrategy(str, Enum):
    HARD_STOP = "hard_stop"
    MAX_ITERATIONS = "max_iterations"
    SEMANTIC_CONVERGENCE = "semantic_convergence"


class LoopDetectionResult(BaseModel):
    has_cycle: bool
    cycle_path: list[str]
    recommended_action: str


class LoopDetective:
    """Cycle detection and loop break strategy logic."""

    def detect_cycles(self, adjacency: dict[str, list[str]]) -> LoopDetectionResult:
        """Detect first cycle in directed graph using DFS."""
        visited: set[str] = set()
        rec_stack: set[str] = set()
        for node in adjacency:
            if node not in visited:
                path = self._dfs(node, adjacency, visited, rec_stack, [])
                if path:
                    return LoopDetectionResult(has_cycle=True, cycle_path=path, recommended_action="hard_stop")
        return LoopDetectionResult(has_cycle=False, cycle_path=[], recommended_action="none")

    def should_break(
        self,
        node_id: str,
        iteration: int,
        strategy: LoopStrategy,
        max_iterations: int = 5,
        state_delta: float | None = None,
        convergence_threshold: float = 0.95,
    ) -> bool:
        """Evaluate loop break condition under selected strategy."""
        if strategy == LoopStrategy.HARD_STOP:
            return True
        if strategy == LoopStrategy.MAX_ITERATIONS:
            return iteration >= max_iterations
        if strategy == LoopStrategy.SEMANTIC_CONVERGENCE:
            return state_delta is not None and state_delta <= (1.0 - convergence_threshold)
        return False

    def _dfs(
        self,
        node: str,
        adjacency: dict,
        visited: set,
        rec_stack: set,
        path: list,
    ) -> list[str] | None:
        """DFS helper returning a cycle path when a back-edge is found."""
        visited.add(node)
        rec_stack.add(node)
        path.append(node)

        for neighbor in adjacency.get(node, []):
            if neighbor not in visited:
                found = self._dfs(neighbor, adjacency, visited, rec_stack, path)
                if found:
                    return found
            elif neighbor in rec_stack:
                idx = path.index(neighbor)
                return path[idx:] + [neighbor]

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