"""
R-Tree Edge Strategy Implementation
This module implements the R_TREE strategy for spatial indexing of edges
with geometric coordinates and efficient spatial queries.
"""

from __future__ import annotations
from typing import Any, NamedTuple
from collections import defaultdict
import math
from ._base_edge import AEdgeStrategy
from ...defs import EdgeMode, EdgeTrait
from collections.abc import Iterator


class Rectangle(NamedTuple):
    """Represents a bounding rectangle."""
    min_x: float
    min_y: float
    max_x: float
    max_y: float

    def area(self) -> float:
        """Calculate rectangle area."""
        return max(0, self.max_x - self.min_x) * max(0, self.max_y - self.min_y)

    def perimeter(self) -> float:
        """Calculate rectangle perimeter."""
        return 2 * (max(0, self.max_x - self.min_x) + max(0, self.max_y - self.min_y))

    def contains_point(self, x: float, y: float) -> bool:
        """Check if rectangle contains point."""
        return self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y

    def intersects(self, other: Rectangle) -> bool:
        """Check if rectangle intersects with another."""
        return not (other.max_x < self.min_x or other.min_x > self.max_x or
                   other.max_y < self.min_y or other.min_y > self.max_y)

    def contains_rectangle(self, other: Rectangle) -> bool:
        """Check if this rectangle contains another."""
        return (self.min_x <= other.min_x and self.max_x >= other.max_x and
                self.min_y <= other.min_y and self.max_y >= other.max_y)

    def union(self, other: Rectangle) -> Rectangle:
        """Get bounding rectangle of union."""
        return Rectangle(
            min(self.min_x, other.min_x),
            min(self.min_y, other.min_y),
            max(self.max_x, other.max_x),
            max(self.max_y, other.max_y)
        )

    def enlargement_needed(self, other: Rectangle) -> float:
        """Calculate area enlargement needed to include another rectangle."""
        union_rect = self.union(other)
        return union_rect.area() - self.area()


