"""Algorithm dispatcher - routes to optimal backend (GPU or Rust).

Based on benchmarks (71K nodes, 121K edges):

GPU (cuGraph) - REQUIRED for:
    - Louvain: 125ms vs 3500ms CPU (28x faster)
    - PageRank: 45ms vs 95ms CPU (2x faster)
    - Triangle Count: 21ms vs 104ms CPU (5x faster)
    - Betweenness: 1.5s vs 28s CPU (19x faster)

Rust (petgraph) - REQUIRED for:
    - SCC: 23ms vs 200ms CPU (9x faster)
    - BFS: Parallel multi-source BFS with rayon
    - Cycles: Parallel cycle detection across SCCs
    - All graph queries: nodes, edges, degrees, etc.

NO NetworkX - Rust is the primary backend for graph operations.
"""

from typing import Dict, List, Optional, Set, Tuple

from loguru import logger

from .gpu import GPUBackend, gpu_available

# Import Rust backend - REQUIRED
try:
    from chainswarm_analyzers_baseline._rust import RustGraph
    _RUST_AVAILABLE = True
    logger.info("Rust (petgraph) backend: OK")
except ImportError:
    raise ImportError(
        "Rust backend required for graph operations. "
        "Build with: maturin develop --release"
    )


def rust_available() -> bool:
    """Check if Rust backend is available."""
    return _RUST_AVAILABLE


def get_backend() -> str:
    """Get the primary backend (gpu or rust)."""
    if gpu_available():
        return 'gpu'
    return 'rust'


