"""
Scorer for evaluation metrics.

Calculates recall, novelty, and performance scores.
Simplified from subnet's scoring_manager without anti-cheat gates.
"""

from dataclasses import dataclass, field
from typing import Dict, Optional
import math

from .validator import ValidationResult


@dataclass
class PatternTypeMetrics:
    """Metrics for a single pattern type."""
    detected: int = 0
    recall: float = 0.0
    novelty_valid: int = 0


@dataclass
class EvalScore:
    """Complete evaluation score result."""

    # Summary scores (0.0 to 1.0)
    final_score: float
    recall_score: float
    novelty_score: float
    performance_score: float

    # Counts
    total_patterns: int
    synthetic_found: int
    synthetic_expected: int
    novelty_valid: int
    novelty_invalid: int

    # Timing
    total_seconds: float
    detector_timing: Dict[str, float] = field(default_factory=dict)

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

    # Comparison to baseline (if available)
    baseline_comparison: Optional[Dict[str, float]] = None


class EvalScorer:
    """
    Calculates evaluation scores for pattern detection results.

    Scoring formula:
    - 50% recall (synthetic patterns found / expected)
    - 25% novelty (valid novel patterns, capped at 50% of synthetic count)
    - 25% performance (based on execution time)
    """

    # Scoring weights
    RECALL_WEIGHT = 0.50
    NOVELTY_WEIGHT = 0.25
    PERFORMANCE_WEIGHT = 0.25

    # Novelty cap (prevents gaming by reporting excessive novelties)
    NOVELTY_CAP_RATIO = 0.5  # Cap at 50% of synthetic count

    # Performance scoring parameters
    BASELINE_TIME = 120.0   # seconds - baseline reference time
    MAX_TIME = 600.0        # seconds - beyond this, performance score = 0

    def __init__(
        self,
        recall_weight: float = 0.50,
        novelty_weight: float = 0.25,
        performance_weight: float = 0.25,
        novelty_cap_ratio: float = 0.5,
        baseline_time: float = 120.0,
        max_time: float = 600.0,
    ):
        """
        Initialize scorer with configurable weights.

        Args:
            recall_weight: Weight for recall score (default 50%)
            novelty_weight: Weight for novelty score (default 25%)
            performance_weight: Weight for performance score (default 25%)
            novelty_cap_ratio: Cap for novelty credit as ratio of synthetic count
            baseline_time: Baseline time for performance scoring
            max_time: Maximum time before performance score = 0
        """
        self.recall_weight = recall_weight
        self.novelty_weight = novelty_weight
        self.performance_weight = performance_weight
        self.novelty_cap_ratio = novelty_cap_ratio
        self.baseline_time = baseline_time
        self.max_time = max_time

        # Validate weights sum to 1.0
        total_weight = recall_weight + novelty_weight + performance_weight
        if abs(total_weight - 1.0) > 0.001:
            raise ValueError(f"Weights must sum to 1.0, got {total_weight}")

    def calculate_score(
        self,
        validation_result: ValidationResult,
        total_time_seconds: float,
        detector_timing: Dict[str, float],
        baseline_score: Optional['EvalScore'] = None,
    ) -> EvalScore:
        """
        Calculate complete evaluation score.

        Args:
            validation_result: Results from FlowValidator
            total_time_seconds: Total execution time
            detector_timing: Per-detector timing breakdown
            baseline_score: Optional baseline score for comparison

        Returns:
            EvalScore with all metrics
        """
        # Calculate component scores
        recall_score = self._calculate_recall(
            validation_result.synthetic_addresses_found,
            validation_result.synthetic_addresses_expected
        )

        novelty_score = self._calculate_novelty(
            validation_result.novelty_valid,
            validation_result.synthetic_addresses_expected
        )

        performance_score = self._calculate_performance(total_time_seconds)

        # Calculate final weighted score
        final_score = (
            self.recall_weight * recall_score +
            self.novelty_weight * novelty_score +
            self.performance_weight * performance_score
        )

        # Build per-pattern-type metrics
        per_pattern_type = {}
        for ptype, counts in validation_result.per_pattern_type.items():
            detected = counts.get('detected', 0)
            synthetic = counts.get('synthetic', 0)
            novelty_valid = counts.get('novelty_valid', 0)

            # Calculate per-type recall (synthetic found / total detected as synthetic)
            # This is an approximation since we don't have per-type expected counts
            per_pattern_type[ptype] = PatternTypeMetrics(
                detected=detected,
                recall=synthetic / detected if detected > 0 else 0.0,
                novelty_valid=novelty_valid,
            )

        # Build comparison to baseline if available
        baseline_comparison = None
        if baseline_score is not None:
            baseline_comparison = {
                'final_score_delta': final_score - baseline_score.final_score,
                'recall_delta': recall_score - baseline_score.recall_score,
                'novelty_delta': novelty_score - baseline_score.novelty_score,
                'performance_delta': performance_score - baseline_score.performance_score,
            }

        return EvalScore(
            final_score=final_score,
            recall_score=recall_score,
            novelty_score=novelty_score,
            performance_score=performance_score,
            total_patterns=validation_result.total_reported,
            synthetic_found=validation_result.synthetic_addresses_found,
            synthetic_expected=validation_result.synthetic_addresses_expected,
            novelty_valid=validation_result.novelty_valid,
            novelty_invalid=validation_result.novelty_invalid,
            total_seconds=total_time_seconds,
            detector_timing=detector_timing,
            per_pattern_type=per_pattern_type,
            baseline_comparison=baseline_comparison,
        )

    def _calculate_recall(self, found: int, expected: int) -> float:
        """
        Calculate recall score.

        Returns: Score 0.0 to 1.0
        """
        if expected == 0:
            return 1.0  # No synthetics = perfect recall
        return found / expected

    def _calculate_novelty(self, novelty_valid: int, synthetic_expected: int) -> float:
        """
        Calculate novelty discovery score.

        Capped to prevent gaming by reporting excessive novelties.

        Returns: Score 0.0 to 1.0
        """
        if synthetic_expected == 0:
            return 0.0  # No novelty scoring if no synthetics

        max_novelty_credit = int(synthetic_expected * self.novelty_cap_ratio)
        if max_novelty_credit == 0:
            return 0.0

        credited_novelty = min(novelty_valid, max_novelty_credit)
        return credited_novelty / max_novelty_credit

    def _calculate_performance(self, execution_time: float) -> float:
        """
        Calculate performance score based on execution time.

        Uses a sigmoid-like curve where:
        - Faster than baseline -> score > 0.5
        - Slower than baseline -> score < 0.5
        - At or above max_time -> score = 0.0

        Returns: Score 0.0 to 1.0
        """
        if execution_time >= self.max_time:
            return 0.0

        if execution_time <= 0:
            return 1.0

        # Sigmoid-like ratio
        ratio = self.baseline_time / execution_time
        return min(1.0, max(0.0, ratio / (1 + ratio)))
