"""
Dependency tracking models for prompt artifact graphs.

Implements FR-6: Dependency Tracking
- FR-6.1: Directed dependency edges between artifacts
- FR-6.2: Cross-space dependency graph
- FR-6.3: Circular dependency detection
"""

from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Dict, Any, List, Optional, Set, Tuple
import uuid


class EdgeType(Enum):
    """Type of dependency relationship between artifacts."""

    REQUIRES = "requires"
    GENERATES = "generates"
    INCLUDES = "includes"


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

    Attributes:
        cycle: List of node IDs forming the cycle
    """

    def __init__(self, message: str, cycle: Optional[List[str]] = None):
        super().__init__(message)
        self.cycle = cycle or []


@dataclass
class DependencyEdge:
    """
    Persistent dependency edge between artifacts.

    Extends the ephemeral DependencyEdge from execution/manifest.py
    with persistence fields (id, run_id, created_at).

    Attributes:
        id: Unique edge identifier
        source_artifact_id: Source artifact ID
        target_artifact_id: Target artifact ID
        run_id: ID of the run that created this edge
        edge_type: Type of dependency relationship
        created_at: When this edge was created
    """

    id: str
    source_artifact_id: str
    target_artifact_id: str
    run_id: str
    edge_type: EdgeType
    created_at: datetime = field(default_factory=datetime.utcnow)

    @classmethod
    def create(
        cls,
        source_artifact_id: str,
        target_artifact_id: str,
        run_id: str,
        edge_type: EdgeType,
    ) -> "DependencyEdge":
        """
        Create a new dependency edge.

        Args:
            source_artifact_id: Source artifact ID
            target_artifact_id: Target artifact ID
            run_id: Run that established this dependency
            edge_type: Type of dependency

        Returns:
            New DependencyEdge instance
        """
        return cls(
            id=str(uuid.uuid4()),
            source_artifact_id=source_artifact_id,
            target_artifact_id=target_artifact_id,
            run_id=run_id,
            edge_type=edge_type,
        )

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "id": self.id,
            "source_artifact_id": self.source_artifact_id,
            "target_artifact_id": self.target_artifact_id,
            "run_id": self.run_id,
            "edge_type": self.edge_type.value,
            "created_at": self.created_at.isoformat(),
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "DependencyEdge":
        """Create from dictionary."""
        return cls(
            id=data["id"],
            source_artifact_id=data["source_artifact_id"],
            target_artifact_id=data["target_artifact_id"],
            run_id=data["run_id"],
            edge_type=EdgeType(data["edge_type"]),
            created_at=datetime.fromisoformat(data["created_at"]),
        )


# DFS node coloring for cycle detection
_WHITE = 0  # Unvisited
_GRAY = 1   # In current DFS path
_BLACK = 2  # Fully processed


class DependencyGraph:
    """
    In-memory directed graph for dependency analysis.

    Maintains forward and reverse adjacency lists for efficient
    traversal in both directions. Supports cycle detection via
    3-color DFS and topological sorting.

    Implements FR-6.2: Cross-space dependency graph
    Implements FR-6.3: Circular dependency detection
    """

    def __init__(self) -> None:
        self._forward: Dict[str, Set[str]] = {}
        self._reverse: Dict[str, Set[str]] = {}
        self._edge_types: Dict[Tuple[str, str], EdgeType] = {}

    @property
    def nodes(self) -> Set[str]:
        """All nodes in the graph."""
        return set(self._forward.keys()) | set(self._reverse.keys())

    @property
    def edge_count(self) -> int:
        """Total number of edges."""
        return sum(len(targets) for targets in self._forward.values())

    def add_edge(
        self,
        source: str,
        target: str,
        edge_type: EdgeType = EdgeType.REQUIRES,
    ) -> None:
        """
        Add a directed edge from source to target.

        Args:
            source: Source node ID
            target: Target node ID
            edge_type: Type of dependency
        """
        if source not in self._forward:
            self._forward[source] = set()
        if target not in self._forward:
            self._forward[target] = set()
        if source not in self._reverse:
            self._reverse[source] = set()
        if target not in self._reverse:
            self._reverse[target] = set()

        self._forward[source].add(target)
        self._reverse[target].add(source)
        self._edge_types[(source, target)] = edge_type

    def get_successors(self, node: str) -> Set[str]:
        """Get direct successors (dependencies) of a node."""
        return set(self._forward.get(node, set()))

    def get_predecessors(self, node: str) -> Set[str]:
        """Get direct predecessors (dependents) of a node."""
        return set(self._reverse.get(node, set()))

    def get_edge_type(self, source: str, target: str) -> Optional[EdgeType]:
        """Get the edge type between two nodes."""
        return self._edge_types.get((source, target))

    def has_edge(self, source: str, target: str) -> bool:
        """Check if an edge exists between source and target."""
        return target in self._forward.get(source, set())

    def detect_cycles(self) -> List[List[str]]:
        """
        Detect all cycles in the graph using 3-color DFS.

        Returns:
            List of cycles, where each cycle is a list of node IDs
        """
        color: Dict[str, int] = {node: _WHITE for node in self.nodes}
        parent: Dict[str, Optional[str]] = {node: None for node in self.nodes}
        cycles: List[List[str]] = []

        def _dfs(node: str) -> None:
            color[node] = _GRAY
            for neighbor in self._forward.get(node, set()):
                if color.get(neighbor, _WHITE) == _GRAY:
                    # Back edge found - extract cycle
                    cycle = [neighbor]
                    current = node
                    while current != neighbor:
                        cycle.append(current)
                        current = parent[current]
                    cycle.append(neighbor)
                    cycle.reverse()
                    cycles.append(cycle)
                elif color.get(neighbor, _WHITE) == _WHITE:
                    parent[neighbor] = node
                    _dfs(neighbor)
            color[node] = _BLACK

        for node in self.nodes:
            if color.get(node, _WHITE) == _WHITE:
                _dfs(node)

        return cycles

    def has_cycle(self) -> bool:
        """Check if the graph contains any cycles."""
        color: Dict[str, int] = {node: _WHITE for node in self.nodes}

        def _dfs(node: str) -> bool:
            color[node] = _GRAY
            for neighbor in self._forward.get(node, set()):
                if color.get(neighbor, _WHITE) == _GRAY:
                    return True
                if color.get(neighbor, _WHITE) == _WHITE and _dfs(neighbor):
                    return True
            color[node] = _BLACK
            return False

        for node in self.nodes:
            if color.get(node, _WHITE) == _WHITE:
                if _dfs(node):
                    return True
        return False

    def topological_sort(self) -> List[str]:
        """
        Topologically sort the graph nodes.

        Returns:
            Nodes in topological order (dependencies before dependents)

        Raises:
            CircularDependencyError: If graph contains cycles
        """
        color: Dict[str, int] = {node: _WHITE for node in self.nodes}
        result: List[str] = []
        cycle_path: List[str] = []

        def _dfs(node: str) -> bool:
            color[node] = _GRAY
            cycle_path.append(node)
            for neighbor in sorted(self._forward.get(node, set())):
                if color.get(neighbor, _WHITE) == _GRAY:
                    # Extract cycle for error reporting
                    idx = cycle_path.index(neighbor)
                    cycle = cycle_path[idx:] + [neighbor]
                    raise CircularDependencyError(
                        f"Circular dependency detected: {' -> '.join(cycle)}",
                        cycle=cycle,
                    )
                if color.get(neighbor, _WHITE) == _WHITE:
                    _dfs(neighbor)
            cycle_path.pop()
            color[node] = _BLACK
            result.append(node)

        for node in sorted(self.nodes):
            if color.get(node, _WHITE) == _WHITE:
                _dfs(node)

        result.reverse()
        return result

    def get_subgraph(self, node_ids: Set[str]) -> "DependencyGraph":
        """
        Extract a subgraph containing only the specified nodes.

        Args:
            node_ids: Set of node IDs to include

        Returns:
            New DependencyGraph with only the specified nodes and their edges
        """
        subgraph = DependencyGraph()
        for source in node_ids:
            for target in self._forward.get(source, set()):
                if target in node_ids:
                    edge_type = self._edge_types.get(
                        (source, target), EdgeType.REQUIRES
                    )
                    subgraph.add_edge(source, target, edge_type)
        # Ensure isolated nodes are present
        for node_id in node_ids:
            if node_id not in subgraph._forward:
                subgraph._forward[node_id] = set()
            if node_id not in subgraph._reverse:
                subgraph._reverse[node_id] = set()
        return subgraph
