"""Rule-based scorers that check patterns and structure."""

import re
import json
from typing import Optional, List, Union

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


class RegexMatch(Scorer):
    r"""
    Checks if output matches a regex pattern.
    
    Examples:
        >>> # Check if output contains a number
        >>> scorer = RegexMatch(pattern=r'\d+')
        >>> run = Run(id="1", input="test", output="Answer: 42")
        >>> score = scorer.score(run)
        >>> score.value
        True
    """
    
    name = "regex_match"
    requires_reference = False
    cacheable = False

    def __init__(self, pattern: str, flags: int = 0):
        """
        Initialize regex matcher.
        
        Args:
            pattern: Regex pattern to match
            flags: Regex flags (e.g., re.IGNORECASE)
        """
        self.pattern = pattern
        self.flags = flags
        self.compiled_pattern = re.compile(pattern, flags)
        # Update name to include pattern
        self.name = f"regex_match_{pattern[:20]}"
    
    def score(self, run: Run) -> Score:
        """
        Check if output matches pattern.
        
        Args:
            run: Run to score
            
        Returns:
            Score with True if pattern found, False otherwise
        """
        output_str = str(run.output)
        match = self.compiled_pattern.search(output_str)
        
        return Score(
            value=bool(match),
            reason=f"Pattern {'found' if match else 'not found'}",
            metadata={
                "pattern": self.pattern,
                "match_position": match.span() if match else None
            }
        )


class JSONValid(Scorer):
    """
    Validates that output is valid JSON.
    
    Examples:
        >>> scorer = JSONValid()
        >>> run = Run(id="1", input="test", output='{"key": "value"}')
        >>> score = scorer.score(run)
        >>> score.value
        True
        
        >>> run2 = Run(id="2", input="test", output='not json')
        >>> score2 = scorer.score(run2)
        >>> score2.value
        False
    """
    
    name = "json_valid"
    requires_reference = False
    cacheable = False
    
    def __init__(self, expected_schema: Optional[dict] = None):
        """
        Initialize JSON validator.
        
        Args:
            expected_schema: Optional schema to validate against (future)
        """
        self.expected_schema = expected_schema
    
    def score(self, run: Run) -> Score:
        """
        Validate JSON structure.
        
        Args:
            run: Run to score
            
        Returns:
            Score with True if valid JSON, False otherwise
        """
        output_str = str(run.output)
        
        try:
            parsed = json.loads(output_str)
            is_valid = True
            reason = "Valid JSON"
            metadata = {"type": type(parsed).__name__}
        except json.JSONDecodeError as e:
            is_valid = False
            reason = f"Invalid JSON: {str(e)}"
            metadata = {"error": str(e)}
        
        return Score(
            value=is_valid,
            reason=reason,
            metadata=metadata
        )



class ContainsKeywords(Scorer):
    """
    Checks if output contains required keywords.
    
    Examples:
        >>> scorer = ContainsKeywords(keywords=["paris", "france"])
        >>> run = Run(id="1", input="capital?", output="Paris is in France")
        >>> score = scorer.score(run)
        >>> score.value
        True
    """

    name = "contains_keywords"
    requires_reference = False
    cacheable = False

    def __init__(self, keywords: List[str], ignore_case: bool = False):
        """
        Args:
            keywords: List of keywords to check for
            ignore_case: Whether to match case
        """
        
        self.keywords = keywords
        self.ignore_case = ignore_case

        # Add keywords to name. Truncate to keep name reasonable
        keywords_str = '_'.join(self.keywords[:3])  # First 3 keywords
        if len(self.keywords) > 3:
             keywords_str += f"_and_{len(self.keywords) - 3}_more"

        self.name = f"contains_{keywords_str}"

    
    def score(self, run: Run) -> Score:
        """Check if all keywords present."""

        output_str = str(run.output)
        if self.ignore_case:
            output_str = output_str.lower()
            keywords = [k.lower() for k in self.keywords]
        else:
            keywords = self.keywords

        found = [kw for kw in keywords if kw in output_str]
        all_found = len(found) == len(keywords)

        return Score(
            value=all_found,
            reason=f"Found {len(found)}/{len(keywords)} keywords",
            metadata={
                "found": found,
                "missing": [kw for kw in keywords if kw not in found]
            }
        )


