"""
Contrato de los proveedores de mensajeria (SMS y WhatsApp).

Antes, `TwilioUtil` se instanciaba dentro de la vista y esta llamaba a sus
metodos directamente. Aqui se define la misma frontera que en el canal de
correo: la vista construye un `MessagingPayload` neutro y el proveedor lo
traduce a su formato. Cambiar Twilio por otro proveedor, o anadir uno de
respaldo, es implementar una clase mas.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import Any, ClassVar

from notifications.channels.common import (
    ProviderConfigurationError,
    SendResult,
    is_retryable_status,
)

__all__ = [
    "MessagingPayload",
    "MessagingProvider",
    "ProviderConfigurationError",
    "SendResult",
    "is_retryable_status",
]


@dataclass(frozen=True)
class MessagingPayload:
    """Un mensaje listo para enviar, independiente del proveedor."""

    #: Numero de destino en formato E.164 (+52...).
    to: str
    #: Canal por el que sale: "sms" o "whatsapp".
    channel: str = "sms"

    #: Texto del mensaje. Excluyente con `template_sid`.
    body: str = ""

    #: Plantilla aprobada del proveedor, para abrir conversacion de WhatsApp
    #: fuera de la ventana de 24 horas.
    template_sid: str = ""
    template_variables: Mapping[str, Any] = field(default_factory=dict)

    #: URL a la que el proveedor notifica los cambios de estado.
    status_callback: str = ""

    #: Metadatos que el proveedor devuelve en los eventos de estado.
    custom_args: Mapping[str, str] = field(default_factory=dict)

    @property
    def uses_template(self) -> bool:
        return bool(self.template_sid)


class MessagingProvider(ABC):
    """Interfaz que debe cumplir cualquier proveedor de SMS o WhatsApp."""

    #: Identificador corto del proveedor.
    name: ClassVar[str] = ""

    #: Canales que sabe atender.
    channels: ClassVar[tuple[str, ...]] = ()

    @abstractmethod
    def send(self, payload: MessagingPayload) -> SendResult:
        """
        Envia el mensaje.

        No debe lanzar excepciones por fallos del proveedor: esos se reportan
        en el `SendResult`. Solo puede propagar errores de programacion.
        """

    def check_configuration(self, channel: str) -> None:
        """
        Verifica que el proveedor pueda operar en ese canal.

        Lanza `ProviderConfigurationError` si falta algo. Se llama al
        seleccionar el proveedor, para fallar pronto y con un mensaje claro en
        lugar de a mitad del envio.
        """
        return None
