"""
Flow Validator for pattern verification.

Validates patterns by tracing actual flows in transfers data.
Ported from subnet's scoring_manager.py with simplified interface.
"""

from dataclasses import dataclass, field
from typing import List, Set, Tuple, Dict, Any, Optional
import pandas as pd
import numpy as np
from loguru import logger


@dataclass
class FlowValidationDetail:
    """Detailed result of a single flow validation check."""
    from_address: str
    to_address: str
    flow_exists: bool
    matching_transfers: int = 0


@dataclass
class PatternValidationDetail:
    """Detailed validation result for a single pattern."""
    pattern_idx: int
    pattern_id: str
    pattern_type: str
    addresses_extracted: List[str] = field(default_factory=list)
    address_source_field: str = ""
    flow_validations: List[FlowValidationDetail] = field(default_factory=list)
    is_synthetic: bool = False
    is_novelty_valid: bool = False
    is_novelty_invalid: bool = False
    failure_reason: str = ""
    error: Optional[str] = None


@dataclass
class ValidationResult:
    """Results from validating all reported patterns."""

    # Synthetic Patterns (from ground_truth)
    synthetic_addresses_expected: int  # Total addresses in ground_truth
    synthetic_addresses_found: int     # Correctly detected

    # Novelty Patterns
    novelty_valid: int      # Verified via flow tracing
    novelty_invalid: int    # Fake - flows don't exist

    # Totals
    total_reported: int     # All patterns reported

    # Per-pattern-type breakdown
    per_pattern_type: Dict[str, Dict[str, int]] = field(default_factory=dict)

    # Detailed validation records (optional)
    validation_details: List[PatternValidationDetail] = field(default_factory=list)

    def get_invalid_patterns(self) -> List[PatternValidationDetail]:
        """Get all patterns that failed validation."""
        return [d for d in self.validation_details if d.is_novelty_invalid]


