"""
Seleccion del proveedor de correo.

El cliente puede pedir uno concreto en el campo `provider` de la peticion; si
no lo hace se usa `EMAIL_DEFAULT_PROVIDER`. Solo se admiten los que aparezcan
en `EMAIL_ENABLED_PROVIDERS`, de modo que habilitar Gmail en un entorno y no
en otro es cuestion de configuracion, no de codigo.
"""

from __future__ import annotations

import logging
from collections.abc import Callable

from django.conf import settings

from notifications.channels.email.base import EmailProvider, ProviderConfigurationError
from notifications.channels.email.console import ConsoleProvider
from notifications.channels.email.gmail import GmailProvider
from notifications.channels.email.sendgrid import SendGridProvider

logger = logging.getLogger("notifications.providers")

#: Constructores, no instancias: cada proveedor lee la configuracion al
#: crearse, lo que permite cambiarla en las pruebas con `override_settings`.
_FACTORIES: dict[str, Callable[[], EmailProvider]] = {
    SendGridProvider.name: SendGridProvider,
    GmailProvider.name: GmailProvider,
    ConsoleProvider.name: ConsoleProvider,
}


class UnknownProvider(Exception):
    """Se pidio un proveedor que no existe o no esta habilitado."""


def available_providers() -> list[str]:
    """Proveedores habilitados en este entorno, en orden de preferencia."""
    enabled = [p for p in settings.EMAIL_ENABLED_PROVIDERS if p in _FACTORIES]
    default = settings.EMAIL_DEFAULT_PROVIDER
    if default in enabled:
        enabled.remove(default)
        enabled.insert(0, default)
    return enabled


def get_provider(name: str | None = None) -> EmailProvider:
    """
    Devuelve una instancia del proveedor solicitado.

    Lanza `UnknownProvider` si no esta habilitado y
    `ProviderConfigurationError` si le falta configuracion. Se comprueba aqui
    para fallar antes de aceptar el envio, en lugar de dejar que la tarea de
    Celery falle mas tarde por un motivo que ya se conocia.
    """
    name = (name or settings.EMAIL_DEFAULT_PROVIDER or "").strip().lower()

    if name not in _FACTORIES:
        raise UnknownProvider(f"El proveedor '{name}' no existe.")
    if name not in settings.EMAIL_ENABLED_PROVIDERS:
        raise UnknownProvider(
            f"El proveedor '{name}' no esta habilitado en este entorno."
        )

    provider = _FACTORIES[name]()
    provider.check_configuration()
    return provider


def get_provider_with_fallback(name: str | None = None) -> EmailProvider:
    """
    Como `get_provider`, pero si el proveedor pedido no esta configurado
    prueba con los demas habilitados.

    Util cuando el objetivo es que el correo salga como sea; no se usa por
    defecto porque un envio silenciosamente redirigido a otro proveedor puede
    salir con un remitente distinto al esperado.
    """
    candidates = [name] if name else []
    candidates += [p for p in available_providers() if p != name]

    last_error: Exception | None = None
    for candidate in candidates:
        try:
            return get_provider(candidate)
        except (UnknownProvider, ProviderConfigurationError) as exc:
            logger.warning(
                "Proveedor '%s' descartado: %s", candidate, exc, extra={"provider": candidate}
            )
            last_error = exc

    raise last_error or UnknownProvider("No hay ningun proveedor de correo disponible.")
