"""
Adjacency List Strategy Implementation

Implements graph operations using adjacency list representation.

Company: eXonware.com
Author: Eng. Muhammad AlShehri
Email: connect@exonware.com
Version: 0.0.1.16
Generation Date: 07-Sep-2025
"""

from typing import Any, Iterator, List, Optional, Dict, Union, Set, Tuple
from collections import defaultdict
from .base import ANodeGraphStrategy
from .contracts import NodeType
from ...types import NodeMode, NodeTrait


class AdjacencyListStrategy(ANodeGraphStrategy):
    """
    Adjacency List node strategy for graph operations.
    
    Uses adjacency list representation for efficient neighbor queries
    and edge oper
    
    # Strategy type classification
    STRATEGY_TYPE = NodeType.GRAPH
ations in sparse graphs.
    """
    
    def __init__(self):
        """Initialize an empty adjacency list."""
        super().__init__()
        self._adj_list: Dict[str, List[Tuple[str, float]]] = defaultdict(list)  # node -> [(neighbor, weight)]
        self._nodes: Dict[str, Any] = {}  # node -> data
        self._mode = NodeMode.ADJACENCY_LIST
        self._traits = {NodeTrait.GRAPH, NodeTrait.SPARSE, NodeTrait.FAST_NEIGHBORS}
    
    def insert(self, key: str, value: Any) -> None:
        """Insert a node into the graph."""
        self._nodes[key] = value
        if key not in self._adj_list:
            self._adj_list[key] = []
    
    def find(self, key: str) -> Optional[Any]:
        """Find a node in the graph."""
        return self._nodes.get(key)
    
    def delete(self, key: str) -> bool:
        """Delete a node and all its edges."""
        if key not in self._nodes:
            return False
        
        # Remove node
        del self._nodes[key]
        
        # Remove all edges to this node
        for node in self._adj_list:
            self._adj_list[node] = [(neighbor, weight) for neighbor, weight in self._adj_list[node] if neighbor != key]
        
        # Remove node's adjacency list
        if key in self._adj_list:
            del self._adj_list[key]
        
        return True
    
    def size(self) -> int:
        """Get the number of nodes in the graph."""
        return len(self._nodes)
    
    def to_native(self) -> Dict[str, Any]:
        """Convert graph to native dictionary format."""
        return {
            'nodes': self._nodes,
            'edges': {node: neighbors for node, neighbors in self._adj_list.items() if neighbors}
        }
    
    def from_native(self, data: Dict[str, Any]) -> None:
        """Load graph from native dictionary format."""
        self._nodes = data.get('nodes', {})
        edges = data.get('edges', {})
        
        self._adj_list.clear()
        for node, neighbors in edges.items():
            self._adj_list[node] = neighbors
    
    def add_edge(self, from_node: str, to_node: str, weight: float = 1.0) -> None:
        """Add an edge between two nodes."""
        if from_node not in self._nodes:
            self._nodes[from_node] = None
        if to_node not in self._nodes:
            self._nodes[to_node] = None
        
        # Add edge (avoid duplicates)
        neighbors = self._adj_list[from_node]
        for i, (neighbor, _) in enumerate(neighbors):
            if neighbor == to_node:
                neighbors[i] = (to_node, weight)
                return
        
        neighbors.append((to_node, weight))
    
    def remove_edge(self, from_node: str, to_node: str) -> bool:
        """Remove an edge between two nodes."""
        if from_node not in self._adj_list:
            return False
        
        neighbors = self._adj_list[from_node]
        for i, (neighbor, _) in enumerate(neighbors):
            if neighbor == to_node:
                neighbors.pop(i)
                return True
        return False
    
    def has_edge(self, from_node: str, to_node: str) -> bool:
        """Check if an edge exists between two nodes."""
        if from_node not in self._adj_list:
            return False
        
        for neighbor, _ in self._adj_list[from_node]:
            if neighbor == to_node:
                return True
        return False
    
    def get_edge_weight(self, from_node: str, to_node: str) -> Optional[float]:
        """Get the weight of an edge between two nodes."""
        if from_node not in self._adj_list:
            return None
        
        for neighbor, weight in self._adj_list[from_node]:
            if neighbor == to_node:
                return weight
        return None
    
    def get_neighbors(self, node: str) -> List[str]:
        """Get all neighbors of a node."""
        if node not in self._adj_list:
            return []
        return [neighbor for neighbor, _ in self._adj_list[node]]
    
    def get_neighbors_with_weights(self, node: str) -> List[Tuple[str, float]]:
        """Get all neighbors with their edge weights."""
        if node not in self._adj_list:
            return []
        return self._adj_list[node].copy()
    
    def get_in_degree(self, node: str) -> int:
        """Get the in-degree of a node."""
        count = 0
        for neighbors in self._adj_list.values():
            for neighbor, _ in neighbors:
                if neighbor == node:
                    count += 1
        return count
    
    def get_out_degree(self, node: str) -> int:
        """Get the out-degree of a node."""
        if node not in self._adj_list:
            return 0
        return len(self._adj_list[node])
    
    def get_degree(self, node: str) -> int:
        """Get the total degree of a node (in + out)."""
        return self.get_in_degree(node) + self.get_out_degree(node)
    
    def is_connected(self, from_node: str, to_node: str) -> bool:
        """Check if two nodes are connected (BFS)."""
        if from_node not in self._nodes or to_node not in self._nodes:
            return False
        
        if from_node == to_node:
            return True
        
        visited = set()
        queue = [from_node]
        
        while queue:
            current = queue.pop(0)
            if current == to_node:
                return True
            
            if current in visited:
                continue
            visited.add(current)
            
            for neighbor, _ in self._adj_list.get(current, []):
                if neighbor not in visited:
                    queue.append(neighbor)
        
        return False
    
    def get_connected_components(self) -> List[Set[str]]:
        """Get all connected components in the graph."""
        visited = set()
        components = []
        
        for node in self._nodes:
            if node not in visited:
                component = set()
                queue = [node]
                
                while queue:
                    current = queue.pop(0)
                    if current in visited:
                        continue
                    
                    visited.add(current)
                    component.add(current)
                    
                    # Add neighbors
                    for neighbor, _ in self._adj_list.get(current, []):
                        if neighbor not in visited:
                            queue.append(neighbor)
                
                if component:
                    components.append(component)
        
        return components
    
    def clear(self) -> None:
        """Clear all nodes and edges."""
        self._nodes.clear()
        self._adj_list.clear()
    
    def __iter__(self) -> Iterator[str]:
        """Iterate through all nodes."""
        for node in self._nodes:
            yield node
    
    def __repr__(self) -> str:
        """String representation of the adjacency list."""
        return f"AdjacencyListStrategy({len(self._nodes)} nodes, {sum(len(neighbors) for neighbors in self._adj_list.values())} edges)"
    
    # Required abstract methods from base classes
    def find_path(self, start: Any, end: Any) -> List[Any]:
        """Find path between nodes using BFS."""
        if start not in self._nodes or end not in self._nodes:
            return []
        
        if start == end:
            return [start]
        
        visited = set()
        queue = [(start, [start])]
        
        while queue:
            current, path = queue.pop(0)
            if current == end:
                return path
            
            if current in visited:
                continue
            visited.add(current)
            
            for neighbor, _ in self._adj_list.get(current, []):
                if neighbor not in visited:
                    queue.append((neighbor, path + [neighbor]))
        
        return []  # No path found
    
    def as_union_find(self):
        """Provide Union-Find behavioral view."""
        return self
    
    def as_neural_graph(self):
        """Provide Neural Graph behavioral view."""
        return self
    
    def as_flow_network(self):
        """Provide Flow Network behavioral view."""
        return self