class FlowValidator:
    """
    Validates patterns by tracing flows in transfer data.

    Pattern types have different validation requirements:
    - Sequential (cycle, layering_path): A->B->C flows must exist
    - Star (motif_fanin/fanout): center<->spoke flows must exist
    - Address-based (burst, proximity, threshold): addresses must exist
    """

    # Address field priority order
    ADDRESS_FIELDS = [
        ('addresses_involved', 'baseline primary'),
        ('addresses', 'legacy'),
        ('address_path', 'path'),
        ('cycle_path', 'cycle'),
        ('layering_path', 'layering'),
        ('network_members', 'network'),
        ('burst_address', 'burst'),
    ]
    FALLBACK_FIELDS = ['address', 'source_address', 'target_address', 'primary_address', 'risk_source_address']

    def __init__(self):
        pass

    def validate_patterns(
        self,
        patterns_df: pd.DataFrame,
        transfers_df: pd.DataFrame,
        ground_truth_df: pd.DataFrame,
        debug: bool = False
    ) -> ValidationResult:
        """
        Validate all reported patterns using flow tracing.

        Args:
            patterns_df: Detected patterns with addresses
            transfers_df: All transactions in the test window
            ground_truth_df: Expected patterns (address-level)
            debug: Enable detailed debug logging

        Returns:
            ValidationResult with counts for each category
        """
        # Get addresses from ground_truth
        gt_addresses = set(ground_truth_df['address'].unique())

        synthetic_found_addresses: Set[str] = set()
        novelty_valid_count = 0
        novelty_invalid_count = 0

        # Per-pattern-type tracking
        per_pattern_type: Dict[str, Dict[str, int]] = {}

        validation_details: List[PatternValidationDetail] = []

        if debug:
            logger.info(
                f"Starting pattern validation: {len(patterns_df)} patterns, "
                f"{len(gt_addresses)} ground truth addresses, {len(transfers_df)} transfers"
            )

        # Process each pattern
        for pattern_idx, pattern in patterns_df.iterrows():
            detail = PatternValidationDetail(
                pattern_idx=pattern_idx,
                pattern_id=str(pattern.get('pattern_id', 'unknown')),
                pattern_type=str(pattern.get('pattern_type', 'unknown')),
            )

            pattern_type = detail.pattern_type
            if pattern_type not in per_pattern_type:
                per_pattern_type[pattern_type] = {
                    'detected': 0, 'synthetic': 0, 'novelty_valid': 0, 'novelty_invalid': 0
                }
            per_pattern_type[pattern_type]['detected'] += 1

            try:
                # Extract addresses from pattern
                pattern_addresses = self._extract_addresses(pattern)
                detail.addresses_extracted = pattern_addresses

                if len(pattern_addresses) == 0:
                    detail.is_novelty_invalid = True
                    detail.failure_reason = "NO_ADDRESSES_EXTRACTED"
                    novelty_invalid_count += 1
                    per_pattern_type[pattern_type]['novelty_invalid'] += 1
                    validation_details.append(detail)
                    continue

                # Validate flows based on pattern type
                flows_valid, flow_details = self._verify_pattern_by_type(
                    pattern_type, pattern_addresses, transfers_df
                )
                detail.flow_validations = flow_details

                if not flows_valid:
                    detail.is_novelty_invalid = True
                    failed_flows = [f for f in flow_details if not f.flow_exists]
                    detail.failure_reason = f"FLOW_NOT_FOUND: {len(failed_flows)} missing flows"
                    novelty_invalid_count += 1
                    per_pattern_type[pattern_type]['novelty_invalid'] += 1
                    validation_details.append(detail)
                    continue

                # Check if addresses are in ground_truth
                pattern_gt_addresses = set(pattern_addresses) & gt_addresses
                if len(pattern_gt_addresses) > 0:
                    synthetic_found_addresses.update(pattern_gt_addresses)
                    detail.is_synthetic = True
                    per_pattern_type[pattern_type]['synthetic'] += 1
                else:
                    detail.is_novelty_valid = True
                    novelty_valid_count += 1
                    per_pattern_type[pattern_type]['novelty_valid'] += 1

                validation_details.append(detail)

            except Exception as e:
                detail.is_novelty_invalid = True
                detail.failure_reason = f"EXCEPTION: {type(e).__name__}"
                detail.error = str(e)
                novelty_invalid_count += 1
                per_pattern_type[pattern_type]['novelty_invalid'] += 1
                validation_details.append(detail)

                if debug:
                    logger.error(f"Pattern validation exception: {e}")
                continue

        result = ValidationResult(
            synthetic_addresses_expected=len(gt_addresses),
            synthetic_addresses_found=len(synthetic_found_addresses),
            novelty_valid=novelty_valid_count,
            novelty_invalid=novelty_invalid_count,
            total_reported=len(patterns_df),
            per_pattern_type=per_pattern_type,
            validation_details=validation_details,
        )

        if debug:
            logger.info(
                f"Validation complete: synthetic={result.synthetic_addresses_found}/{result.synthetic_addresses_expected}, "
                f"novelty_valid={result.novelty_valid}, novelty_invalid={result.novelty_invalid}"
            )

        return result

    def _extract_addresses(self, pattern: pd.Series) -> List[str]:
        """Extract addresses from a pattern row."""

        def extract_from_value(val) -> List[str]:
            """Safely extract addresses from any type of value."""
            if val is None:
                return []
            if isinstance(val, np.ndarray):
                return val.tolist()
            if isinstance(val, (list, tuple)):
                return list(val)
            if isinstance(val, str):
                return [s.strip() for s in val.split(',') if s.strip()]
            if hasattr(val, 'tolist'):
                return val.tolist()
            return [str(val)]

        # Try each address field in priority order
        for field_name, _ in self.ADDRESS_FIELDS:
            if field_name in pattern:
                val = pattern[field_name]
                if isinstance(val, (list, np.ndarray)):
                    if len(val) > 0:
                        addrs = extract_from_value(val)
                        if addrs:
                            return addrs
                elif pd.notna(val):
                    addrs = extract_from_value(val)
                    if addrs:
                        return addrs

        # Fallback: single address columns
        pattern_addresses = []
        for col in self.FALLBACK_FIELDS:
            if col in pattern:
                val = pattern[col]
                if pd.notna(val):
                    pattern_addresses.append(str(val))

        return pattern_addresses

    def _verify_pattern_by_type(
        self,
        pattern_type: str,
        addresses: List[str],
        transfers_df: pd.DataFrame
    ) -> Tuple[bool, List[FlowValidationDetail]]:
        """Route to appropriate validation based on pattern type."""

        # Sequential path patterns
        if pattern_type in ['cycle', 'layering_path']:
            return self._verify_sequential_flows(addresses, transfers_df)

        # Star patterns
        elif pattern_type in ['motif_fanin', 'motif_fanout']:
            return self._verify_star_pattern(addresses, transfers_df, pattern_type)

        # Address-based patterns (no flow verification needed)
        elif pattern_type in ['temporal_burst', 'proximity_risk', 'threshold_evasion', 'smurfing_network']:
            all_exist = self._verify_addresses_exist(addresses, transfers_df)
            return all_exist, []

        else:
            # Unknown pattern type - default to existence check
            all_exist = self._verify_addresses_exist(addresses, transfers_df)
            return all_exist, []

    def _verify_sequential_flows(
        self,
        addresses: List[str],
        transfers_df: pd.DataFrame
    ) -> Tuple[bool, List[FlowValidationDetail]]:
        """Verify sequential flows (A->B->C->D)."""
        flow_details: List[FlowValidationDetail] = []
        all_valid = True

        if len(addresses) < 2:
            return False, []

        for i in range(len(addresses) - 1):
            from_addr = addresses[i]
            to_addr = addresses[i + 1]

            matching = transfers_df[
                (transfers_df['from_address'] == from_addr) &
                (transfers_df['to_address'] == to_addr)
            ]
            match_count = len(matching)
            flow_exists = match_count > 0

            flow_details.append(FlowValidationDetail(
                from_address=from_addr,
                to_address=to_addr,
                flow_exists=flow_exists,
                matching_transfers=match_count
            ))

            if not flow_exists:
                all_valid = False

        return all_valid, flow_details

    def _verify_star_pattern(
        self,
        addresses: List[str],
        transfers_df: pd.DataFrame,
        pattern_type: str
    ) -> Tuple[bool, List[FlowValidationDetail]]:
        """Verify star topology: center connected to all spokes."""
        flow_details: List[FlowValidationDetail] = []
        all_valid = True

        if len(addresses) < 2:
            return False, []

        center = addresses[0]
        spokes = addresses[1:]

        for spoke in spokes:
            if pattern_type == 'motif_fanout':
                from_addr, to_addr = center, spoke
            else:  # motif_fanin
                from_addr, to_addr = spoke, center

            matching = transfers_df[
                (transfers_df['from_address'] == from_addr) &
                (transfers_df['to_address'] == to_addr)
            ]
            match_count = len(matching)
            flow_exists = match_count > 0

            flow_details.append(FlowValidationDetail(
                from_address=from_addr,
                to_address=to_addr,
                flow_exists=flow_exists,
                matching_transfers=match_count
            ))

            if not flow_exists:
                all_valid = False

        return all_valid, flow_details

    def _verify_addresses_exist(
        self,
        addresses: List[str],
        transfers_df: pd.DataFrame
    ) -> bool:
        """Verify addresses appear in transfers (not requiring specific flows)."""
        all_addresses = set(transfers_df['from_address']).union(set(transfers_df['to_address']))
        return all(addr in all_addresses for addr in addresses)
