"""Helper types for validating model temperature parameters."""

from abc import ABC, abstractmethod
from typing import Optional

__all__ = [
    "TemperatureConstraint",
    "FixedTemperatureConstraint",
    "RangeTemperatureConstraint",
    "DiscreteTemperatureConstraint",
]

# Common heuristics for determining temperature support when explicit
# capabilities are unavailable (e.g., custom/local models).
_TEMP_UNSUPPORTED_PATTERNS = {
    "o1",
    "o3",
    "o4",  # OpenAI O-series reasoning models
    "deepseek-reasoner",
    "deepseek-r1",
    "r1",  # DeepSeek reasoner variants
}

_TEMP_UNSUPPORTED_KEYWORDS = {
    "reasoner",  # Catch additional DeepSeek-style naming patterns
}


class TemperatureConstraint(ABC):
    """Contract for temperature validation used by `ModelCapabilities`.

    Concrete providers describe their temperature behaviour by creating
    subclasses that expose three operations:
    * `validate` – decide whether a requested temperature is acceptable.
    * `get_corrected_value` – coerce out-of-range values into a safe default.
    * `get_description` – provide a human readable error message for users.

    Providers call these hooks before sending traffic to the underlying API so
    that unsupported temperatures never reach the remote service.
    """

    @abstractmethod
    def validate(self, temperature: float) -> bool:
        """Return ``True`` when the temperature may be sent to the backend."""

    @abstractmethod
    def get_corrected_value(self, temperature: float) -> float:
        """Return a valid substitute for an out-of-range temperature."""

    @abstractmethod
    def get_description(self) -> str:
        """Describe the acceptable range to include in error messages."""

    @abstractmethod
    def get_default(self) -> float:
        """Return the default temperature for the model."""

    @staticmethod
    def infer_support(model_name: str) -> tuple[bool, str]:
        """Heuristically determine whether a model supports temperature."""

        model_lower = model_name.lower()

        for pattern in _TEMP_UNSUPPORTED_PATTERNS:
            conditions = (
                pattern == model_lower,
                model_lower.startswith(f"{pattern}-"),
                model_lower.startswith(f"openai/{pattern}"),
                model_lower.startswith(f"deepseek/{pattern}"),
                model_lower.endswith(f"-{pattern}"),
                f"/{pattern}" in model_lower,
                f"-{pattern}-" in model_lower,
            )
            if any(conditions):
                return False, f"detected pattern '{pattern}'"

        for keyword in _TEMP_UNSUPPORTED_KEYWORDS:
            if keyword in model_lower:
                return False, f"detected keyword '{keyword}'"

        return True, "default assumption for models without explicit metadata"

    @staticmethod
    def resolve_settings(
        model_name: str,
        constraint_hint: Optional[str] = None,
    ) -> tuple[bool, "TemperatureConstraint", str]:
        """Derive temperature support and constraint for a model.

        Args:
            model_name: Canonical model identifier or alias.
            constraint_hint: Optional configuration hint (``"fixed"``,
                ``"range"``, ``"discrete"``). When provided, the hint is
                honoured directly.

        Returns:
            Tuple ``(supports_temperature, constraint, diagnosis)`` describing
            whether temperature may be tuned, the constraint object that should
            be attached to :class:`ModelCapabilities`, and the reasoning behind
            the decision.
        """

        if constraint_hint:
            constraint = TemperatureConstraint.create(constraint_hint)
            supports_temperature = constraint_hint != "fixed"
            reason = f"constraint hint '{constraint_hint}'"
            return supports_temperature, constraint, reason

        supports_temperature, reason = TemperatureConstraint.infer_support(model_name)
        if supports_temperature:
            constraint: TemperatureConstraint = RangeTemperatureConstraint(0.0, 2.0, 0.7)
        else:
            constraint = FixedTemperatureConstraint(1.0)

        return supports_temperature, constraint, reason

    @staticmethod
    def create(constraint_type: str) -> "TemperatureConstraint":
        """Factory that yields the appropriate constraint for a configuration hint."""

        if constraint_type == "fixed":
            # Fixed temperature models (O3/O4) only support temperature=1.0
            return FixedTemperatureConstraint(1.0)
        if constraint_type == "discrete":
            # For models with specific allowed values - using common OpenAI values as default
            return DiscreteTemperatureConstraint([0.0, 0.3, 0.7, 1.0, 1.5, 2.0], 0.3)
        # Default range constraint (for "range" or None)
        return RangeTemperatureConstraint(0.0, 2.0, 0.3)


class FixedTemperatureConstraint(TemperatureConstraint):
    """Constraint for models that enforce an exact temperature (for example O3)."""

    def __init__(self, value: float):
        self.value = value

    def validate(self, temperature: float) -> bool:
        return abs(temperature - self.value) < 1e-6  # Handle floating point precision

    def get_corrected_value(self, temperature: float) -> float:
        return self.value

    def get_description(self) -> str:
        return f"Only supports temperature={self.value}"

    def get_default(self) -> float:
        return self.value


class RangeTemperatureConstraint(TemperatureConstraint):
    """Constraint for providers that expose a continuous min/max temperature range."""

    def __init__(self, min_temp: float, max_temp: float, default: Optional[float] = None):
        self.min_temp = min_temp
        self.max_temp = max_temp
        self.default_temp = default or (min_temp + max_temp) / 2

    def validate(self, temperature: float) -> bool:
        return self.min_temp <= temperature <= self.max_temp

    def get_corrected_value(self, temperature: float) -> float:
        return max(self.min_temp, min(self.max_temp, temperature))

    def get_description(self) -> str:
        return f"Supports temperature range [{self.min_temp}, {self.max_temp}]"

    def get_default(self) -> float:
        return self.default_temp


class DiscreteTemperatureConstraint(TemperatureConstraint):
    """Constraint for models that permit a discrete list of temperature values."""

    def __init__(self, allowed_values: list[float], default: Optional[float] = None):
        self.allowed_values = sorted(allowed_values)
        self.default_temp = default or allowed_values[len(allowed_values) // 2]

    def validate(self, temperature: float) -> bool:
        return any(abs(temperature - val) < 1e-6 for val in self.allowed_values)

    def get_corrected_value(self, temperature: float) -> float:
        return min(self.allowed_values, key=lambda x: abs(x - temperature))

    def get_description(self) -> str:
        return f"Supports temperatures: {self.allowed_values}"

    def get_default(self) -> float:
        return self.default_temp
