"""Resolve the active AI provider per capability.

Settings are read fresh from ``ir.config_parameter`` on every call — there
is intentionally NO module-level cache so flipping a provider in the admin
UI takes effect on the next request without restarting Odoo.

Public surface:

* :func:`get_active_provider(env, capability)` — returns the provider key
  configured for ``capability`` (one of ``text|image|audio|video``).
* :func:`classify_provider_error(exc)` — turns an arbitrary exception from
  any third-party SDK into one of ``quota|auth|network|other`` so callers
  can decide whether to fall back automatically.
* :class:`ProviderQuotaError`, :class:`ProviderAuthError` — typed errors
  that callers may raise when they want a strongly-typed signal.

The capability tables also enumerate the **allowed free providers** per
capability — these are the ones the fallback chain uses when the paid
provider returns a quota or auth error.
"""

from __future__ import annotations

import logging

_logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Capabilities & provider names
# ---------------------------------------------------------------------------
#
# ``auto`` means "pick the first paid provider that's configured, else fall
# back to the first free provider that works". Admins who want to *force* a
# specific provider should pick its name explicitly in the UI.

CAPABILITIES = {
    'text': {
        'param': 'encoach.ai.text_provider',
        'default': 'openai',
        'paid': ['openai'],
        'free': ['mock'],
    },
    'image': {
        'param': 'encoach.ai.image_provider',
        'default': 'auto',
        'paid': ['openai'],
        'free': ['pillow', 'unsplash', 'mock'],
    },
    'audio': {
        'param': 'encoach.ai.audio_provider',
        'default': 'auto',
        'paid': ['polly', 'elevenlabs'],
        'free': ['gtts', 'silent'],
    },
    'video': {
        'param': 'encoach.ai.video_provider',
        'default': 'auto',
        'paid': [],
        'free': ['ffmpeg', 'static'],
    },
}


# ---------------------------------------------------------------------------
# Typed errors
# ---------------------------------------------------------------------------


class ProviderQuotaError(RuntimeError):
    """Raised when a provider returns a billing/quota error.

    Examples include OpenAI ``insufficient_quota``, AWS Polly
    ``ThrottlingException``, ElevenLabs character-limit rejection, and any
    HTTP 402/429. Callers should treat this as a soft failure and try the
    next provider in the fallback chain.
    """


class ProviderAuthError(RuntimeError):
    """Raised when a provider refuses authentication (missing/invalid key)."""


# ---------------------------------------------------------------------------
# Resolution
# ---------------------------------------------------------------------------


def get_active_provider(env, capability):
    """Read the currently-active provider key for ``capability``.

    Always reads from ``ir.config_parameter`` — never cached.
    """
    cap = CAPABILITIES[capability]
    return env['ir.config_parameter'].sudo().get_param(
        cap['param'], cap['default'],
    ) or cap['default']


def get_paid_provider_keys(env, capability):
    """Return the list of paid providers for the capability that have an
    API key configured. Used to decide whether to skip straight to free
    fallbacks when ``auto`` is selected but no paid keys exist.
    """
    Param = env['ir.config_parameter'].sudo()
    available = []
    for provider in CAPABILITIES[capability]['paid']:
        if _provider_has_credentials(Param, provider):
            available.append(provider)
    return available


def _provider_has_credentials(Param, provider):
    if provider == 'openai':
        return bool(Param.get_param('encoach_ai.openai_api_key'))
    if provider == 'polly':
        return bool(Param.get_param('encoach_ai.aws_access_key')) and bool(
            Param.get_param('encoach_ai.aws_secret_key')
        )
    if provider == 'elevenlabs':
        return bool(Param.get_param('encoach_ai.elevenlabs_api_key'))
    return False


def resolve_chain(env, capability, *, requested=None):
    """Return an ordered list of providers to try for ``capability``.

    Args:
        capability: ``'text' | 'image' | 'audio' | 'video'``.
        requested: optional explicit provider override (e.g. body param).
            When supplied it goes to the front of the chain.
    """
    cap = CAPABILITIES[capability]
    chain = []
    selected = (requested or get_active_provider(env, capability) or '').strip()

    if selected and selected != 'auto':
        chain.append(selected)

    # Paid providers with credentials, then free fallbacks
    if selected == 'auto' or not selected:
        for p in get_paid_provider_keys(env, capability):
            if p not in chain:
                chain.append(p)
    for p in cap['free']:
        if p not in chain:
            chain.append(p)
    return chain


# ---------------------------------------------------------------------------
# Error classification
# ---------------------------------------------------------------------------


_QUOTA_TOKENS = (
    'insufficient_quota', 'quota', 'billing',
    '429', '402',
    'rate limit', 'rate_limit', 'rate-limit',
    'throttlingexception', 'thresholdexceeded',
    'limit_exceeded', 'character limit', 'too many requests',
)
_AUTH_TOKENS = (
    'invalid_api_key', 'incorrect api key', 'authentication',
    'unauthorized', 'access denied', '401', '403',
    'authenticationerror', 'permissiondenied', 'invalid api key',
    'missing api key',
)


def classify_provider_error(exc):
    """Map any provider exception to one of: ``quota|auth|network|other``."""
    msg = (str(exc) or '').lower()
    name = type(exc).__name__.lower()
    blob = msg + ' ' + name
    if any(t in blob for t in _QUOTA_TOKENS):
        return 'quota'
    if any(t in blob for t in _AUTH_TOKENS):
        return 'auth'
    if 'timeout' in blob or 'connection' in blob or 'network' in msg:
        return 'network'
    return 'other'


def should_fallback(exc):
    """Whether the caller should try the next provider in the chain."""
    return classify_provider_error(exc) in ('quota', 'auth', 'network')
