from openevalkit.run import Run
from openevalkit.scorers.base import Scorer
from openevalkit.score import Score
from openevalkit.errors import ScoringError


"""Performance scorers that measure execution characteristics like latency and cost."""

class Latency(Scorer):
    """
    Measures response latency from run metrics.

    Require 'latency' field in run.metrics

    Examples:
        >>> scorer = Latency()
        >>> run = Run(
        ...     id="1",
        ...     input="test",
        ...     output="response",
        ...     metrics={"latency": 0.5}
        ... )
        >>> score = scorer.score(run)
        >>> score.value
        0.5
    """

    name = "latency"
    requires_reference = False
    cacheable = False

    def score(self, run: Run) -> Score:
        if run.metrics is None or 'latency' not in run.metrics:
            raise ScoringError(f"{self.name} requires 'latency' in run.metrics."
                               f"{run.id} has no latency.")
        
        latency = run.metrics['latency']
        return Score(
            value=latency,
            reason=f"Response took {latency:.3f}s",
            metadata={"unit": "seconds"}
            )


class Cost(Scorer):
    """
    Measures API cost from run metrics.
    
    Requires 'cost' field in run.metrics.
    
    Examples:
        >>> scorer = Cost()
        >>> run = Run(
        ...     id="1",
        ...     input="test",
        ...     output="response",
        ...     metrics={"cost": 0.0001}
        ... )
        >>> score = scorer.score(run)
        >>> score.value
        0.0001
    """
    
    name = "cost"
    requires_reference = False
    cacheable = False
    
    def score(self, run: Run) -> Score:
        """
        Extract cost from run metrics.
        
        Args:
            run: Run to score
            
        Returns:
            Score with cost value in dollars
            
        Raises:
            ScoringError: If cost not in metrics
        """
        if run.metrics is None or 'cost' not in run.metrics:
            raise ScoringError(
                f"{self.name} requires 'cost' in run.metrics. "
                f"Run {run.id} has no cost."
            )
        
        cost = run.metrics['cost']
        
        return Score(
            value=cost,
            reason=f"Cost ${cost:.6f}",
            metadata={"unit": "USD"}
        )


class TokenCount(Scorer):
    """
    Counts tokens in output.
    
    Can use either run.metrics['tokens'] or estimate from text length.
    
    Examples:
        >>> scorer = TokenCount()
        >>> run = Run(
        ...     id="1",
        ...     input="test",
        ...     output="Hello world",
        ...     metrics={"tokens": 2}
        ... )
        >>> score = scorer.score(run)
        >>> score.value
        2
    """
    
    name = "token_count"
    requires_reference = False
    cacheable = False
    
    def __init__(self, estimate_if_missing: bool = True):
        """
        Args:
            estimate_if_missing: If True, estimate tokens from length when not in metrics
        """
        self.estimate_if_missing = estimate_if_missing
    
    def score(self, run: Run) -> Score:
        """Extract or estimate token count."""
        
        # Try to get from metrics first
        if run.metrics and 'tokens' in run.metrics:
            tokens = run.metrics['tokens']
            method = "exact"
        elif self.estimate_if_missing:
            # Rough estimate: ~4 chars per token
            output_str = str(run.output)
            tokens = len(output_str) / 4
            method = "estimated"
        else:
            raise ScoringError(
                f"{self.name} requires 'tokens' in run.metrics or set  estimate_if_missing=True"
                f"Run {run.id} has no token count."
            )
        
        return Score(
            value=tokens,
            reason=f"{int(tokens)} tokens ({method})",
            metadata={"method": method}
        )