class StartsWith(Scorer):
    """
    Checks if output starts with a given prefix.

    Examples:
        >>> scorer = StartsWith(prefix="Answer:")
        >>> run = Run(id="1", input="q", output="Answer: 42")
        >>> score = scorer.score(run)
        >>> score.value
        True
    """

    name = "starts_with"
    requires_reference = False
    cacheable = False

    def __init__(self, prefix: str, ignore_case: bool = False):
        """
        Args:
            prefix: The prefix string to check for.
            ignore_case: Whether to ignore case when matching.
        """
        self.prefix = prefix
        self.ignore_case = ignore_case
        self.name = f"starts_with_{prefix[:20]}"

    def score(self, run: Run) -> Score:
        """Check if output starts with prefix."""
        output_str = str(run.output)
        prefix = self.prefix

        if self.ignore_case:
            output_str = output_str.lower()
            prefix = prefix.lower()

        matches = output_str.startswith(prefix)

        return Score(
            value=matches,
            reason=f"Output {'starts' if matches else 'does not start'} with '{self.prefix}'",
            metadata={"prefix": self.prefix, "ignore_case": self.ignore_case},
        )


class EndsWith(Scorer):
    """
    Checks if output ends with a given suffix.

    Examples:
        >>> scorer = EndsWith(suffix=".")
        >>> run = Run(id="1", input="q", output="The answer is 42.")
        >>> score = scorer.score(run)
        >>> score.value
        True
    """

    name = "ends_with"
    requires_reference = False
    cacheable = False

    def __init__(self, suffix: str, ignore_case: bool = False):
        """
        Args:
            suffix: The suffix string to check for.
            ignore_case: Whether to ignore case when matching.
        """
        self.suffix = suffix
        self.ignore_case = ignore_case
        self.name = f"ends_with_{suffix[:20]}"

    def score(self, run: Run) -> Score:
        """Check if output ends with suffix."""
        output_str = str(run.output)
        suffix = self.suffix

        if self.ignore_case:
            output_str = output_str.lower()
            suffix = suffix.lower()

        matches = output_str.endswith(suffix)

        return Score(
            value=matches,
            reason=f"Output {'ends' if matches else 'does not end'} with '{self.suffix}'",
            metadata={"suffix": self.suffix, "ignore_case": self.ignore_case},
        )


class LengthCheck(Scorer):
    """
    Validates that output length is within specified bounds.

    Examples:
        >>> scorer = LengthCheck(min_length=10, max_length=500)
        >>> run = Run(id="1", input="q", output="Hello world, this is a test.")
        >>> score = scorer.score(run)
        >>> score.value
        True
    """

    name = "length_check"
    requires_reference = False
    cacheable = False

    def __init__(
        self,
        min_length: int = 0,
        max_length: Optional[int] = None,
    ):
        """
        Args:
            min_length: Minimum allowed length (inclusive). Default: 0.
            max_length: Maximum allowed length (inclusive). None means no upper limit.
        """
        self.min_length = min_length
        self.max_length = max_length

    def score(self, run: Run) -> Score:
        """Check if output length is within bounds."""
        output_str = str(run.output)
        length = len(output_str)

        above_min = length >= self.min_length
        below_max = self.max_length is None or length <= self.max_length
        in_range = above_min and below_max

        if not above_min:
            reason = f"Too short: {length} < {self.min_length}"
        elif not below_max:
            reason = f"Too long: {length} > {self.max_length}"
        else:
            reason = f"Length {length} is within bounds"

        return Score(
            value=in_range,
            reason=reason,
            metadata={
                "actual_length": length,
                "min_length": self.min_length,
                "max_length": self.max_length,
            },
        )





