"""Layering pattern detector using Rust parallel path finding.

Optimized with Rust + rayon for ~450x speedup.
No NetworkX dependency.
"""

from typing import Dict, List, Any, Optional

import numpy as np
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,
)


class LayeringDetector(BasePatternDetector):
    """Detects layering patterns using parallel path search.

    Uses Rust + rayon for parallel DFS (~450x faster than NetworkX).
    """

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

    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 []

        # Get config values
        min_path_length = self._get_config_value('path_analysis', 'min_path_length', 3)
        max_path_length = self._get_config_value('path_analysis', 'max_path_length', 10)
        max_paths_to_check = self._get_config_value('path_analysis', 'max_paths_to_check', 1000)
        high_volume_percentile = self._get_config_value('path_analysis', 'high_volume_percentile', 90)
        max_source_nodes = self._get_config_value('path_analysis', 'max_source_nodes', 50)
        max_target_nodes = self._get_config_value('path_analysis', 'max_target_nodes', 50)
        layering_min_volume = self._get_config_value('path_analysis', 'layering_min_volume', 1000)
        layering_cv_threshold = self._get_config_value('path_analysis', 'layering_cv_threshold', 0.5)

        # Detect using Rust
        return self._detect_layering(
            graph_data, window_days, processing_date,
            min_path_length, max_path_length, max_paths_to_check,
            high_volume_percentile, max_source_nodes, max_target_nodes,
            layering_min_volume, layering_cv_threshold
        )

    def _detect_layering(
        self,
        graph_data: GraphData,
        window_days: int,
        processing_date: str,
        min_path_length: int,
        max_path_length: int,
        max_paths_to_check: int,
        high_volume_percentile: float,
        max_source_nodes: int,
        max_target_nodes: int,
        layering_min_volume: float,
        layering_cv_threshold: float
    ) -> List[Dict[str, Any]]:
        """Detect layering patterns using Rust parallel path finder."""
        graph = graph_data.graph

        # Get high volume nodes from Rust
        high_vol_nodes = graph.get_high_volume_nodes(high_volume_percentile)

        if len(high_vol_nodes) < 2:
            return []

        # Extract node names
        source_nodes = [n for n, v in high_vol_nodes[:max_source_nodes]]
        target_nodes = [n for n, v in high_vol_nodes[:max_target_nodes]]

        # Find layering paths in parallel (Rust + rayon)
        # This function already filters by min_volume and max_cv
        max_paths_per_source = max(1, max_paths_to_check // max_source_nodes)

        paths = graph.find_layering_paths(
            source_nodes,
            target_nodes,
            min_path_length,
            max_path_length,
            max_paths_per_source,
            layering_min_volume,
            layering_cv_threshold
        )

        # Convert paths to pattern objects
        patterns_by_hash = {}

        for path_nodes, edge_weights, total_volume in paths:
            if len(path_nodes) < min_path_length:
                continue

            sorted_path = sorted(path_nodes)
            pattern_hash = generate_pattern_hash(PatternType.LAYERING_PATH, sorted_path)

            if pattern_hash in patterns_by_hash:
                continue

            # Calculate scores
            layering_score = self._calculate_layering_score(
                path_nodes, edge_weights, total_volume
            )
            severity = self._determine_layering_severity(path_nodes, total_volume)
            unique_assets = self._get_path_assets(graph_data, path_nodes)

            # Build address roles
            address_roles = {path_nodes[0]: 'source'}
            for addr in path_nodes[1:-1]:
                address_roles[addr] = 'intermediary'
            address_roles[path_nodes[-1]] = 'destination'

            pattern_id = generate_pattern_id(PatternType.LAYERING_PATH, pattern_hash)

            pattern = {
                'pattern_id': pattern_id,
                'pattern_type': PatternType.LAYERING_PATH,
                'pattern_hash': pattern_hash,
                'addresses_involved': path_nodes,
                'address_roles': address_roles,
                'transaction_ids': [],
                'total_amount_usd': total_volume,
                'detection_method': DetectionMethod.PATH_ANALYSIS,
                'confidence_score': layering_score,
                'severity': severity,
                'evidence': {
                    'path_length': len(path_nodes),
                    'path_addresses': path_nodes,
                    'unique_assets': unique_assets,
                    'layering_score': layering_score,
                    'edge_weights': edge_weights,
                },
                'window_days': window_days,
                'processing_date': processing_date,
                'network': self.network or '',
            }

            patterns_by_hash[pattern_hash] = pattern

        logger.info(f"Detected {len(patterns_by_hash)} unique layering patterns")
        return list(patterns_by_hash.values())

    def _get_path_assets(
        self,
        graph_data: GraphData,
        path: List[str]
    ) -> List[str]:
        """Get unique assets used in path."""
        assets = set()
        for i in range(len(path) - 1):
            asset = graph_data.get_edge_attr(path[i], path[i + 1], 'asset_symbol')
            if asset:
                assets.add(asset)
        return list(assets)

    def _calculate_layering_score(
        self,
        path: List[str],
        edge_weights: List[float],
        path_volume: float
    ) -> float:
        """Calculate layering score from path characteristics."""
        score = 0.5

        if len(path) >= 4:
            score += 0.1
        if len(path) >= 6:
            score += 0.1

        if path_volume > 10000:
            score += 0.1
        if path_volume > 100000:
            score += 0.1

        if len(edge_weights) > 1:
            mean_w = np.mean(edge_weights)
            if mean_w > 0:
                cv = np.std(edge_weights) / mean_w
                if cv < 0.2:
                    score += 0.1

        return min(score, 1.0)

    def _determine_layering_severity(
        self,
        path: List[str],
        path_volume: float
    ) -> str:
        """Determine severity based on path characteristics."""
        has_fraudulent = any(self._is_fraudulent_address(addr) for addr in path)

        if has_fraudulent:
            return Severity.CRITICAL

        if path_volume > 100000 and len(path) >= 5:
            return Severity.HIGH

        if path_volume > 50000 or len(path) >= 6:
            return Severity.HIGH

        if path_volume > 10000:
            return Severity.MEDIUM

        return Severity.LOW
