from abc import ABC, abstractmethod
from typing import List, Dict
import numpy as np

from openevalkit.run import Run
from openevalkit.score import Score

class Scorer(ABC):
    """
    Base class for all scorers.

    Scorers evaluate runs and return scores.
    Subclasses must implement score() method.
    """

    name: str
    requires_reference: bool = False # Subclasses declare if they need reference
    cacheable: bool = False # Whether a score is cacheable

    @abstractmethod
    def score(self, run: Run) -> Score:
        """
        Score a single run
        
        Args:
            run: The run to score
        
        Returns:
            Score object with value, reason, metadata
        
        Raises:
            ScoringError: If scoring fails
        """

        raise NotImplementedError()
    
    def aggregate(self, scores: List[Score]) -> Dict[str, float]:
        """
        Aggregates multiple scores into summary statistics.
        
        Default implementation:
        - Boolean scores -> accuracy (% true)
        - Numeric scores -> statistics (mean, median, std)

        Override for custom aggregation logic

        Returns:
            Dict of metric name -> aggregate_value 
        """

        if not scores:
            return {}
        
        values = [score.value for score in scores]

        # Boolean scores -> accuracy
        if isinstance(values[0], bool):
            accuracy = sum(values) / len(values)
            return {f"{self.name}": accuracy}
        
        # Numeric scores -> statistics
        elif isinstance(values[0], (int, float)):
            return {
                f"{self.name}_mean": np.mean(values),
                f"{self.name}_median": np.median(values),
                f"{self.name}_std": np.std(values)
            }
    
        # Other types → just count
        else:
            return {f"{self.name}_count": len(values)}

     