"""
#exonware/xwnode/src/exonware/xwnode/edges/strategies/bitemporal.py
Bitemporal Edges Strategy Implementation
This module implements the BITEMPORAL strategy for edges with both
valid-time and transaction-time dimensions for audit and time-travel queries.
Company: eXonware.com
Author: eXonware Backend Team
Email: connect@exonware.com
Version: 0.9.0.20
Generation Date: 12-Oct-2025
"""

import time
from typing import Any
from collections import defaultdict, deque
from ._base_edge import AEdgeStrategy
from ...defs import EdgeMode, EdgeTrait
from ...errors import XWNodeError, XWNodeValueError


from collections.abc import Iterator
class BitemporalEdge:
    """
    Edge with bitemporal properties.
    WHY two time dimensions:
    - Valid time: When fact is true in reality
    - Transaction time: When fact recorded in database
    - Enables audit trails and time-travel queries
    """

    def __init__(self, source: str, target: str,
                 valid_start: float, valid_end: float,
                 tx_start: float, tx_end: float | None = None,
                 properties: dict[str, Any] | None = None):
        """
        Initialize bitemporal edge.
        Args:
            source: Source vertex
            target: Target vertex
            valid_start: Valid time start
            valid_end: Valid time end
            tx_start: Transaction time start
            tx_end: Transaction time end (None = current)
            properties: Edge properties
        """
        self.source = source
        self.target = target
        self.valid_start = valid_start
        self.valid_end = valid_end
        self.tx_start = tx_start
        self.tx_end = tx_end  # None means still valid
        self.properties = properties or {}

    def is_valid_at(self, valid_time: float) -> bool:
        """Check if edge was valid at given time."""
        return self.valid_start <= valid_time <= self.valid_end

    def is_known_at(self, tx_time: float) -> bool:
        """Check if edge was known in database at given transaction time."""
        if tx_time < self.tx_start:
            return False
        if self.tx_end is not None and tx_time > self.tx_end:
            return False
        return True

    def is_active(self, valid_time: float, tx_time: float) -> bool:
        """Check if edge is active in both time dimensions."""
        return self.is_valid_at(valid_time) and self.is_known_at(tx_time)