class SpatialEdge:
    """Represents an edge with spatial coordinates."""

    def __init__(self, edge_id: str, source: str, target: str, 
                 source_coords: tuple[float, float], 
                 target_coords: tuple[float, float], **properties):
        self.edge_id = edge_id
        self.source = source
        self.target = target
        self.source_coords = source_coords
        self.target_coords = target_coords
        self.properties = properties.copy()
        # Calculate bounding rectangle
        self.bounding_rect = Rectangle(
            min(source_coords[0], target_coords[0]),
            min(source_coords[1], target_coords[1]),
            max(source_coords[0], target_coords[0]),
            max(source_coords[1], target_coords[1])
        )
        # Calculate edge length
        dx = target_coords[0] - source_coords[0]
        dy = target_coords[1] - source_coords[1]
        self.length = math.sqrt(dx * dx + dy * dy)

    def intersects_rectangle(self, rect: Rectangle) -> bool:
        """Check if edge intersects with rectangle."""
        # First check if bounding rectangles intersect
        if not self.bounding_rect.intersects(rect):
            return False
        # Check if either endpoint is inside rectangle
        if (rect.contains_point(*self.source_coords) or 
            rect.contains_point(*self.target_coords)):
            return True
        # Check line-rectangle intersection (simplified)
        return self._line_intersects_rectangle(rect)

    def _line_intersects_rectangle(self, rect: Rectangle) -> bool:
        """Check if line segment intersects rectangle edges."""
        x1, y1 = self.source_coords
        x2, y2 = self.target_coords
        # Check intersection with each rectangle edge
        edges = [
            ((rect.min_x, rect.min_y), (rect.max_x, rect.min_y)),  # Bottom
            ((rect.max_x, rect.min_y), (rect.max_x, rect.max_y)),  # Right
            ((rect.max_x, rect.max_y), (rect.min_x, rect.max_y)),  # Top
            ((rect.min_x, rect.max_y), (rect.min_x, rect.min_y))   # Left
        ]
        for (ex1, ey1), (ex2, ey2) in edges:
            if self._segments_intersect((x1, y1), (x2, y2), (ex1, ey1), (ex2, ey2)):
                return True
        return False

    def _segments_intersect(self, p1: tuple[float, float], p2: tuple[float, float],
                           p3: tuple[float, float], p4: tuple[float, float]) -> bool:
        """Check if two line segments intersect."""
        def ccw(A, B, C):
            return (C[1] - A[1]) * (B[0] - A[0]) > (B[1] - A[1]) * (C[0] - A[0])
        return ccw(p1, p3, p4) != ccw(p2, p3, p4) and ccw(p1, p2, p3) != ccw(p1, p2, p4)

    def distance_to_point(self, x: float, y: float) -> float:
        """Calculate distance from point to edge."""
        x1, y1 = self.source_coords
        x2, y2 = self.target_coords
        # Vector from source to target
        dx = x2 - x1
        dy = y2 - y1
        if dx == 0 and dy == 0:
            # Degenerate case: source and target are same point
            return math.sqrt((x - x1) ** 2 + (y - y1) ** 2)
        # Parameter t for projection onto line
        t = max(0, min(1, ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy)))
        # Closest point on line segment
        closest_x = x1 + t * dx
        closest_y = y1 + t * dy
        return math.sqrt((x - closest_x) ** 2 + (y - closest_y) ** 2)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary representation."""
        return {
            'id': self.edge_id,
            'source': self.source,
            'target': self.target,
            'source_coords': self.source_coords,
            'target_coords': self.target_coords,
            'length': self.length,
            'bounding_rect': {
                'min_x': self.bounding_rect.min_x,
                'min_y': self.bounding_rect.min_y,
                'max_x': self.bounding_rect.max_x,
                'max_y': self.bounding_rect.max_y
            },
            'properties': self.properties
        }


class RTreeNode:
    """Node in the R-Tree structure."""

    def __init__(self, is_leaf: bool = False, max_entries: int = 4):
        self.is_leaf = is_leaf
        self.max_entries = max_entries
        self.entries: list[tuple[Rectangle, Any]] = []  # (bounding_rect, child_or_edge)
        self.bounding_rect: Rectangle | None = None

    def is_full(self) -> bool:
        """Check if node is full."""
        return len(self.entries) >= self.max_entries

    def update_bounding_rect(self) -> None:
        """Update bounding rectangle to encompass all entries."""
        if not self.entries:
            self.bounding_rect = None
            return
        min_x = min(rect.min_x for rect, _ in self.entries)
        min_y = min(rect.min_y for rect, _ in self.entries)
        max_x = max(rect.max_x for rect, _ in self.entries)
        max_y = max(rect.max_y for rect, _ in self.entries)
        self.bounding_rect = Rectangle(min_x, min_y, max_x, max_y)

    def add_entry(self, rect: Rectangle, data: Any) -> None:
        """Add entry to node."""
        self.entries.append((rect, data))
        self.update_bounding_rect()


class RTreeStrategy(AEdgeStrategy):
    """
    R-Tree edge strategy for spatial indexing of edges.
    WHY this strategy:
    - Geographic networks require spatial queries (find roads near point, route planning)
    - R-Tree provides O(log N) spatial lookups vs O(N) brute force
    - MBR efficiently bounds edge geometry for pruning search space
    - Industry-standard spatial index (PostGIS, MongoDB, Oracle Spatial)
    WHY this implementation:
    - SpatialEdge class stores coordinates and computed bounding rectangle
    - Hierarchical R-Tree nodes with MBR-based partitioning
    - Range queries use rectangle intersection for efficient filtering
    - Point queries calculate distance from point to edge segment
    Time Complexity:
    - Add Edge: O(log N) - tree descent and insertion
    - Range Query: O(log N + K) where K = result count
    - Point Query: O(log N) - spatial search
    Space Complexity: O(N * log N) - tree structure for N edges
    Trade-offs:
    - Advantage: Fast spatial queries, handles real-world geography
    - Limitation: Slower exact lookups than hash table
    - Compared to QUADTREE: Better for clustered/non-uniform data
    Best for:
    - Road networks (GPS routing, proximity search)
    - Utility infrastructure (power lines, pipelines)
    - Delivery logistics (route optimization, service areas)
    - GIS applications (spatial joins, nearest facility)
    Not recommended for:
    - Non-spatial graphs - indexing overhead wasted
    - Uniform grid data - QUADTREE more appropriate
    - 1D spatial data - use interval tree
    Following eXonware Priorities:
    1. Security: Bounds validation prevents coordinate injection
    2. Usability: Standard GIS query API (range, point)
    3. Maintainability: Well-documented R-Tree algorithm
    4. Performance: O(log N) vs O(N) for spatial lookups
    5. Extensibility: Supports R*-Tree variants, bulk loading
    """

    def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
        """Initialize the R-Tree strategy."""
        super().__init__(EdgeMode.R_TREE, traits, **options)
        self.max_entries = options.get('max_entries', 4)
        self.min_entries = max(1, self.max_entries // 2)
        self.is_directed = options.get('directed', True)
        # Core storage
        self._edges: dict[str, SpatialEdge] = {}
        self._vertex_coords: dict[str, tuple[float, float]] = {}
        self._vertices: set[str] = set()
        # R-Tree structure
        self._root: RTreeNode | None = None
        self._edge_count = 0
        self._edge_id_counter = 0
        # Statistics
        self._tree_height = 0
        self._total_nodes = 0

    def get_supported_traits(self) -> EdgeTrait:
        """Get the traits supported by the R-tree strategy."""
        return (EdgeTrait.SPATIAL | EdgeTrait.DIRECTED | EdgeTrait.WEIGHTED | EdgeTrait.SPARSE)

    def _generate_edge_id(self) -> str:
        """Generate unique edge ID."""
        self._edge_id_counter += 1
        return f"spatial_edge_{self._edge_id_counter}"

    def _choose_leaf(self, rect: Rectangle) -> RTreeNode:
        """Choose leaf node for insertion using minimum enlargement heuristic."""
        if self._root is None:
            self._root = RTreeNode(is_leaf=True, max_entries=self.max_entries)
            return self._root
        current = self._root
        while not current.is_leaf:
            best_child = None
            best_enlargement = float('inf')
            best_area = float('inf')
            for child_rect, child_node in current.entries:
                enlargement = child_rect.enlargement_needed(rect)
                area = child_rect.area()
                if (enlargement < best_enlargement or 
                    (enlargement == best_enlargement and area < best_area)):
                    best_enlargement = enlargement
                    best_area = area
                    best_child = child_node
            current = best_child
        return current

    def _split_node(self, node: RTreeNode) -> tuple[RTreeNode, RTreeNode]:
        """Split overfull node using quadratic split algorithm."""
        # Find two entries with maximum waste of space
        max_waste = -1
        seed1_idx = seed2_idx = 0
        for i in range(len(node.entries)):
            for j in range(i + 1, len(node.entries)):
                rect1, _ = node.entries[i]
                rect2, _ = node.entries[j]
                union_area = rect1.union(rect2).area()
                waste = union_area - rect1.area() - rect2.area()
                if waste > max_waste:
                    max_waste = waste
                    seed1_idx = i
                    seed2_idx = j
        # Create two new nodes
        node1 = RTreeNode(is_leaf=node.is_leaf, max_entries=self.max_entries)
        node2 = RTreeNode(is_leaf=node.is_leaf, max_entries=self.max_entries)
        # Add seeds
        node1.add_entry(*node.entries[seed1_idx])
        node2.add_entry(*node.entries[seed2_idx])
        # Distribute remaining entries
        remaining_entries = [node.entries[i] for i in range(len(node.entries)) 
                           if i != seed1_idx and i != seed2_idx]
        for rect, data in remaining_entries:
            enlargement1 = node1.bounding_rect.enlargement_needed(rect)
            enlargement2 = node2.bounding_rect.enlargement_needed(rect)
            if enlargement1 < enlargement2:
                node1.add_entry(rect, data)
            elif enlargement2 < enlargement1:
                node2.add_entry(rect, data)
            else:
                # Equal enlargement, choose smaller area
                if node1.bounding_rect.area() <= node2.bounding_rect.area():
                    node1.add_entry(rect, data)
                else:
                    node2.add_entry(rect, data)
        return node1, node2

    def _insert_with_split(self, edge: SpatialEdge) -> None:
        """Insert edge with node splitting and upward propagation."""
        # Build path from root to leaf for split propagation
        path = []
        if self._root is None:
            self._root = RTreeNode(is_leaf=True, max_entries=self.max_entries)
        current = self._root
        while not current.is_leaf:
            path.append(current)
            best_child = None
            best_enlargement = float('inf')
            best_area = float('inf')
            for child_rect, child_node in current.entries:
                enlargement = child_rect.enlargement_needed(edge.bounding_rect)
                area = child_rect.area()
                if (enlargement < best_enlargement or
                    (enlargement == best_enlargement and area < best_area)):
                    best_enlargement = enlargement
                    best_area = area
                    best_child = child_node
            current = best_child
        # Insert into leaf
        current.add_entry(edge.bounding_rect, edge)
        # Handle overflow - propagate splits up the tree
        node = current
        while len(node.entries) > self.max_entries:
            node1, node2 = self._split_node(node)
            if node is self._root:
                new_root = RTreeNode(is_leaf=False, max_entries=self.max_entries)
                new_root.add_entry(node1.bounding_rect, node1)
                new_root.add_entry(node2.bounding_rect, node2)
                self._root = new_root
                self._tree_height += 1
                break
            else:
                parent = path.pop() if path else None
                if parent is None:
                    break
                # Remove old entry and add two new ones
                parent.entries = [(r, c) for r, c in parent.entries if c is not node]
                parent.add_entry(node1.bounding_rect, node1)
                parent.add_entry(node2.bounding_rect, node2)
                node = parent
    # ============================================================================
    # CORE EDGE OPERATIONS
    # ============================================================================

    def add_edge(self, source: str, target: str, **properties) -> str:
        """Add spatial edge with coordinates."""
        # Extract coordinates from properties
        source_coords = properties.pop('source_coords', None)
        target_coords = properties.pop('target_coords', None)
        if source_coords is None and source in self._vertex_coords:
            source_coords = self._vertex_coords[source]
        if target_coords is None and target in self._vertex_coords:
            target_coords = self._vertex_coords[target]
        if source_coords is None or target_coords is None:
            raise ValueError("Both source_coords and target_coords must be provided")
        # Generate edge ID
        edge_id = properties.pop('edge_id', self._generate_edge_id())
        # Create spatial edge
        spatial_edge = SpatialEdge(edge_id, source, target, source_coords, target_coords, **properties)
        # Store edge and update vertices
        self._edges[edge_id] = spatial_edge
        self._vertex_coords[source] = source_coords
        self._vertex_coords[target] = target_coords
        self._vertices.add(source)
        self._vertices.add(target)
        # Insert into R-Tree
        self._insert_with_split(spatial_edge)
        self._edge_count += 1
        return edge_id

    def remove_edge(self, source: str, target: str, edge_id: str | None = None) -> bool:
        """Remove spatial edge with proper R-Tree cleanup."""
        edge_to_remove = None
        edge_id_to_remove = None
        # Find the edge to remove
        if edge_id and edge_id in self._edges:
            edge = self._edges[edge_id]
            if edge.source == source and edge.target == target:
                edge_to_remove = edge
                edge_id_to_remove = edge_id
        else:
            # Find edge by endpoints
            for eid, edge in self._edges.items():
                if edge.source == source and edge.target == target:
                    edge_to_remove = edge
                    edge_id_to_remove = eid
                    break
        if edge_to_remove is None:
            return False
        # Remove from edge storage
        del self._edges[edge_id_to_remove]
        self._edge_count -= 1
        # Remove from R-Tree structure
        self._remove_from_rtree(edge_to_remove)
        return True

    def _remove_from_rtree(self, edge: SpatialEdge) -> bool:
        """Remove edge from R-Tree structure with proper cleanup."""
        if self._root is None:
            return False
        # Find the leaf node containing this edge
        leaf_node = self._find_leaf_containing_edge(self._root, edge)
        if leaf_node is None:
            return False
        # Remove edge from leaf node
        edge_removed = False
        for i, (rect, data) in enumerate(leaf_node.entries):
            if isinstance(data, SpatialEdge) and data.edge_id == edge.edge_id:
                leaf_node.entries.pop(i)
                edge_removed = True
                break
        if not edge_removed:
            return False
        # Update leaf node's bounding rectangle
        leaf_node.update_bounding_rect()
        # Handle underflow in leaf node
        if len(leaf_node.entries) < self.min_entries and leaf_node != self._root:
            self._handle_underflow(leaf_node)
        else:
            # Propagate bounding rectangle changes up the tree
            self._propagate_changes_up(leaf_node)
        # Rebuild tree if root becomes empty
        if self._root and len(self._root.entries) == 0:
            self._rebuild_tree()
        return True

    def _find_leaf_containing_edge(self, node: RTreeNode, edge: SpatialEdge) -> RTreeNode | None:
        """Find the leaf node containing the specified edge."""
        if node.is_leaf:
            # Check if this leaf contains the edge
            for rect, data in node.entries:
                if isinstance(data, SpatialEdge) and data.edge_id == edge.edge_id:
                    return node
            return None
        # Search in child nodes
        for rect, child_node in node.entries:
            if rect.intersects(edge.bounding_rect):
                result = self._find_leaf_containing_edge(child_node, edge)
                if result is not None:
                    return result
        return None

    def _handle_underflow(self, node: RTreeNode) -> None:
        """Handle underflow in a node by redistributing or merging entries."""
        # Find sibling nodes
        parent = self._find_parent(self._root, node)
        if parent is None:
            return
        siblings = []
        for rect, child in parent.entries:
            if child != node:
                siblings.append(child)
        # Try to redistribute entries from siblings
        for sibling in siblings:
            if len(sibling.entries) > self.min_entries:
                # Redistribute one entry from sibling
                entry_to_move = sibling.entries.pop()
                node.entries.append(entry_to_move)
                # Update bounding rectangles
                node.update_bounding_rect()
                sibling.update_bounding_rect()
                self._propagate_changes_up(parent)
                return
        # If redistribution fails, merge with a sibling
        if siblings:
            sibling = siblings[0]
            # Move all entries from node to sibling
            sibling.entries.extend(node.entries)
            sibling.update_bounding_rect()
            # Remove node from parent
            for i, (rect, child) in enumerate(parent.entries):
                if child == node:
                    parent.entries.pop(i)
                    break
            parent.update_bounding_rect()
            self._propagate_changes_up(parent)

    def _find_parent(self, current: RTreeNode, target: RTreeNode) -> RTreeNode | None:
        """Find the parent of a target node."""
        if current.is_leaf:
            return None
        for rect, child in current.entries:
            if child == target:
                return current
            result = self._find_parent(child, target)
            if result is not None:
                return result
        return None

    def _propagate_changes_up(self, node: RTreeNode) -> None:
        """Propagate bounding rectangle changes up the tree."""
        parent = self._find_parent(self._root, node)
        if parent is not None:
            parent.update_bounding_rect()
            self._propagate_changes_up(parent)

    def _rebuild_tree(self) -> None:
        """Rebuild the R-Tree from existing edges."""
        if not self._edges:
            self._root = None
            self._tree_height = 0
            self._total_nodes = 0
            return
        # Collect all edges
        edges = list(self._edges.values())
        # Clear current tree
        self._root = None
        self._tree_height = 0
        self._total_nodes = 0
        # Rebuild tree by re-inserting all edges
        for edge in edges:
            self._insert_edge_into_tree(edge)

    def _insert_edge_into_tree(self, edge: SpatialEdge) -> None:
        """Insert edge into R-Tree structure."""
        if self._root is None:
            self._root = RTreeNode(is_leaf=True, max_entries=self.max_entries)
            self._tree_height = 1
            self._total_nodes = 1
        # Choose leaf for insertion
        leaf = self._choose_leaf(edge.bounding_rect)
        # Add edge to leaf
        leaf.add_entry(edge.bounding_rect, edge)
        # Handle overflow
        if leaf.is_full():
            self._handle_overflow(leaf)

    def _handle_overflow(self, node: RTreeNode) -> None:
        """Handle overflow in a node by splitting."""
        if not node.is_full():
            return
        # Split the node
        node1, node2 = self._split_node(node)
        # If this is the root, create a new root
        if node == self._root:
            new_root = RTreeNode(is_leaf=False, max_entries=self.max_entries)
            new_root.add_entry(node1.bounding_rect, node1)
            new_root.add_entry(node2.bounding_rect, node2)
            self._root = new_root
            self._tree_height += 1
            self._total_nodes += 1
        else:
            # Replace the original node with the first split node
            parent = self._find_parent(self._root, node)
            if parent is not None:
                # Find and replace the original node
                for i, (rect, child) in enumerate(parent.entries):
                    if child == node:
                        parent.entries[i] = (node1.bounding_rect, node1)
                        break
                # Add the second split node to parent
                parent.add_entry(node2.bounding_rect, node2)
                parent.update_bounding_rect()
                # Handle overflow in parent if necessary
                if parent.is_full():
                    self._handle_overflow(parent)

    def has_edge(self, source: str, target: str) -> bool:
        """Check if edge exists."""
        for edge in self._edges.values():
            if edge.source == source and edge.target == target:
                return True
        return False

    def get_edge_data(self, source: str, target: str) -> dict[str, Any] | None:
        """Get edge data."""
        for edge in self._edges.values():
            if edge.source == source and edge.target == target:
                return edge.to_dict()
        return None

    def neighbors(self, vertex: str, direction: str = 'out') -> Iterator[str]:
        """Get neighbors of vertex."""
        neighbors = set()
        for edge in self._edges.values():
            if direction in ['out', 'both'] and edge.source == vertex:
                neighbors.add(edge.target)
            if direction in ['in', 'both'] and edge.target == vertex:
                neighbors.add(edge.source)
        return iter(neighbors)

    def degree(self, vertex: str, direction: str = 'out') -> int:
        """Get degree of vertex."""
        return len(list(self.neighbors(vertex, direction)))

    def edges(self, data: bool = False) -> Iterator[tuple]:
        """Get all edges."""
        for edge in self._edges.values():
            if data:
                yield (edge.source, edge.target, edge.to_dict())
            else:
                yield (edge.source, edge.target)

    def vertices(self) -> Iterator[str]:
        """Get all vertices."""
        return iter(self._vertices)

    def __len__(self) -> int:
        """Get number of edges."""
        return self._edge_count

    def vertex_count(self) -> int:
        """Get number of vertices."""
        return len(self._vertices)

    def clear(self) -> None:
        """Clear all data."""
        self._edges.clear()
        self._vertex_coords.clear()
        self._vertices.clear()
        self._root = None
        self._edge_count = 0
        self._edge_id_counter = 0
        self._tree_height = 0

    def add_vertex(self, vertex: str, coords: tuple[float, float] = None) -> None:
        """Add vertex with coordinates."""
        self._vertices.add(vertex)
        if coords:
            self._vertex_coords[vertex] = coords

    def remove_vertex(self, vertex: str) -> bool:
        """Remove vertex and all its edges."""
        if vertex not in self._vertices:
            return False
        # Remove all edges involving this vertex
        edges_to_remove = [eid for eid, edge in self._edges.items() 
                          if edge.source == vertex or edge.target == vertex]
        for edge_id in edges_to_remove:
            edge = self._edges[edge_id]
            self.remove_edge(edge.source, edge.target, edge_id)
        self._vertices.discard(vertex)
        self._vertex_coords.pop(vertex, None)
        return True
    # ============================================================================
    # SPATIAL QUERY OPERATIONS
    # ============================================================================

    def range_query(self, min_x: float, min_y: float, max_x: float, max_y: float) -> list[SpatialEdge]:
        """Find all edges intersecting with rectangle."""
        query_rect = Rectangle(min_x, min_y, max_x, max_y)
        result = []
        def search_node(node: RTreeNode):
            if node is None:
                return
            for rect, data in node.entries:
                if query_rect.intersects(rect):
                    if node.is_leaf:
                        # data is SpatialEdge
                        if data.intersects_rectangle(query_rect):
                            result.append(data)
                    else:
                        # data is child node
                        search_node(data)
        search_node(self._root)
        return result

    def point_query(self, x: float, y: float, radius: float = 0.0) -> list[SpatialEdge]:
        """Find edges near a point within radius."""
        if radius == 0.0:
            # Exact point query
            query_rect = Rectangle(x, y, x, y)
        else:
            # Range query with radius
            query_rect = Rectangle(x - radius, y - radius, x + radius, y + radius)
        candidates = self.range_query(query_rect.min_x, query_rect.min_y, 
                                    query_rect.max_x, query_rect.max_y)
        if radius == 0.0:
            return candidates
        # Filter by actual distance
        result = []
        for edge in candidates:
            if edge.distance_to_point(x, y) <= radius:
                result.append(edge)
        return result

    def nearest_neighbor(self, x: float, y: float, k: int = 1) -> list[tuple[SpatialEdge, float]]:
        """Find k nearest edges to point."""
        # Simple implementation - can be optimized with priority queue
        distances = []
        for edge in self._edges.values():
            distance = edge.distance_to_point(x, y)
            distances.append((edge, distance))
        distances.sort(key=lambda x: x[1])
        return distances[:k]

    def edges_in_region(self, center_x: float, center_y: float, radius: float) -> list[SpatialEdge]:
        """Find all edges within circular region."""
        # Use square approximation for efficiency
        candidates = self.range_query(center_x - radius, center_y - radius,
                                    center_x + radius, center_y + radius)
        result = []
        for edge in candidates:
            # Check if any part of edge is within radius
            dist_to_source = math.sqrt((edge.source_coords[0] - center_x) ** 2 + 
                                     (edge.source_coords[1] - center_y) ** 2)
            dist_to_target = math.sqrt((edge.target_coords[0] - center_x) ** 2 + 
                                     (edge.target_coords[1] - center_y) ** 2)
            if dist_to_source <= radius or dist_to_target <= radius:
                result.append(edge)
            elif edge.distance_to_point(center_x, center_y) <= radius:
                result.append(edge)
        return result

    def get_bounding_box(self) -> Rectangle | None:
        """Get bounding box of all edges."""
        if self._root and self._root.bounding_rect:
            return self._root.bounding_rect
        return None

    def spatial_statistics(self) -> dict[str, Any]:
        """Get spatial statistics."""
        if not self._edges:
            return {'edges': 0, 'vertices': 0, 'total_length': 0, 'avg_length': 0}
        lengths = [edge.length for edge in self._edges.values()]
        bounding_box = self.get_bounding_box()
        return {
            'edges': len(self._edges),
            'vertices': len(self._vertices),
            'total_length': sum(lengths),
            'avg_length': sum(lengths) / len(lengths),
            'min_length': min(lengths),
            'max_length': max(lengths),
            'bounding_box': bounding_box._asdict() if bounding_box else None,
            'tree_height': self._tree_height,
            'spatial_extent': {
                'width': bounding_box.max_x - bounding_box.min_x if bounding_box else 0,
                'height': bounding_box.max_y - bounding_box.min_y if bounding_box else 0
            }
        }
    # ============================================================================
    # PERFORMANCE CHARACTERISTICS
    # ============================================================================
    @property

    def backend_info(self) -> dict[str, Any]:
        """Get backend implementation info."""
        return {
            'strategy': 'R_TREE',
            'backend': 'R-Tree spatial index with quadratic split',
            'max_entries': self.max_entries,
            'min_entries': self.min_entries,
            'is_directed': self.is_directed,
            'complexity': {
                'insert': f'O(log_M n)',  # M = max_entries
                'delete': f'O(log_M n)',
                'range_query': f'O(log_M n + k)',  # k = result size
                'point_query': f'O(log_M n + k)',
                'space': 'O(n)'
            }
        }
    @property

    def metrics(self) -> dict[str, Any]:
        """Get performance metrics."""
        stats = self.spatial_statistics()
        return {
            'edges': self._edge_count,
            'vertices': len(self._vertices),
            'tree_height': self._tree_height,
            'avg_edge_length': f"{stats.get('avg_length', 0):.2f}",
            'spatial_extent': f"{stats.get('spatial_extent', {}).get('width', 0):.1f} x {stats.get('spatial_extent', {}).get('height', 0):.1f}",
            'memory_usage': f"{self._edge_count * 150 + len(self._vertices) * 50} bytes (estimated)"
        }
