"""Address feature analyzer.

Computes features for each address using Rust and GPU backends.
No NetworkX dependency.
"""

from typing import Dict, List, Any, Optional
from decimal import Decimal
from collections import defaultdict
import math

import numpy as np
from loguru import logger
from cdlib import algorithms as cd_algorithms
from scipy.stats import skew, kurtosis

from chainswarm_analyzers_baseline.graph.builder import GraphData
from chainswarm_analyzers_baseline.backends.dispatcher import AlgorithmDispatcher


class AddressFeatureAnalyzer:
    """Compute features for addresses using Rust/GPU backends."""

    def analyze(
        self,
        graph_data: GraphData,
        address_labels: Dict[str, Dict[str, Any]],
        transfer_aggregates: Optional[Dict[str, Dict[str, Any]]] = None
    ) -> Dict[str, Dict[str, Any]]:
        graph = graph_data.graph

        if graph.node_count() == 0:
            raise ValueError("Empty graph provided - cannot compute features")

        if transfer_aggregates is None:
            transfer_aggregates = {}

        logger.info(f"Computing features for {graph.node_count()} addresses")

        # Create dispatcher for algorithm routing
        dispatcher = AlgorithmDispatcher(graph)

        logger.info("Computing global graph analytics")
        graph_analytics = self._compute_all_graph_algorithms(graph_data, dispatcher)

        flows_by_address = self._build_flows_index_from_graph(graph_data)

        results = {}
        for address in graph.nodes():
            flows = flows_by_address.get(address, [])
            aggregates = transfer_aggregates.get(address, {})

            base_features = self._compute_base_features(address, flows)
            volume_features = self._compute_volume_features(address, flows)
            statistical_features = self._compute_statistical_features(address, flows, aggregates)
            flow_features = self._compute_flow_features(address, flows, aggregates)
            graph_features = graph_analytics.get(address, self._empty_graph_features())
            directional_features = self._compute_directional_flow_features(address, flows)
            behavioral_features = self._compute_behavioral_features(address, flows, aggregates)
            edge_features = self._compute_edge_features(address, graph_data)

            label_info = address_labels.get(address, {})
            label_features = self._compute_label_features(address, label_info)

            all_features = {
                'address': address,
                **base_features,
                **volume_features,
                **statistical_features,
                **flow_features,
                **graph_features,
                **directional_features,
                **behavioral_features,
                **edge_features,
                **label_features,
            }

            results[address] = all_features

        logger.info(f"Computed features for {len(results)} addresses")
        return results

    def _build_flows_index_from_graph(
        self,
        graph_data: GraphData
    ) -> Dict[str, List[Dict[str, Any]]]:
        """Build flows index from graph edges."""
        flows_by_address: Dict[str, List[Dict[str, Any]]] = defaultdict(list)

        graph = graph_data.graph
        for src, tgt, weight in graph.edges():
            # Get additional attributes from edge_attrs
            tx_count = graph_data.get_edge_attr(src, tgt, 'tx_count', 1)

            flow = {
                'from_address': src,
                'to_address': tgt,
                'amount_usd_sum': float(weight),
                'tx_count': int(tx_count)
            }
            flows_by_address[src].append(flow)
            flows_by_address[tgt].append(flow)

        return dict(flows_by_address)

    def _compute_base_features(
        self,
        address: str,
        flows: List[Dict[str, Any]]
    ) -> Dict[str, Any]:
        tx_in = sum(f['tx_count'] for f in flows if f['to_address'] == address)
        tx_out = sum(f['tx_count'] for f in flows if f['from_address'] == address)

        recipients = {f['to_address'] for f in flows if f['from_address'] == address}
        senders = {f['from_address'] for f in flows if f['to_address'] == address}

        return {
            'degree_in': len(senders),
            'degree_out': len(recipients),
            'degree_total': len(senders | recipients),
            'unique_counterparties': len(senders | recipients),
            'tx_in_count': tx_in,
            'tx_out_count': tx_out,
            'tx_total_count': tx_in + tx_out,
            'unique_recipients_count': len(recipients),
            'unique_senders_count': len(senders),
        }

    def _compute_volume_features(
        self,
        address: str,
        flows: List[Dict[str, Any]]
    ) -> Dict[str, Any]:
        total_in = Decimal('0')
        total_out = Decimal('0')
        amounts_in: List[float] = []
        amounts_out: List[float] = []

        for f in flows:
            amount = Decimal(str(f['amount_usd_sum']))
            if f['to_address'] == address:
                total_in += amount
                amounts_in.append(float(amount))
            if f['from_address'] == address:
                total_out += amount
                amounts_out.append(float(amount))

        all_amounts = amounts_in + amounts_out
        tx_in_count = len(amounts_in)
        tx_out_count = len(amounts_out)

        median_in = float(np.median(amounts_in)) if amounts_in else 0.0
        median_out = float(np.median(amounts_out)) if amounts_out else 0.0
        max_tx = float(max(all_amounts)) if all_amounts else 0.0
        min_tx = float(min(all_amounts)) if all_amounts else 0.0

        total_volume = total_in + total_out

        return {
            'total_in_usd': total_in,
            'total_out_usd': total_out,
            'net_flow_usd': total_in - total_out,
            'total_volume_usd': total_volume,
            'avg_tx_in_usd': total_in / max(tx_in_count, 1),
            'avg_tx_out_usd': total_out / max(tx_out_count, 1),
            'median_tx_in_usd': Decimal(str(median_in)),
            'median_tx_out_usd': Decimal(str(median_out)),
            'max_tx_usd': Decimal(str(max_tx)),
            'min_tx_usd': Decimal(str(min_tx)),
        }

    def _compute_statistical_features(
        self,
        address: str,
        flows: List[Dict[str, Any]],
        aggregates: Dict[str, Any]
    ) -> Dict[str, Any]:
        moments = aggregates.get('amount_moments', {})
        n = int(moments.get('n', 0))
        s1 = float(moments.get('s1', 0.0))
        s2 = float(moments.get('s2', 0.0))

        if n >= 2 and s1 > 0:
            mean = s1 / n
            variance = max(0, (s2 - s1 ** 2 / n) / (n - 1))
            std = math.sqrt(variance)
            cv = std / max(mean, 1.0)
        else:
            all_amounts = [float(f['amount_usd_sum']) for f in flows]

            if len(all_amounts) < 2:
                return {
                    'amount_variance': 0.0,
                    'amount_skewness': 0.0,
                    'amount_kurtosis': 0.0,
                    'volume_std': 0.0,
                    'volume_cv': 0.0,
                }

            variance = float(np.var(all_amounts, ddof=1))
            std = float(np.std(all_amounts, ddof=1))
            mean = float(np.mean(all_amounts))
            cv = std / max(mean, 1.0)

        all_amounts = [float(f['amount_usd_sum']) for f in flows]
        if len(all_amounts) >= 3:
            skewness = float(skew(all_amounts))
            kurt = float(kurtosis(all_amounts)) if len(all_amounts) >= 4 else 0.0
        else:
            skewness = 0.0
            kurt = 0.0

        return {
            'amount_variance': variance,
            'amount_skewness': skewness,
            'amount_kurtosis': kurt,
            'volume_std': std,
            'volume_cv': cv,
        }

    def _compute_flow_features(
        self,
        address: str,
        flows: List[Dict[str, Any]],
        aggregates: Dict[str, Any]
    ) -> Dict[str, Any]:
        if not flows:
            return {
                'flow_concentration': 0.0,
                'flow_diversity': 0.0,
                'counterparty_concentration': 0.0,
                'concentration_ratio': 0.0,
                'reciprocity_ratio': 0.0,
                'velocity_score': 0.0,
            }

        flow_amounts = [float(f['amount_usd_sum']) for f in flows]
        total_volume = sum(flow_amounts)

        if total_volume <= 0:
            return {
                'flow_concentration': 0.0,
                'flow_diversity': 0.0,
                'counterparty_concentration': 0.0,
                'concentration_ratio': 0.0,
                'reciprocity_ratio': 0.0,
                'velocity_score': 0.0,
            }

        counterparty_volumes: Dict[str, float] = defaultdict(float)
        for f in flows:
            cp = f['from_address'] if f['to_address'] == address else f['to_address']
            counterparty_volumes[cp] += float(f['amount_usd_sum'])

        total_in = sum(f['amount_usd_sum'] for f in flows if f['to_address'] == address)
        total_out = sum(f['amount_usd_sum'] for f in flows if f['from_address'] == address)
        total_vol = total_in + total_out

        tx_total = sum(f['tx_count'] for f in flows)

        reciprocity_stats = aggregates.get('reciprocity_stats', {})
        if reciprocity_stats.get('total_volume', 0) > 0:
            recip_total = float(reciprocity_stats.get('total_volume', 0))
            recip_vol = float(reciprocity_stats.get('reciprocal_volume', 0))
            reciprocity_ratio = min(recip_vol, recip_total) / max(recip_total, 1)
        else:
            reciprocity_ratio = float(min(total_in, total_out) / max(total_vol, 1))

        return {
            'flow_concentration': self._calculate_gini_coefficient(flow_amounts),
            'flow_diversity': self._calculate_normalized_entropy(flow_amounts),
            'counterparty_concentration': self._calculate_gini_coefficient(
                list(counterparty_volumes.values())
            ),
            'concentration_ratio': max(counterparty_volumes.values()) / total_volume
                if counterparty_volumes and total_volume > 0 else 0.0,
            'reciprocity_ratio': reciprocity_ratio,
            'velocity_score': float(min(tx_total / 100.0, 1.0)),
        }

    def _compute_directional_flow_features(
        self,
        address: str,
        flows: List[Dict[str, Any]]
    ) -> Dict[str, Any]:
        if not flows:
            return {
                'in_out_ratio': 0.5,
                'flow_asymmetry': 0.0,
                'dominant_flow_direction': 'balanced',
                'flow_direction_entropy': 0.0,
                'counterparty_overlap_ratio': 0.0,
            }

        total_in = sum(f['amount_usd_sum'] for f in flows if f['to_address'] == address)
        total_out = sum(f['amount_usd_sum'] for f in flows if f['from_address'] == address)

        senders = {f['from_address'] for f in flows if f['to_address'] == address}
        recipients = {f['to_address'] for f in flows if f['from_address'] == address}

        total_vol = total_in + total_out

        in_count = len([f for f in flows if f['to_address'] == address])
        out_count = len([f for f in flows if f['from_address'] == address])
        total_count = in_count + out_count

        if total_count > 0:
            p_in = in_count / total_count
            p_out = out_count / total_count
            dir_entropy = self._calculate_shannon_entropy([p_in, p_out])
        else:
            dir_entropy = 0.0

        if total_in > 1.5 * total_out:
            dominant = 'incoming'
        elif total_out > 1.5 * total_in:
            dominant = 'outgoing'
        else:
            dominant = 'balanced'

        return {
            'in_out_ratio': total_in / total_vol if total_vol > 0 else 0.5,
            'flow_asymmetry': abs(total_in - total_out) / total_vol if total_vol > 0 else 0.0,
            'dominant_flow_direction': dominant,
            'flow_direction_entropy': dir_entropy,
            'counterparty_overlap_ratio': len(senders & recipients) / max(len(senders | recipients), 1),
        }

    def _compute_behavioral_features(
        self,
        address: str,
        flows: List[Dict[str, Any]],
        aggregates: Dict[str, Any]
    ) -> Dict[str, Any]:
        temporal_patterns = aggregates.get('temporal_patterns', {})
        temporal_summaries = aggregates.get('temporal_summaries', {})
        behavioral_counters = aggregates.get('behavioral_counters', {})
        hourly_volumes = aggregates.get('hourly_volumes', [0.0] * 24)
        interevent_stats = aggregates.get('interevent_stats', {})
        reciprocity_stats = aggregates.get('reciprocity_stats', {})

        hourly_activity = temporal_patterns.get('hourly_activity', [0] * 24)
        daily_activity = temporal_patterns.get('daily_activity', [0] * 7)
        peak_activity_hour = temporal_patterns.get('peak_activity_hour', 0)
        peak_activity_day = temporal_patterns.get('peak_activity_day', 0)

        if len(hourly_activity) != 24:
            hourly_activity = (list(hourly_activity) + [0] * 24)[:24]
        if len(daily_activity) != 7:
            daily_activity = (list(daily_activity) + [0] * 7)[:7]

        first_timestamp = temporal_summaries.get('first_timestamp', 0)
        last_timestamp = temporal_summaries.get('last_timestamp', 0)
        total_tx_count = temporal_summaries.get('total_tx_count', 0)
        activity_days = temporal_summaries.get('distinct_activity_days', 1)
        total_volume = temporal_summaries.get('total_volume', 0.0)
        weekend_tx_count = temporal_summaries.get('weekend_tx_count', 0)
        night_tx_count = temporal_summaries.get('night_tx_count', 0)

        if not isinstance(hourly_volumes, list) or len(hourly_volumes) != 24:
            hourly_volumes = [0.0] * 24

        total_txs = sum(hourly_activity)
        hour_entropy = self._calculate_entropy(hourly_activity) if total_txs > 0 else 0.0
        daily_entropy = self._calculate_entropy(daily_activity) if total_txs > 0 else 0.0

        max_entropy = math.log2(24) if 24 > 1 else 1.0
        regularity_score = 1.0 - (hour_entropy / max_entropy) if hour_entropy > 0 else 1.0

        burst_factor = 0.0
        if total_txs > 0:
            max_hourly = max(hourly_activity)
            expected = total_txs / 24.0
            burst_factor = max_hourly / expected if expected > 0 else 0.0

        weekend_ratio = weekend_tx_count / max(total_tx_count, 1)
        night_ratio = night_tx_count / max(total_tx_count, 1)

        consistency_score = regularity_score * (1.0 - (hour_entropy / max_entropy)) if hour_entropy > 0 else regularity_score

        activity_span_days = 1
        if first_timestamp > 0 and last_timestamp > first_timestamp:
            activity_span_days = max(1, (last_timestamp - first_timestamp) // 86400000)

        avg_daily_volume = Decimal(str(total_volume / max(activity_days, 1)))

        total_tx_pos = behavioral_counters.get('total_tx_pos_amount', 0)
        round_number_count = behavioral_counters.get('round_number_count', 0)
        small_amount_count = behavioral_counters.get('small_amount_count', 0)
        unusual_tx_count = behavioral_counters.get('unusual_tx_count', 0)

        if total_tx_pos > 0:
            round_number_ratio = round_number_count / total_tx_pos
            unusual_timing_score = unusual_tx_count / total_tx_pos
            small_ratio = small_amount_count / total_tx_pos
            structuring_score = min(1.0, small_ratio * 1.5 if small_ratio > 0.5 and small_amount_count >= 3 else small_ratio)
        else:
            round_number_ratio = 0.0
            unusual_timing_score = 0.0
            structuring_score = 0.0

        non_zero_volumes = [v for v in hourly_volumes if v > 0]
        hourly_volume_variance = float(np.var(non_zero_volumes)) if len(non_zero_volumes) > 1 else 0.0
        peak_volume_hour = int(np.argmax(hourly_volumes)) if sum(hourly_volumes) > 0 else 0

        sum_volumes = sum(non_zero_volumes)
        intraday_volume_ratio = 0.0
        if sum_volumes > 0:
            max_vol = max(hourly_volumes)
            expected_vol = sum_volumes / 24.0
            intraday_volume_ratio = max_vol / expected_vol if expected_vol > 0 else 0.0

        hourly_tx_entropy = 0.0
        if total_txs > 0:
            probs = [c / total_txs for c in hourly_activity if c > 0]
            hourly_tx_entropy = self._calculate_shannon_entropy(probs) if probs else 0.0

        volume_concentration_score = self._calculate_gini_coefficient(non_zero_volumes)

        mean_inter_s = interevent_stats.get('mean_inter_s', 0.0)
        std_inter_s = interevent_stats.get('std_inter_s', 0.0)
        n_inter = interevent_stats.get('n', 0)

        flow_burstiness = 0.0
        if n_inter >= 2 and (mean_inter_s + std_inter_s) > 0:
            flow_burstiness = max(0.0, min(1.0, (std_inter_s - mean_inter_s) / (std_inter_s + mean_inter_s)))

        all_amounts = [float(f['amount_usd_sum']) for f in flows]
        amount_predictability = 0.0
        if len(all_amounts) > 1:
            mean_amt = np.mean(all_amounts)
            std_amt = np.std(all_amounts)
            cv = std_amt / max(mean_amt, 1.0)
            amount_predictability = max(0.0, 1.0 - min(1.0, cv))

        total_vol = float(reciprocity_stats.get('total_volume', 0))
        recip_vol = float(reciprocity_stats.get('reciprocal_volume', 0))
        flow_reciprocity_entropy = 0.0
        if total_vol > 0:
            p_rec = max(0.0, min(1.0, recip_vol / total_vol))
            p_non = 1.0 - p_rec
            ent = 0.0
            if p_rec > 0:
                ent -= p_rec * math.log2(p_rec)
            if p_non > 0:
                ent -= p_non * math.log2(p_non)
            flow_reciprocity_entropy = max(0.0, min(1.0, ent))

        return {
            'activity_days': activity_days,
            'activity_span_days': activity_span_days,
            'avg_daily_volume_usd': avg_daily_volume,
            'peak_hour': peak_activity_hour,
            'peak_day': peak_activity_day,
            'regularity_score': regularity_score,
            'burst_factor': burst_factor,
            'hourly_entropy': hour_entropy,
            'daily_entropy': daily_entropy,
            'weekend_transaction_ratio': weekend_ratio,
            'night_transaction_ratio': night_ratio,
            'consistency_score': consistency_score,
            'is_new_address': first_timestamp > 0,
            'is_dormant_reactivated': False,
            'round_number_ratio': round_number_ratio,
            'unusual_timing_score': unusual_timing_score,
            'structuring_score': structuring_score,
            'hourly_volume_variance': hourly_volume_variance,
            'peak_volume_hour': peak_volume_hour,
            'intraday_volume_ratio': intraday_volume_ratio,
            'hourly_transaction_entropy': hourly_tx_entropy,
            'volume_concentration_score': volume_concentration_score,
            'hourly_activity': [int(x) for x in hourly_activity],
            'daily_activity': [int(x) for x in daily_activity],
            'peak_activity_hour': peak_activity_hour,
            'peak_activity_day': peak_activity_day,
            'small_transaction_ratio': structuring_score,
            'flow_reciprocity_entropy': flow_reciprocity_entropy,
            'counterparty_stability': 0.0,
            'flow_burstiness': flow_burstiness,
            'transaction_regularity': regularity_score,
            'amount_predictability': amount_predictability,
            'unique_assets_in': 1,
            'unique_assets_out': 1,
            'dominant_asset_in': 'NATIVE',
            'dominant_asset_out': 'NATIVE',
            'asset_diversity_score': 0.0,
            'first_activity_timestamp': first_timestamp,
            'last_activity_timestamp': last_timestamp,
        }

    def _compute_edge_features(
        self,
        address: str,
        graph_data: GraphData
    ) -> Dict[str, Any]:
        """Compute edge-based features using GraphData."""
        graph = graph_data.graph

        # Get in and out edges
        in_edges = graph.in_edges(address)
        out_edges = graph.out_edges(address)

        all_edges = [(src, address) for src, _ in in_edges] + [(address, tgt) for tgt, _ in out_edges]

        if not all_edges:
            return {
                'avg_relationship_age_days': 0,
                'max_relationship_age_days': 0,
                'bidirectional_relationship_ratio': 0.0,
                'avg_edge_reciprocity': 0.0,
                'multi_asset_edge_ratio': 0.0,
                'edge_hourly_entropy': 0.0,
                'edge_weekly_entropy': 0.0,
            }

        relationship_ages = []
        bidirectional_count = 0
        reciprocity_scores = []
        multi_asset_count = 0
        aggregate_hourly = [0] * 24
        aggregate_weekly = [0] * 7

        for src, tgt in all_edges:
            first_seen = graph_data.get_edge_attr(src, tgt, 'first_seen_timestamp', 0)
            last_seen = graph_data.get_edge_attr(src, tgt, 'last_seen_timestamp', 0)

            if first_seen > 0:
                age_days = (last_seen - first_seen) / 86400000
                relationship_ages.append(age_days)

            if graph_data.get_edge_attr(src, tgt, 'is_bidirectional', False):
                bidirectional_count += 1

            reciprocity_scores.append(
                graph_data.get_edge_attr(src, tgt, 'reciprocity_ratio', 0.0)
            )

            if graph_data.get_edge_attr(src, tgt, 'unique_assets', 0) > 1:
                multi_asset_count += 1

            hourly = graph_data.get_edge_attr(src, tgt, 'hourly_pattern', [0] * 24)
            weekly = graph_data.get_edge_attr(src, tgt, 'weekly_pattern', [0] * 7)

            if isinstance(hourly, list) and len(hourly) == 24:
                for i in range(24):
                    aggregate_hourly[i] += hourly[i]
            if isinstance(weekly, list) and len(weekly) == 7:
                for i in range(7):
                    aggregate_weekly[i] += weekly[i]

        n_edges = len(all_edges)
        avg_relationship_age = sum(relationship_ages) / len(relationship_ages) if relationship_ages else 0
        max_relationship_age = max(relationship_ages) if relationship_ages else 0
        bidirectional_ratio = bidirectional_count / n_edges
        avg_reciprocity = sum(reciprocity_scores) / len(reciprocity_scores)
        multi_asset_ratio = multi_asset_count / n_edges

        hourly_entropy = self._calculate_entropy(aggregate_hourly)
        weekly_entropy = self._calculate_entropy(aggregate_weekly)

        return {
            'avg_relationship_age_days': int(avg_relationship_age),
            'max_relationship_age_days': int(max_relationship_age),
            'bidirectional_relationship_ratio': float(bidirectional_ratio),
            'avg_edge_reciprocity': float(avg_reciprocity),
            'multi_asset_edge_ratio': float(multi_asset_ratio),
            'edge_hourly_entropy': float(hourly_entropy),
            'edge_weekly_entropy': float(weekly_entropy),
        }

    def _compute_label_features(
        self,
        address: str,
        label_info: Dict[str, Any]
    ) -> Dict[str, Any]:
        address_type = label_info.get('address_type', 'UNKNOWN')
        trust_level = label_info.get('trust_level', 'UNKNOWN')

        return {
            'is_whale': address_type == 'WHALE',
            'is_exchange_like': address_type in ('EXCHANGE', 'DEX'),
            'is_mixer_like': address_type == 'MIXER',
            'is_hub': False,
            'is_dust_collector': False,
            'is_dormant_activated': False,
            'has_cycle_involvement': False,
            'has_layering_involvement': False,
            'data_quality_score': 1.0 if trust_level == 'VERIFIED' else 0.5,
            'feature_confidence_score': 1.0,
            'risk_proximity_1hop': 0.0,
            'risk_proximity_2hop': 0.0,
        }

    def _compute_all_graph_algorithms(
        self,
        graph_data: GraphData,
        dispatcher: AlgorithmDispatcher
    ) -> Dict[str, Dict[str, Any]]:
        """Compute graph algorithms using dispatcher (GPU/Rust routing)."""
        results: Dict[str, Dict[str, Any]] = defaultdict(
            lambda: self._empty_graph_features()
        )

        graph = graph_data.graph

        # Use dispatcher for centrality (routes to GPU)
        pagerank_scores = self._compute_pagerank(dispatcher)
        betweenness_scores = self._compute_betweenness_centrality(dispatcher)

        # Use Rust for other algorithms
        closeness_scores = self._compute_closeness_centrality(graph)
        kcore_scores = self._compute_kcore(graph)
        clustering_scores = self._compute_clustering_coefficient(graph)
        communities = self._compute_community_detection(graph_data, dispatcher)
        khop_features = self._compute_khop_features(graph_data, dispatcher)

        for address in graph.nodes():
            pagerank_val = pagerank_scores.get(address, 0.0)
            betweenness_val = betweenness_scores.get(address, 0.0)
            clustering_val = clustering_scores.get(address, 0.0)

            in_deg, out_deg = graph.degree_map().get(address, (0, 0))
            degree = in_deg + out_deg

            results[address].update({
                'pagerank': pagerank_val,
                'betweenness': betweenness_val,
                'closeness': closeness_scores.get(address, 0.0),
                'clustering_coefficient': clustering_val,
                'kcore': kcore_scores.get(address, 0),
                'community_id': communities.get(address, -1),
                'centrality_score': (
                    pagerank_val * 0.4 +
                    betweenness_val * 0.3 +
                    clustering_val * 0.3
                ),
                'degree': degree,
            })

            khop = khop_features.get(address, {})
            results[address].update({
                'khop1_count': khop.get('khop1_count', 0),
                'khop2_count': khop.get('khop2_count', 0),
                'khop3_count': khop.get('khop3_count', 0),
                'khop1_volume_usd': Decimal(str(khop.get('khop1_volume_usd', 0))),
                'khop2_volume_usd': Decimal(str(khop.get('khop2_volume_usd', 0))),
                'khop3_volume_usd': Decimal(str(khop.get('khop3_volume_usd', 0))),
            })

        logger.info(f"Graph analytics computed for {len(results)} addresses")
        return dict(results)

    def _compute_pagerank(
        self,
        dispatcher: AlgorithmDispatcher
    ) -> Dict[str, float]:
        """Compute PageRank using GPU dispatcher."""
        try:
            return dispatcher.pagerank(alpha=0.85)
        except Exception as e:
            logger.warning(f"PageRank failed: {e}, using default values")
            return {}

    def _compute_betweenness_centrality(
        self,
        dispatcher: AlgorithmDispatcher
    ) -> Dict[str, float]:
        """Compute betweenness centrality using GPU dispatcher."""
        try:
            return dispatcher.betweenness_centrality(k=1000)
        except Exception as e:
            logger.warning(f"Betweenness centrality failed: {e}")
            return {}

    def _compute_closeness_centrality(
        self,
        graph
    ) -> Dict[str, float]:
        """Compute closeness centrality using Rust."""
        try:
            return graph.closeness_centrality()
        except Exception as e:
            logger.warning(f"Closeness centrality failed: {e}")
            return {}

    def _compute_kcore(
        self,
        graph
    ) -> Dict[str, int]:
        """Compute k-core using Rust."""
        try:
            return graph.core_number()
        except Exception as e:
            logger.warning(f"K-core computation failed: {e}")
            return {}

    def _compute_clustering_coefficient(
        self,
        graph
    ) -> Dict[str, float]:
        """Compute clustering coefficient using Rust."""
        try:
            return graph.clustering_coefficient()
        except Exception as e:
            logger.warning(f"Clustering coefficient failed: {e}")
            return {}

    def _compute_community_detection(
        self,
        graph_data: GraphData,
        dispatcher: AlgorithmDispatcher
    ) -> Dict[str, int]:
        """Compute communities using GPU Louvain."""
        graph = graph_data.graph

        if graph.node_count() < 2:
            return {node: 0 for node in graph.nodes()}

        try:
            communities = dispatcher.louvain()
            # Convert list of sets to dict mapping node -> community_id
            result = {}
            for i, community in enumerate(communities):
                for node in community:
                    result[node] = i
            return result
        except Exception as e:
            logger.warning(f"Community detection failed: {e}")
            return {node: -1 for node in graph.nodes()}

    def _compute_khop_features(
        self,
        graph_data: GraphData,
        dispatcher: AlgorithmDispatcher,
        max_k: int = 3
    ) -> Dict[str, Dict[str, Any]]:
        """Compute k-hop neighborhood features using Rust BFS."""
        khop_results: Dict[str, Dict[str, Any]] = defaultdict(dict)

        graph = graph_data.graph
        volume_map = graph.volume_map()

        for node in graph.nodes():
            for k in range(1, max_k + 1):
                try:
                    # Use BFS to find neighbors within k hops
                    distances = dispatcher.bfs(node, cutoff=k)

                    # Get nodes at exactly k hops (not self)
                    neighbors = {n for n, d in distances.items() if d <= k and n != node}

                    # Sum neighbor volumes
                    neighbor_volume = 0.0
                    for n in neighbors:
                        in_vol, out_vol = volume_map.get(n, (0.0, 0.0))
                        neighbor_volume += in_vol + out_vol

                    khop_results[node][f'khop{k}_count'] = len(neighbors)
                    khop_results[node][f'khop{k}_volume_usd'] = neighbor_volume
                except Exception:
                    khop_results[node][f'khop{k}_count'] = 0
                    khop_results[node][f'khop{k}_volume_usd'] = 0.0

        return dict(khop_results)

    def _empty_graph_features(self) -> Dict[str, Any]:
        return {
            'pagerank': 0.0,
            'betweenness': 0.0,
            'closeness': 0.0,
            'clustering_coefficient': 0.0,
            'kcore': 0,
            'community_id': -1,
            'centrality_score': 0.0,
            'degree': 0,
            'khop1_count': 0,
            'khop2_count': 0,
            'khop3_count': 0,
            'khop1_volume_usd': Decimal('0'),
            'khop2_volume_usd': Decimal('0'),
            'khop3_volume_usd': Decimal('0'),
        }

    def _calculate_entropy(self, values: List[int]) -> float:
        total = sum(values)
        if total == 0:
            return 0.0
        return -sum(
            (v / total) * math.log2(v / total)
            for v in values if v > 0
        )

    def _calculate_shannon_entropy(self, probabilities: List[float]) -> float:
        return -sum(
            p * math.log2(p)
            for p in probabilities if p > 0
        )

    def _calculate_normalized_entropy(self, values: List[float]) -> float:
        if len(values) <= 1:
            return 0.0

        total = sum(values)
        if total <= 0:
            return 0.0

        probs = [v / total for v in values if v > 0]
        if not probs:
            return 0.0

        entropy = -sum(p * math.log2(p) for p in probs if p > 0)
        max_entropy = math.log2(len(probs)) if len(probs) > 1 else 1.0

        return entropy / max_entropy if max_entropy > 0 else 0.0

    def _calculate_gini_coefficient(self, values: List[float]) -> float:
        if not values:
            return 0.0

        values = sorted([v for v in values if v > 0])
        n = len(values)
        if n <= 1:
            return 0.0

        total = sum(values)
        if total <= 0:
            return 0.0

        cumsum = 0.0
        for i, v in enumerate(values):
            cumsum += (i + 1) * v

        return 1.0 - (2.0 * cumsum) / (n * total) + (n + 1) / n
