"""Base class for LLM providers."""

# Single placeholder for ChatOpenAI(api_key=...) when no real key is
# configured (unauthenticated local LM Studio, llama.cpp, etc.). Subclasses
# MUST NOT override this constant; per-provider drift here was the reason
# we previously had three different placeholder strings.
OPTIONAL_API_KEY_PLACEHOLDER = "not-required"  # noqa: S105


def normalize_provider(provider):
    """Normalize provider name to lowercase canonical form.

    All provider comparisons in route/service code should use
    this function to ensure consistent casing.
    """
    return provider.lower() if provider else None


class BaseLLMProvider:
    """Base class for all LLM providers.

    Defines the minimum interface that all providers must satisfy.
    Subclasses should override the methods below as needed.

    **Class attribute contract:**

    - ``api_key_setting`` — settings key for the provider's API key
      (e.g. ``"llm.openai.api_key"``), or ``None`` for providers that have
      no API key concept at all.
    - ``api_key_optional`` — when ``True``, a missing/whitespace-only key
      returns ``None`` from ``resolve_api_key()`` instead of raising.
      Construction-time callers can then substitute
      ``OPTIONAL_API_KEY_PLACEHOLDER`` (typical for local providers like
      LM Studio that may or may not have auth enabled).
    - ``provider_name`` — display name used in error messages and logs.
      Subclasses MUST override this away from the ``"unknown"`` default
      or auto-discovery will skip them.

    **Helper methods (use these instead of reading settings directly):**

    - ``resolve_api_key(snapshot)`` → ``str | None``. Returns the
      stripped key, ``None`` for optional providers when missing, or
      raises ``ValueError`` for required providers when missing.
    - ``resolve_api_key_or_placeholder(snapshot)`` → ``str``. Same as
      above but substitutes the placeholder when optional+missing — use
      this when constructing ``ChatOpenAI(api_key=...)``.
    - ``build_bearer_header(snapshot)`` → ``dict``. Returns
      ``{"Authorization": "Bearer <key>"}`` or ``{}``. Use this when
      sending Authorization headers to a provider's REST API.
    - ``has_api_key(snapshot)`` → ``bool``. Use in ``is_available()``
      overrides to fail-closed when a required key is missing.

    **`create_llm()` contract:** subclass implementations return a *bare*
    LangChain ``BaseChatModel``. Wrapping (rate limiting, token counting,
    think-tag stripping) is applied by ``llm_config.get_llm()`` after the
    provider class returns. Do NOT wrap inside ``create_llm()``.
    """

    # Settings key for this provider's API key. None means the provider
    # has no API key concept at all.
    api_key_setting: str | None = None

    # Display name used in error messages and logs. Subclasses set this.
    provider_name: str = "unknown"

    # When True, a missing/whitespace-only key returns None (callers may
    # substitute OPTIONAL_API_KEY_PLACEHOLDER) instead of raising.
    api_key_optional: bool = False

    @classmethod
    def create_llm(cls, model_name=None, temperature=0.7, **kwargs):
        """Create and return a LangChain chat model instance.

        Subclasses MUST override this method. The returned LLM is BARE —
        wrapping (rate limiting, token counting, think-tag stripping) is
        applied by ``llm_config.get_llm()`` after this returns.

        Args:
            model_name: Name of the model to use
            temperature: Model temperature (0.0-1.0)
            **kwargs: Additional arguments including settings_snapshot

        Returns:
            A configured BaseChatModel instance

        Raises:
            NotImplementedError: If not overridden by subclass
        """
        raise NotImplementedError(f"{cls.__name__} must implement create_llm()")

    @classmethod
    def is_available(cls, settings_snapshot=None):
        """Check if this provider is available.

        Returns False by default (fail-closed). Subclasses MUST override
        this method to implement their own availability logic.
        """
        return False

    @classmethod
    def requires_auth_for_models(cls):
        """Whether auth is needed to list models.

        Returns True by default. Override in subclasses that allow
        unauthenticated model listing (e.g., local providers).
        """
        return True

    @classmethod
    def resolve_api_key(cls, settings_snapshot=None) -> str | None:
        """Read this provider's API key from settings, normalized.

        Returns:
            The stripped key if non-empty; None if optional and missing
            or whitespace-only, or if api_key_setting is None.
        Raises:
            ValueError if required (api_key_optional=False) and missing.
        """
        from ...config.thread_settings import get_setting_from_snapshot

        if not cls.api_key_setting:
            return None
        raw = get_setting_from_snapshot(
            cls.api_key_setting, "", settings_snapshot=settings_snapshot
        )
        key = str(raw or "").strip()
        if key:
            return key
        if cls.api_key_optional:
            return None
        raise ValueError(
            f"{cls.provider_name} API key not configured. "
            f"Please set {cls.api_key_setting} in settings."
        )

    @classmethod
    def resolve_api_key_or_placeholder(cls, settings_snapshot=None) -> str:
        """For ``ChatOpenAI(api_key=...)``-style construction.

        Always returns a string. Real key when set; otherwise the unified
        ``OPTIONAL_API_KEY_PLACEHOLDER`` so the langchain client doesn't
        reject the construction call.
        """
        key = cls.resolve_api_key(settings_snapshot)
        return key if key else OPTIONAL_API_KEY_PLACEHOLDER

    @classmethod
    def build_bearer_header(cls, settings_snapshot=None) -> dict[str, str]:
        """Build an ``Authorization: Bearer`` header from the resolved key.

        Returns an empty dict when no real key is configured (so callers
        can spread it conditionally). Required-key providers that raise
        from ``resolve_api_key`` are treated as "no header" here too —
        the construction-time error happens elsewhere.
        """
        try:
            key = cls.resolve_api_key(settings_snapshot)
        except ValueError:
            return {}
        return {"Authorization": f"Bearer {key}"} if key else {}

    @classmethod
    def has_api_key(cls, settings_snapshot=None) -> bool:
        """True if a real key is configured.

        Returns False for missing/whitespace keys (required providers raise
        ValueError; we swallow it). Also returns False on broader settings
        errors so callers like ``is_available()`` fail closed instead of
        propagating settings infrastructure failures.
        """
        try:
            return cls.resolve_api_key(settings_snapshot) is not None
        except Exception:
            return False
