"""Declarative parameter policies for canonical models.

Policy resolution follows a clear hierarchy:
1. Explicit per-model overrides (_POLICY_BY_MODEL)
2. Catalog capabilities ("no-temperature" -> reasoning policy)
3. Prefix fallback for dynamic/unknown models
4. Default policy

The catalog is the single source of truth for model capabilities. The prefix
fallback exists only to handle models discovered dynamically that aren't yet
in the catalog.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ember.models.catalog import ModelInfo

logger = logging.getLogger(__name__)


@dataclass(frozen=True, slots=True)
class ResponsesParamPolicy:
    """Controls which sampling knobs Ember may pass to a model."""

    allow_temperature: bool = True
    allow_top_p: bool = True
    allow_reasoning: bool = True


_DEFAULT_POLICY = ResponsesParamPolicy()

# Policy for reasoning models that do not support temperature/top_p.
_REASONING_POLICY = ResponsesParamPolicy(allow_temperature=False, allow_top_p=False)

# Fallback prefixes for dynamic/unknown models not in catalog.
# Primary source of truth is catalog capabilities ("no-temperature").
_REASONING_MODEL_PREFIXES = ("gpt-5", "o1", "o3", "o4")

# Explicit overrides for specific model IDs.
_POLICY_BY_MODEL: dict[str, ResponsesParamPolicy] = {
    # Anthropic Responses does not support reasoning controls yet.
    "claude-opus-4-1-20250805": ResponsesParamPolicy(allow_reasoning=False),
    "claude-sonnet-4-5-20250929": ResponsesParamPolicy(allow_reasoning=False),
}


def _try_get_model_info(model_id: str) -> "ModelInfo | None":
    """Attempt to fetch model info from catalog without raising.

    Args:
        model_id: The model identifier to look up.

    Returns:
        ModelInfo if found in the static catalog, None otherwise.
    """
    try:
        from ember.models.catalog import get_model_info

        return get_model_info(model_id, include_dynamic=False)
    except (KeyError, ImportError):
        return None


def _is_reasoning_model(model_id: str) -> bool:
    """Check if model is a reasoning model by prefix (fallback for unknown models)."""
    lowered = model_id.lower()
    return any(lowered.startswith(prefix) for prefix in _REASONING_MODEL_PREFIXES)


def get_responses_policy(model_id: str) -> ResponsesParamPolicy:
    """Return the parameter policy for ``model_id``.

    Resolution order:
    1. Explicit per-model overrides (_POLICY_BY_MODEL)
    2. Catalog capabilities ("no-temperature" -> reasoning policy)
    3. Prefix fallback for dynamic/unknown models
    4. Default policy

    Args:
        model_id: The model identifier to get policy for.

    Returns:
        The appropriate ResponsesParamPolicy for the model.
    """
    key = model_id.strip().lower()

    # 1. Check explicit overrides first.
    if key in _POLICY_BY_MODEL:
        return _POLICY_BY_MODEL[key]

    # 2. Query catalog capabilities (primary source of truth).
    info = _try_get_model_info(key)
    if info is not None:
        caps = info.capabilities or ()
        if "no-temperature" in caps:
            return _REASONING_POLICY

    # 3. Fallback to prefix matching for unknown models.
    if _is_reasoning_model(key):
        logger.debug(
            "Model '%s' matched reasoning policy via prefix fallback; "
            "consider adding it to MODEL_CATALOG with 'no-temperature' capability",
            model_id,
        )
        return _REASONING_POLICY

    return _DEFAULT_POLICY


__all__ = ["ResponsesParamPolicy", "get_responses_policy"]