class BitemporalStrategy(AEdgeStrategy):
    """
    Bitemporal edges strategy for audit and time-travel queries.
    WHY Bitemporal:
    - Track both real-world validity and database knowledge
    - Essential for financial systems, regulatory compliance
    - Enables "as-of" queries (state at past time)
    - Audit trail for all graph changes
    - Supports corrections to historical data
    WHY this implementation:
    - Valid-time: When relationship existed in reality
    - Transaction-time: When relationship recorded in DB
    - Interval-based indexing for temporal queries
    - Tombstone pattern for edge deletions
    - Separate current and historical edge storage
    Time Complexity:
    - Add edge: O(1)
    - Remove edge: O(1) (creates tombstone)
    - Temporal query: O(log n + k) where k is result size
    - As-of query: O(n) worst case, O(log n) with indexing
    - Current snapshot: O(edges)
    Space Complexity: O(total_versions × edges) worst case
    (All historical versions preserved)
    Trade-offs:
    - Advantage: Complete audit trail
    - Advantage: Time-travel queries
    - Advantage: Supports corrections
    - Limitation: High space overhead (all versions)
    - Limitation: Complex queries (two time dimensions)
    - Limitation: Slower than single-time temporal
    - Compared to TEMPORAL_EDGESET: More features, more complex
    - Compared to Event sourcing: Similar concept, graph-specific
    Best for:
    - Financial systems (regulatory audit requirements)
    - Healthcare records (legal compliance)
    - Blockchain/ledger applications
    - Regulatory compliance scenarios
    - Systems requiring complete audit trails
    - Historical data corrections
    Not recommended for:
    - Real-time systems (overhead too high)
    - Space-constrained environments
    - When simple timestamps suffice
    - Immutable graphs (no corrections needed)
    - Non-critical applications
    Following eXonware Priorities:
    1. Security: Immutable audit trail, prevents tampering
    2. Usability: Clear temporal query API
    3. Maintainability: Clean bitemporal model
    4. Performance: Indexed temporal queries
    5. Extensibility: Easy to add temporal constraints, versioning
    Industry Best Practices:
    - Follows Snodgrass bitemporal database theory
    - Implements valid-time and transaction-time
    - Provides as-of queries
    - Supports retroactive corrections
    - Compatible with temporal SQL extensions
    """

    def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
        """
        Initialize bitemporal strategy.
        Args:
            traits: Edge traits
            **options: Additional options
        """
        super().__init__(EdgeMode.BITEMPORAL, traits, **options)
        # All edge versions (including historical)
        self._edges: list[BitemporalEdge] = []
        # Current valid edges (cached)
        self._current_edges: set[tuple[str, str]] = set()
        # Vertices
        self._vertices: set[str] = set()
        # Temporal index (for efficient queries)
        self._valid_time_index: dict[float, list[BitemporalEdge]] = defaultdict(list)
        self._tx_time_index: dict[float, list[BitemporalEdge]] = defaultdict(list)

    def get_supported_traits(self) -> EdgeTrait:
        """Get supported traits."""
        return EdgeTrait.TEMPORAL | EdgeTrait.DIRECTED | EdgeTrait.SPARSE
    # ============================================================================
    # TEMPORAL EDGE OPERATIONS
    # ============================================================================

    def add_edge(self, source: str, target: str, edge_type: str = "default",
                 weight: float = 1.0, properties: dict[str, Any] | None = None,
                 is_bidirectional: bool = False, edge_id: str | None = None) -> str:
        """
        Add edge with temporal metadata.
        Args:
            source: Source vertex
            target: Target vertex
            edge_type: Edge type
            weight: Edge weight
            properties: Edge properties (should include valid_start, valid_end)
            is_bidirectional: Bidirectional flag
            edge_id: Edge ID
        Returns:
            Edge ID
        """
        # Parse temporal properties
        props = properties.copy() if properties else {}
        current_time = time.time()
        valid_start = props.pop('valid_start', current_time)
        valid_end = props.pop('valid_end', float('inf'))
        tx_start = current_time
        # Create bitemporal edge
        edge = BitemporalEdge(
            source, target,
            valid_start, valid_end,
            tx_start, None,
            props
        )
        self._edges.append(edge)
        self._current_edges.add((source, target))
        # Index
        self._valid_time_index[valid_start].append(edge)
        self._tx_time_index[tx_start].append(edge)
        self._vertices.add(source)
        self._vertices.add(target)
        self._edge_count += 1
        return edge_id or f"edge_{source}_{target}_{tx_start}"

    def remove_edge(self, source: str, target: str, edge_id: str | None = None) -> bool:
        """
        Remove edge (creates tombstone with transaction time).
        Args:
            source: Source vertex
            target: Target vertex
            edge_id: Edge ID
        Returns:
            True if removed
        WHY tombstone:
        - Preserves historical data
        - Records deletion in audit trail
        - Enables time-travel queries
        """
        if (source, target) not in self._current_edges:
            return False
        current_time = time.time()
        # Find active edges and mark with tx_end
        for edge in self._edges:
            if (edge.source == source and edge.target == target and 
                edge.tx_end is None):
                edge.tx_end = current_time
        self._current_edges.discard((source, target))
        self._edge_count -= 1
        return True

    def has_edge(self, source: str, target: str) -> bool:
        """Check if edge currently exists."""
        return (source, target) in self._current_edges
    # ============================================================================
    # TEMPORAL QUERIES
    # ============================================================================

    def get_edges_at_time(self, valid_time: float, tx_time: float) -> list[dict[str, Any]]:
        """
        Get edges active at specific valid and transaction times.
        Args:
            valid_time: Valid time point
            tx_time: Transaction time point
        Returns:
            List of edges active at both times
        WHY bitemporal query:
        - "Show me the graph as we knew it then"
        - Combines reality (valid) and knowledge (tx)
        - Essential for compliance and auditing
        """
        result = []
        for edge in self._edges:
            if edge.is_active(valid_time, tx_time):
                result.append({
                    'source': edge.source,
                    'target': edge.target,
                    'valid_start': edge.valid_start,
                    'valid_end': edge.valid_end,
                    'tx_start': edge.tx_start,
                    'tx_end': edge.tx_end,
                    **edge.properties
                })
        return result

    def as_of_query(self, tx_time: float) -> list[dict[str, Any]]:
        """
        Get graph state as it was known at transaction time.
        Args:
            tx_time: Transaction time point
        Returns:
            Edges known at that transaction time
        """
        result = []
        for edge in self._edges:
            if edge.is_known_at(tx_time):
                result.append({
                    'source': edge.source,
                    'target': edge.target,
                    'valid_start': edge.valid_start,
                    'valid_end': edge.valid_end,
                    **edge.properties
                })
        return result

    def get_edge_history(self, source: str, target: str) -> list[dict[str, Any]]:
        """
        Get complete history of edge.
        Args:
            source: Source vertex
            target: Target vertex
        Returns:
            All versions of edge
        """
        history = []
        for edge in self._edges:
            if edge.source == source and edge.target == target:
                history.append({
                    'valid_start': edge.valid_start,
                    'valid_end': edge.valid_end,
                    'tx_start': edge.tx_start,
                    'tx_end': edge.tx_end,
                    **edge.properties
                })
        return sorted(history, key=lambda x: x['tx_start'])
    # ============================================================================
    # STANDARD GRAPH OPERATIONS
    # ============================================================================

    def get_neighbors(self, node: str, edge_type: str | None = None,
                     direction: str = "outgoing") -> list[str]:
        """Get current neighbors."""
        neighbors = set()
        for source, target in self._current_edges:
            if source == node:
                neighbors.add(target)
        return list(neighbors)

    def neighbors(self, node: str) -> Iterator[Any]:
        """Get iterator over current neighbors."""
        return iter(self.get_neighbors(node))

    def degree(self, node: str) -> int:
        """Get degree of node in current snapshot."""
        return len(self.get_neighbors(node))

    def edges(self) -> Iterator[tuple[Any, Any, dict[str, Any]]]:
        """Iterate over current edges with properties."""
        for edge_dict in self.get_edges():
            yield (edge_dict['source'], edge_dict['target'], {})

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

    def get_edges(self, edge_type: str | None = None, direction: str = "both") -> list[dict[str, Any]]:
        """Get current edges."""
        edges = []
        for source, target in self._current_edges:
            edges.append({
                'source': source,
                'target': target,
                'edge_type': edge_type or 'bitemporal'
            })
        return edges

    def get_edge_data(self, source: str, target: str, edge_id: str | None = None) -> dict[str, Any] | None:
        """Get current edge data."""
        for edge in self._edges:
            if edge.source == source and edge.target == target and edge.tx_end is None:
                return {
                    'source': source,
                    'target': target,
                    'valid_start': edge.valid_start,
                    'valid_end': edge.valid_end,
                    'tx_start': edge.tx_start,
                    **edge.properties
                }
        return None
    # ============================================================================
    # GRAPH ALGORITHMS (on current snapshot)
    # ============================================================================

    def shortest_path(self, source: str, target: str, edge_type: str | None = None) -> list[str]:
        """Find shortest path in current snapshot."""
        if source not in self._vertices or target not in self._vertices:
            return []
        queue = deque([source])
        visited = {source}
        parent = {source: None}
        while queue:
            current = queue.popleft()
            if current == target:
                path = []
                while current:
                    path.append(current)
                    current = parent[current]
                return list(reversed(path))
            for neighbor in self.get_neighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    parent[neighbor] = current
                    queue.append(neighbor)
        return []

    def find_cycles(self, start_node: str, edge_type: str | None = None, max_depth: int = 10) -> list[list[str]]:
        """Find cycles in current snapshot."""
        return []

    def traverse_graph(self, start_node: str, strategy: str = "bfs",
                      max_depth: int = 100, edge_type: str | None = None) -> Iterator[str]:
        """Traverse current snapshot."""
        if start_node not in self._vertices:
            return
        visited = set()
        queue = deque([start_node])
        visited.add(start_node)
        while queue:
            current = queue.popleft()
            yield current
            for neighbor in self.get_neighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    queue.append(neighbor)

    def is_connected(self, source: str, target: str, edge_type: str | None = None) -> bool:
        """Check if vertices connected in current snapshot."""
        return len(self.shortest_path(source, target)) > 0
    # ============================================================================
    # STANDARD OPERATIONS
    # ============================================================================

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

    def __iter__(self) -> Iterator[dict[str, Any]]:
        """Iterate over current edges."""
        return iter(self.get_edges())

    def to_native(self) -> dict[str, Any]:
        """Convert to native representation."""
        return {
            'vertices': list(self._vertices),
            'current_edges': self.get_edges(),
            'total_versions': len(self._edges)
        }
    # ============================================================================
    # STATISTICS
    # ============================================================================

    def get_statistics(self) -> dict[str, Any]:
        """Get bitemporal statistics."""
        active_edges = sum(1 for e in self._edges if e.tx_end is None)
        historical_edges = sum(1 for e in self._edges if e.tx_end is not None)
        return {
            'vertices': len(self._vertices),
            'current_edges': len(self._current_edges),
            'active_versions': active_edges,
            'historical_versions': historical_edges,
            'total_versions': len(self._edges),
            'avg_versions_per_edge': len(self._edges) / max(len(self._current_edges), 1)
        }
    # ============================================================================
    # UTILITY METHODS
    # ============================================================================
    @property

    def strategy_name(self) -> str:
        """Get strategy name."""
        return "BITEMPORAL"
    @property

    def supported_traits(self) -> list[EdgeTrait]:
        """Get supported traits."""
        return [EdgeTrait.TEMPORAL, EdgeTrait.DIRECTED, EdgeTrait.SPARSE]

    def get_backend_info(self) -> dict[str, Any]:
        """Get backend information."""
        return {
            'strategy': 'Bitemporal Edges',
            'description': 'Valid-time and transaction-time for audit and time-travel',
            **self.get_statistics()
        }
