"""
Edge (connection) management between nodes in workflows.
"""

from typing import Optional
from pydantic import BaseModel


class Edge(BaseModel):
    """
    Represents a connection between two nodes in a workflow.
    """
    id: str
    source_node_id: str
    source_output: str = "output"  # Output port name
    target_node_id: str
    target_input: str = "input"  # Input port name
    condition: Optional[str] = None  # For conditional edges
    
    def __repr__(self) -> str:
        return (
            f"<Edge(id='{self.id}', "
            f"{self.source_node_id}.{self.source_output} -> "
            f"{self.target_node_id}.{self.target_input})>"
        )


class EdgeValidator:
    """
    Validates edge connections between nodes.
    """
    
    @staticmethod
    def validate(edge: Edge, nodes: dict) -> bool:
        """
        Validate that an edge connection is valid.
        
        Args:
            edge: Edge to validate
            nodes: Dictionary of nodes in the workflow
        
        Returns:
            True if edge is valid
        
        Raises:
            ValueError: If edge is invalid
        """
        # Check source node exists
        if edge.source_node_id not in nodes:
            raise ValueError(f"Source node '{edge.source_node_id}' not found")
        
        # Check target node exists
        if edge.target_node_id not in nodes:
            raise ValueError(f"Target node '{edge.target_node_id}' not found")
        
        # Prevent self-loops
        if edge.source_node_id == edge.target_node_id:
            raise ValueError("Self-loops are not allowed")
        
        source_node = nodes[edge.source_node_id]
        target_node = nodes[edge.target_node_id]
        
        # Check source has the output
        source_outputs = [out.name for out in source_node.metadata.outputs]
        if source_outputs and edge.source_output not in source_outputs:
            raise ValueError(
                f"Source node '{edge.source_node_id}' does not have output '{edge.source_output}'"
            )
        
        # Check target has the input
        target_inputs = [inp.name for inp in target_node.metadata.inputs]
        if target_inputs and edge.target_input not in target_inputs:
            raise ValueError(
                f"Target node '{edge.target_node_id}' does not have input '{edge.target_input}'"
            )
        
        return True
    
    @staticmethod
    def detect_cycles(edges: list, nodes: dict) -> bool:
        """
        Detect if there are cycles in the workflow graph.
        
        Args:
            edges: List of edges
            nodes: Dictionary of nodes
        
        Returns:
            True if cycles are detected
        """
        # Build adjacency list
        graph = {node_id: [] for node_id in nodes}
        for edge in edges:
            graph[edge.source_node_id].append(edge.target_node_id)
        
        # DFS to detect cycles
        visited = set()
        rec_stack = set()
        
        def has_cycle(node_id):
            visited.add(node_id)
            rec_stack.add(node_id)
            
            for neighbor in graph.get(node_id, []):
                if neighbor not in visited:
                    if has_cycle(neighbor):
                        return True
                elif neighbor in rec_stack:
                    return True
            
            rec_stack.remove(node_id)
            return False
        
        for node_id in nodes:
            if node_id not in visited:
                if has_cycle(node_id):
                    return True
        
        return False