class AlgorithmDispatcher:
    """Dispatches algorithms to optimal backend.

    Uses the fastest backend for each algorithm:
        - GPU (cuGraph): Louvain, PageRank, Triangles, Betweenness
        - Rust (petgraph): Everything else - SCC, BFS, Cycles, graph queries

    Usage:
        dispatcher = AlgorithmDispatcher(rust_graph)
        communities = dispatcher.louvain()      # GPU
        sccs = dispatcher.strongly_connected_components()  # Rust
    """

    def __init__(self, graph: RustGraph):
        """Initialize dispatcher with a RustGraph.

        Args:
            graph: RustGraph (petgraph) instance

        Raises:
            RuntimeError: If GPU is required but not available
        """
        self._rust = graph
        self._gpu: Optional[GPUBackend] = None
        self._gpu_loaded = False

        # Cache edge data for GPU loading
        self._sources: Optional[List[str]] = None
        self._targets: Optional[List[str]] = None
        self._weights: Optional[List[float]] = None

        # Check GPU availability
        self._use_gpu = gpu_available()

        # Backend status logged at module level, not per-instance

    @classmethod
    def from_arrays(
        cls,
        sources: List[str],
        targets: List[str],
        weights: List[float]
    ) -> "AlgorithmDispatcher":
        """Create dispatcher directly from edge arrays.

        This is the recommended way to create a dispatcher when you
        have raw edge data (e.g., from parquet files).

        Args:
            sources: List of source addresses
            targets: List of target addresses
            weights: List of edge weights

        Returns:
            AlgorithmDispatcher instance
        """
        graph = RustGraph()
        graph.add_edges(sources, targets, weights)
        dispatcher = cls(graph)

        # Cache arrays for GPU loading
        dispatcher._sources = sources
        dispatcher._targets = targets
        dispatcher._weights = weights

        return dispatcher

    def _ensure_gpu_loaded(self) -> None:
        """Load graph to GPU. Raises if GPU not available."""
        if not self._use_gpu:
            raise RuntimeError(
                "GPU backend required but not available. "
                "Install cuGraph: pip install cugraph-cu12"
            )

        if self._gpu_loaded:
            return

        self._gpu = GPUBackend()

        # Get edge data from Rust graph or cache
        if self._sources is not None:
            sources, targets, weights = self._sources, self._targets, self._weights
        else:
            # Extract from RustGraph
            edges = self._rust.edges()
            sources = [e[0] for e in edges]
            targets = [e[1] for e in edges]
            weights = [e[2] for e in edges]

        n_nodes, n_edges = self._gpu.load_from_edges(sources, targets, weights)
        self._gpu_loaded = True
        logger.debug(f"Loaded graph to GPU: {n_nodes} nodes, {n_edges} edges")

    # =========================================================================
    # GPU algorithms (cuGraph) - 28x faster for Louvain
    # =========================================================================

    def louvain(self, resolution: float = 1.0) -> List[Set[str]]:
        """Louvain community detection on GPU.

        Performance: 125ms GPU vs 3500ms CPU (28x faster)
        """
        self._ensure_gpu_loaded()
        logger.debug("Running Louvain on GPU")
        return self._gpu.louvain(resolution=resolution)

    def pagerank(self, alpha: float = 0.85, max_iter: int = 100) -> Dict[str, float]:
        """PageRank centrality on GPU.

        Performance: 45ms GPU vs 95ms CPU (2x faster)
        """
        self._ensure_gpu_loaded()
        logger.debug("Running PageRank on GPU")
        return self._gpu.pagerank(alpha=alpha, max_iter=max_iter)

    def triangle_count(self) -> int:
        """Count triangles on GPU.

        Performance: 21ms GPU vs 104ms CPU (5x faster)
        """
        self._ensure_gpu_loaded()
        logger.debug("Running triangle count on GPU")
        return self._gpu.triangle_count()

    def betweenness_centrality(self, k: Optional[int] = None) -> Dict[str, float]:
        """Betweenness centrality on GPU.

        Performance: 1.5s GPU vs 28s CPU (19x faster)
        Note: Full betweenness is O(n*m), sampling with k is recommended.
        """
        self._ensure_gpu_loaded()
        logger.debug(f"Running betweenness centrality on GPU (k={k})")
        return self._gpu.betweenness_centrality(k=k)

    # =========================================================================
    # Rust algorithms (petgraph) - Primary backend
    # =========================================================================

    def strongly_connected_components(self) -> List[Set[str]]:
        """Strongly connected components on Rust.

        Performance: 23ms Rust vs 200ms CPU (9x faster)
        Note: GPU SCC is broken (32x slower than CPU)
        """
        logger.debug("Running SCC on Rust")
        sccs = self._rust.strongly_connected_components()
        return [set(scc) for scc in sccs]

    def bfs(self, source: str, cutoff: Optional[int] = None) -> Dict[str, int]:
        """Breadth-first search on Rust.

        Uses Rust BFS which is optimized for parallel multi-source.
        """
        logger.debug("Running BFS on Rust")
        return self._rust.bfs(source, cutoff)

    def multi_source_bfs(
        self,
        sources: List[str],
        cutoff: Optional[int] = None
    ) -> List[Tuple[str, str, int]]:
        """Parallel multi-source BFS on Rust.

        Args:
            sources: List of source addresses
            cutoff: Maximum distance (default 3)

        Returns:
            List of (source, target, distance) tuples
        """
        logger.debug(f"Running multi-source BFS on Rust ({len(sources)} sources)")
        return self._rust.multi_source_bfs(sources, cutoff)

    def simple_cycles(
        self,
        length_limit: Optional[int] = None,
        max_cycles_per_scc: int = 100
    ) -> List[List[str]]:
        """Find simple cycles using Rust (parallel across SCCs)."""
        max_len = length_limit or 10
        logger.debug(f"Running simple_cycles on Rust (parallel, max_length={max_len})")
        return self._rust.simple_cycles(max_len, max_cycles_per_scc)

    def find_suspicious_cycles(
        self,
        max_length: int = 10,
        max_cycles: int = 100,
        min_volume: float = 0.0
    ) -> List[Tuple[List[str], float, float]]:
        """Find cycles ranked by volume and suspicion score."""
        logger.debug(f"Running suspicious cycle detection on Rust")
        return self._rust.find_suspicious_cycles(max_length, max_cycles, min_volume)

    def find_layering_paths(
        self,
        sources: List[str],
        targets: List[str],
        min_length: int = 3,
        max_length: int = 10,
        max_paths_per_source: int = 10,
        min_volume: float = 0.0,
        max_cv: float = 0.5
    ) -> List[Tuple[List[str], List[float], float]]:
        """Find layering paths on Rust (parallel from sources)."""
        logger.debug(f"Running layering path detection on Rust")
        return self._rust.find_layering_paths(
            sources, targets, min_length, max_length,
            max_paths_per_source, min_volume, max_cv
        )

    def calculate_risk_proximity(
        self,
        risk_addresses: List[str],
        max_distance: int = 3,
        decay_factor: float = 1.0
    ) -> List[Tuple[str, float, str, int]]:
        """Calculate risk proximity scores on Rust."""
        logger.debug(f"Running risk proximity on Rust ({len(risk_addresses)} risk addresses)")
        return self._rust.calculate_risk_proximity(risk_addresses, max_distance, decay_factor)

    # =========================================================================
    # Graph queries (Rust) - Replace NetworkX operations
    # =========================================================================

    def node_count(self) -> int:
        """Get number of nodes."""
        return self._rust.node_count()

    def edge_count(self) -> int:
        """Get number of edges."""
        return self._rust.edge_count()

    def nodes(self) -> List[str]:
        """Get all node names."""
        return self._rust.nodes()

    def edges(self) -> List[Tuple[str, str, float]]:
        """Get all edges as (source, target, weight) tuples."""
        return self._rust.edges()

    def in_degree(self, node: str) -> int:
        """Get in-degree for a node."""
        return self._rust.in_degree(node)

    def out_degree(self, node: str) -> int:
        """Get out-degree for a node."""
        return self._rust.out_degree(node)

    def degree_map(self) -> Dict[str, Tuple[int, int]]:
        """Get in/out degree for all nodes."""
        return self._rust.degree_map()

    def volume_map(self) -> Dict[str, Tuple[float, float]]:
        """Get in/out volume for all nodes."""
        return self._rust.volume_map()

    def neighbors(self, node: str) -> List[str]:
        """Get all neighbors (undirected) for a node."""
        return self._rust.neighbors(node)

    def subgraph(self, nodes: List[str]) -> RustGraph:
        """Create a subgraph containing only the specified nodes."""
        return self._rust.subgraph(nodes)

    def density(self) -> float:
        """Calculate graph density."""
        return self._rust.density()

    def clustering_coefficient(self) -> Dict[str, float]:
        """Calculate clustering coefficient for all nodes."""
        return self._rust.clustering_coefficient()

    def average_clustering(self) -> float:
        """Calculate average clustering coefficient."""
        return self._rust.average_clustering()

    def closeness_centrality(self) -> Dict[str, float]:
        """Calculate closeness centrality for all nodes (Rust parallel)."""
        logger.debug("Running closeness centrality on Rust")
        return self._rust.closeness_centrality()

    def core_number(self) -> Dict[str, int]:
        """Calculate k-core number for each node."""
        logger.debug("Running k-core decomposition on Rust")
        return self._rust.core_number()

    # =========================================================================
    # Pattern detection (Rust) - For detectors
    # =========================================================================

    def detect_fan_in_motifs(
        self,
        min_sources: int = 5,
        min_volume: float = 0.0
    ) -> List[Tuple[str, List[str], float, int]]:
        """Detect fan-in motifs (consolidation patterns)."""
        return self._rust.detect_fan_in_motifs(min_sources, min_volume)

    def detect_fan_out_motifs(
        self,
        min_targets: int = 5,
        min_volume: float = 0.0
    ) -> List[Tuple[str, List[str], float, int]]:
        """Detect fan-out motifs (distribution patterns)."""
        return self._rust.detect_fan_out_motifs(min_targets, min_volume)

    def detect_threshold_evasion(
        self,
        threshold: float,
        tolerance_pct: float = 0.1
    ) -> List[Tuple[str, str, float, float]]:
        """Detect threshold evasion patterns."""
        return self._rust.detect_threshold_evasion(threshold, tolerance_pct)

    def detect_temporal_bursts(
        self,
        addresses: List[str],
        timestamps_list: List[List[int]],
        volumes_list: List[List[float]],
        counterparties_list: List[List[str]],
        time_window_ms: int,
        min_transactions: int = 10,
        min_intensity: float = 3.0,
        z_threshold: float = 2.0
    ) -> List[Tuple[str, int, int, int, float, float, float, List[str]]]:
        """Detect temporal bursts (parallel per address)."""
        return self._rust.detect_temporal_bursts(
            addresses, timestamps_list, volumes_list, counterparties_list,
            time_window_ms, min_transactions, min_intensity, z_threshold
        )

    def find_chain_bursts(
        self,
        sources: List[str],
        targets: List[str],
        timestamps: List[int],
        volumes: List[float],
        time_window_ms: int,
        min_chain_length: int = 5,
        max_chains: int = 1000
    ) -> List[Tuple[List[str], float, int, int]]:
        """Find chain bursts in temporal edge data."""
        return self._rust.find_chain_bursts(
            sources, targets, timestamps, volumes,
            time_window_ms, min_chain_length, max_chains
        )

    # =========================================================================
    # Utility methods
    # =========================================================================

    def get_backend_for_algorithm(self, algorithm: str) -> str:
        """Get which backend is used for a specific algorithm."""
        gpu_algorithms = {'louvain', 'pagerank', 'triangle_count', 'betweenness_centrality'}
        rust_algorithms = {
            'scc', 'strongly_connected_components', 'bfs', 'multi_source_bfs',
            'simple_cycles', 'find_suspicious_cycles', 'find_layering_paths',
            'calculate_risk_proximity', 'closeness_centrality', 'core_number',
            'clustering_coefficient', 'average_clustering', 'density',
            'detect_fan_in_motifs', 'detect_fan_out_motifs', 'detect_threshold_evasion',
            'detect_temporal_bursts', 'find_chain_bursts'
        }

        if algorithm in gpu_algorithms:
            return 'gpu' if self._use_gpu else 'unavailable'
        if algorithm in rust_algorithms:
            return 'rust'
        return 'unknown'

    @property
    def gpu_enabled(self) -> bool:
        """Check if GPU is available."""
        return self._use_gpu

    @property
    def rust_enabled(self) -> bool:
        """Check if Rust is available."""
        return True  # Always available (required)

    @property
    def graph(self) -> RustGraph:
        """Get the underlying RustGraph."""
        return self._rust

    def verify_backends(self) -> Dict[str, bool]:
        """Verify all required backends are available."""
        return {
            'gpu': self._use_gpu,
            'rust': True,  # Always available (required)
        }
