"""Network pattern detector (smurfing, sybil).

Uses Rust (petgraph) for SCC and graph queries.
Uses GPU (cuGraph) for Louvain community detection.
No NetworkX dependency.
"""

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

from loguru import logger

from chainswarm_analyzers_baseline.graph.builder import GraphData
from chainswarm_analyzers_baseline.patterns.base_detector import (
    BasePatternDetector,
    PatternType,
    DetectionMethod,
    Severity,
    generate_pattern_hash,
    generate_pattern_id,
)
from chainswarm_analyzers_baseline.backends.dispatcher import AlgorithmDispatcher


class NetworkDetector(BasePatternDetector):
    """Network pattern detector with GPU acceleration.

    Uses cuGraph GPU backend for Louvain community detection (33x faster).
    Uses Rust for SCC (GPU is 32x slower due to cuGraph bug).
    """

    def __init__(
        self,
        config: Optional[Dict] = None,
        address_labels_cache: Optional[Dict[str, Dict[str, Any]]] = None,
        network: Optional[str] = None
    ):
        super().__init__(config, address_labels_cache, network)
        self._dispatcher: Optional[AlgorithmDispatcher] = None

    @property
    def pattern_type(self) -> str:
        return PatternType.SMURFING_NETWORK

    def detect(
        self,
        graph_data: GraphData,
        address_labels: Dict[str, Dict[str, Any]],
        window_days: int,
        processing_date: str
    ) -> List[Dict[str, Any]]:
        self._address_labels_cache = address_labels
        self._graph_data = graph_data

        graph = graph_data.graph

        if graph.node_count() == 0:
            return []

        # Initialize dispatcher for GPU/Rust algorithm routing
        self._dispatcher = AlgorithmDispatcher(graph)

        patterns = []

        # SCC uses Rust (GPU is 32x slower)
        scc_patterns = self._analyze_scc(graph_data, window_days, processing_date)
        patterns.extend(scc_patterns)
        logger.debug(f"SCC analysis found {len(scc_patterns)} patterns")

        # Louvain uses GPU (33x faster)
        smurfing_patterns = self._detect_smurfing(graph_data, window_days, processing_date)
        patterns.extend(smurfing_patterns)
        logger.debug(f"Smurfing detection found {len(smurfing_patterns)} patterns")

        sybil_patterns = self._detect_sybil_networks(graph_data, window_days, processing_date)
        patterns.extend(sybil_patterns)
        logger.debug(f"Sybil detection found {len(sybil_patterns)} patterns")

        logger.info(f"Detected {len(patterns)} network patterns")
        return patterns

    def _analyze_scc(
        self,
        graph_data: GraphData,
        window_days: int,
        processing_date: str
    ) -> List[Dict[str, Any]]:
        """Analyze strongly connected components."""
        patterns_by_hash = {}

        # Use Rust SCC (9x faster than NetworkX)
        sccs = self._dispatcher.strongly_connected_components()

        min_scc_size = self._get_config_value('scc_analysis', 'min_scc_size', 5)

        scc_sizes = [len(scc) for scc in sccs]
        if not scc_sizes:
            return []

        graph = graph_data.graph

        for scc in sccs:
            scc_size = len(scc)

            if scc_size < min_scc_size:
                continue

            sorted_scc = sorted(list(scc))
            pattern_hash = generate_pattern_hash(PatternType.SMURFING_NETWORK, sorted_scc)

            if pattern_hash in patterns_by_hash:
                continue

            # Create subgraph and calculate metrics
            scc_nodes = list(scc)
            scc_graph = graph.subgraph(scc_nodes)

            # Get total volume from edges in subgraph
            total_volume = 0.0
            for src, tgt, weight in scc_graph.edges():
                total_volume += weight

            density = scc_graph.density()

            coordination_score = self._calculate_coordination_score(scc_graph)
            hub_addresses = self._identify_hubs_in_network(scc_graph)

            address_roles = {
                addr: 'hub' if addr in hub_addresses else 'participant'
                for addr in sorted_scc
            }

            pattern_id = generate_pattern_id('scc', pattern_hash)

            pattern = {
                'pattern_id': pattern_id,
                'pattern_type': PatternType.SMURFING_NETWORK,
                'pattern_hash': pattern_hash,
                'addresses_involved': sorted_scc,
                'address_roles': address_roles,
                'transaction_ids': [],
                'total_amount_usd': total_volume,
                'detection_method': DetectionMethod.SCC_ANALYSIS,
                'confidence_score': min(density + 0.3, 1.0),
                'severity': self._determine_network_severity(
                    sorted_scc, total_volume, density
                ),
                'evidence': {
                    'network_size': scc_size,
                    'hub_addresses': hub_addresses,
                    'spoke_count': scc_size - len(hub_addresses),
                    'coordination_score': coordination_score,
                },
                'window_days': window_days,
                'processing_date': processing_date,
                'network': self.network or '',
            }

            patterns_by_hash[pattern_hash] = pattern

        return list(patterns_by_hash.values())

    def _detect_smurfing(
        self,
        graph_data: GraphData,
        window_days: int,
        processing_date: str
    ) -> List[Dict[str, Any]]:
        """Detect smurfing networks using Louvain community detection (GPU)."""
        patterns_by_hash = {}

        min_community_size = self._get_config_value('network_analysis', 'min_community_size', 5)
        max_community_size = self._get_config_value('network_analysis', 'max_community_size', 100)
        small_tx_threshold = self._get_config_value('network_analysis', 'small_transaction_threshold', 1000)
        small_tx_ratio_threshold = self._get_config_value('network_analysis', 'small_transaction_ratio_threshold', 0.7)
        density_threshold = self._get_config_value('network_analysis', 'density_threshold', 0.1)

        graph = graph_data.graph

        # Use GPU Louvain (33x faster than CPU)
        try:
            communities = self._dispatcher.louvain()
        except RuntimeError as e:
            logger.warning(f"Louvain failed (GPU may not be available): {e}")
            return []

        for community in communities:
            community_size = len(community)

            if community_size < min_community_size or community_size > max_community_size:
                continue

            # Create subgraph
            community_nodes = list(community)
            community_graph = graph.subgraph(community_nodes)

            if self._is_smurfing_network(
                community_graph, graph_data, community_nodes,
                small_tx_threshold, small_tx_ratio_threshold, density_threshold
            ):
                sorted_community = sorted(community_nodes)
                pattern_hash = generate_pattern_hash(
                    PatternType.SMURFING_NETWORK, sorted_community
                )

                if pattern_hash in patterns_by_hash:
                    continue

                density = community_graph.density()

                # Get total volume
                total_volume = 0.0
                for src, tgt, weight in community_graph.edges():
                    total_volume += weight

                hub_addresses = self._identify_hubs_in_network(community_graph)
                coordination_score = self._calculate_coordination_score(community_graph)

                address_roles = {
                    addr: 'hub' if addr in hub_addresses else 'participant'
                    for addr in sorted_community
                }

                pattern_id = generate_pattern_id(
                    PatternType.SMURFING_NETWORK, pattern_hash
                )

                pattern = {
                    'pattern_id': pattern_id,
                    'pattern_type': PatternType.SMURFING_NETWORK,
                    'pattern_hash': pattern_hash,
                    'addresses_involved': sorted_community,
                    'address_roles': address_roles,
                    'transaction_ids': [],
                    'total_amount_usd': total_volume,
                    'detection_method': DetectionMethod.NETWORK_ANALYSIS,
                    'confidence_score': self._calculate_smurfing_confidence(
                        community_graph, graph_data, community_nodes, small_tx_threshold
                    ),
                    'severity': self._determine_network_severity(
                        sorted_community, total_volume, density
                    ),
                    'evidence': {
                        'network_size': community_size,
                        'hub_addresses': hub_addresses,
                        'spoke_count': community_size - len(hub_addresses),
                        'coordination_score': coordination_score,
                    },
                    'window_days': window_days,
                    'processing_date': processing_date,
                    'network': self.network or '',
                }

                patterns_by_hash[pattern_hash] = pattern

        return list(patterns_by_hash.values())

    def _detect_sybil_networks(
        self,
        graph_data: GraphData,
        window_days: int,
        processing_date: str
    ) -> List[Dict[str, Any]]:
        """Detect sybil networks (addresses with similar targets)."""
        patterns_by_hash = {}

        min_network_size = self._get_config_value('sybil_detection', 'min_network_size', 10)
        similarity_threshold = self._get_config_value('sybil_detection', 'similarity_threshold', 0.8)
        max_addresses = self._get_config_value('sybil_detection', 'max_addresses_to_check', 5000)

        graph = graph_data.graph

        # Get outgoing targets for each node
        outgoing_targets: Dict[str, Set[str]] = {}
        for node in graph.nodes():
            out_edges = graph.out_edges(node)
            targets = {tgt for tgt, _ in out_edges}
            if len(targets) >= 2:
                outgoing_targets[node] = targets

        # Limit addresses to check
        if len(outgoing_targets) > max_addresses:
            logger.debug(f"Sybil detection: limiting from {len(outgoing_targets)} to {max_addresses} addresses")
            # Sort by number of targets (descending)
            sorted_items = sorted(outgoing_targets.items(), key=lambda x: len(x[1]), reverse=True)
            outgoing_targets = dict(sorted_items[:max_addresses])

        logger.debug(f"Sybil detection: checking {len(outgoing_targets)} addresses")

        processed: Set[str] = set()
        addr_list = list(outgoing_targets.keys())

        for i, addr1 in enumerate(addr_list):
            if addr1 in processed:
                continue

            targets1 = outgoing_targets[addr1]
            cluster = {addr1}

            for addr2 in addr_list[i+1:]:
                if addr2 in processed:
                    continue

                targets2 = outgoing_targets[addr2]
                union_size = len(targets1 | targets2)
                if union_size > 0:
                    similarity = len(targets1 & targets2) / union_size
                    if similarity >= similarity_threshold:
                        cluster.add(addr2)

            if len(cluster) >= min_network_size:
                sorted_cluster = sorted(list(cluster))
                pattern_hash = generate_pattern_hash(
                    PatternType.SYBIL_NETWORK, sorted_cluster
                )

                if pattern_hash not in patterns_by_hash:
                    # Create subgraph and get volume
                    cluster_graph = graph.subgraph(list(cluster))
                    total_volume = sum(w for _, _, w in cluster_graph.edges())

                    hub_addresses = self._identify_hubs_in_network(cluster_graph)

                    address_roles = {
                        addr: 'hub' if addr in hub_addresses else 'sybil'
                        for addr in sorted_cluster
                    }

                    pattern_id = generate_pattern_id(
                        PatternType.SYBIL_NETWORK, pattern_hash
                    )

                    pattern = {
                        'pattern_id': pattern_id,
                        'pattern_type': PatternType.SYBIL_NETWORK,
                        'pattern_hash': pattern_hash,
                        'addresses_involved': sorted_cluster,
                        'address_roles': address_roles,
                        'transaction_ids': [],
                        'total_amount_usd': total_volume,
                        'detection_method': DetectionMethod.NETWORK_ANALYSIS,
                        'confidence_score': 0.7,
                        'severity': Severity.HIGH,
                        'evidence': {
                            'network_size': len(cluster),
                            'hub_addresses': hub_addresses,
                            'spoke_count': len(cluster) - len(hub_addresses),
                            'coordination_score': 0.9,
                        },
                        'window_days': window_days,
                        'processing_date': processing_date,
                        'network': self.network or '',
                    }

                    patterns_by_hash[pattern_hash] = pattern

                processed.update(cluster)

        return list(patterns_by_hash.values())

    def _is_smurfing_network(
        self,
        community_graph,  # RustGraph subgraph
        graph_data: GraphData,
        community_nodes: List[str],
        small_tx_threshold: float,
        small_tx_ratio_threshold: float,
        density_threshold: float
    ) -> bool:
        """Check if a community exhibits smurfing characteristics."""
        if community_graph.node_count() < 5:
            return False

        # Get volumes from edges
        volumes = [w for _, _, w in community_graph.edges()]

        if not volumes:
            return False

        small_tx_ratio = sum(1 for v in volumes if v < small_tx_threshold) / len(volumes)
        density = community_graph.density()

        return small_tx_ratio > small_tx_ratio_threshold and density > density_threshold

    def _identify_hubs_in_network(self, network_graph) -> List[str]:
        """Identify hub nodes in a network subgraph."""
        if network_graph.node_count() < 3:
            return []

        # Get degree for each node
        degree_map = network_graph.degree_map()
        degrees = [
            (node, in_deg + out_deg)
            for node, (in_deg, out_deg) in degree_map.items()
        ]
        degrees.sort(key=lambda x: x[1], reverse=True)

        num_hubs = max(1, len(degrees) // 5)
        return [node for node, _ in degrees[:num_hubs]]

    def _calculate_coordination_score(self, network_graph) -> float:
        """Calculate coordination score (average clustering coefficient)."""
        if network_graph.node_count() < 2:
            return 0.0

        try:
            return network_graph.average_clustering()
        except Exception:
            return 0.0

    def _calculate_smurfing_confidence(
        self,
        community_graph,  # RustGraph subgraph
        graph_data: GraphData,
        community_nodes: List[str],
        small_tx_threshold: float
    ) -> float:
        """Calculate confidence score for smurfing pattern."""
        volumes = [w for _, _, w in community_graph.edges()]

        if not volumes:
            return 0.0

        small_tx_ratio = sum(1 for v in volumes if v < small_tx_threshold) / len(volumes)
        density = community_graph.density()

        return min((small_tx_ratio * 0.6 + density * 0.4), 1.0)

    def _determine_network_severity(
        self,
        addresses: List[str],
        total_volume: float,
        density: float
    ) -> str:
        """Determine severity based on network characteristics."""
        has_fraudulent = any(self._is_fraudulent_address(addr) for addr in addresses)

        if has_fraudulent:
            return Severity.CRITICAL

        if total_volume > 100000 and len(addresses) >= 20:
            return Severity.HIGH

        if total_volume > 50000 or len(addresses) >= 30:
            return Severity.HIGH

        if total_volume > 10000:
            return Severity.MEDIUM

        return Severity.LOW
