"""
Proveedor Twilio para SMS y WhatsApp.

Reescritura de `notifications/utils/twilio.py`. Lo que se corrige:

1. Las credenciales y los numeros de origen estaban escritos en el archivo, y
   por tanto en el historial del repositorio. Ahora vienen de la configuracion.
2. `except:` desnudos que convertian cualquier fallo en un `ValueError(
   "Error sending message")`, sin distinguir "el numero no existe" de "Twilio
   esta caido" y sin dejar rastro del motivo. Ahora se traduce a un
   `SendResult` que sabe si merece la pena reintentar.
3. No habia tiempo de espera. Una llamada sin timeout puede dejar bloqueado un
   worker indefinidamente.
4. Se registra la URL de callback de estado, de modo que las entregas y los
   fallos posteriores llegan a la auditoria en lugar de perderse.
"""

from __future__ import annotations

import json
import logging

from django.conf import settings
from twilio.base.exceptions import TwilioRestException
from twilio.rest import Client

from notifications.channels.common import (
    ProviderConfigurationError,
    SendResult,
    is_retryable_status,
)
from notifications.channels.messaging.base import MessagingPayload, MessagingProvider

logger = logging.getLogger("notifications.channels.twilio")

CHANNEL_SMS = "sms"
CHANNEL_WHATSAPP = "whatsapp"

#: Codigos de error de Twilio que indican un problema permanente con el
#: destinatario. Reintentar solo gasta cuota y vuelve a fallar igual.
#: https://www.twilio.com/docs/api/errors
CODIGOS_PERMANENTES = frozenset(
    {
        21211,  # numero 'to' invalido
        21214,  # numero 'to' no es un movil valido
        21408,  # sin permiso para enviar a esa region
        21610,  # el destinatario se dio de baja
        21612,  # el numero de origen no puede alcanzar ese destino
        21614,  # 'to' no admite SMS
        63003,  # canal de WhatsApp: destinatario no encontrado
        63024,  # parametros de plantilla invalidos
    }
)


class TwilioProvider(MessagingProvider):
    name = "twilio"
    channels = (CHANNEL_SMS, CHANNEL_WHATSAPP)

    def __init__(self, client: Client | None = None):
        self._client = client

    # -- Configuracion ------------------------------------------------------

    def check_configuration(self, channel: str) -> None:
        if not settings.TWILIO_ACCOUNT_SID or not settings.TWILIO_AUTH_TOKEN:
            raise ProviderConfigurationError(
                "Faltan TWILIO_ACCOUNT_SID o TWILIO_AUTH_TOKEN en el entorno."
            )
        if channel == CHANNEL_SMS and not settings.TWILIO_SMS_FROM:
            raise ProviderConfigurationError("Falta TWILIO_SMS_FROM en el entorno.")
        if channel == CHANNEL_WHATSAPP and not settings.TWILIO_WHATSAPP_FROM:
            raise ProviderConfigurationError(
                "Falta TWILIO_WHATSAPP_FROM en el entorno."
            )

    @property
    def client(self) -> Client:
        if self._client is None:
            self._client = Client(
                settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN
            )
        return self._client

    # -- Construccion del mensaje ------------------------------------------

    def _build_kwargs(self, payload: MessagingPayload) -> dict:
        if payload.channel == CHANNEL_WHATSAPP:
            destino = f"whatsapp:{payload.to}"
            origen = f"whatsapp:{settings.TWILIO_WHATSAPP_FROM}"
        else:
            destino = payload.to
            origen = settings.TWILIO_SMS_FROM

        kwargs: dict = {"to": destino}

        if payload.uses_template:
            # Con plantilla hay que salir por el servicio de mensajeria, no
            # por un numero suelto.
            if not settings.TWILIO_MESSAGING_SERVICE_SID:
                raise ProviderConfigurationError(
                    "Falta TWILIO_MESSAGING_SERVICE_SID: es necesario para "
                    "enviar plantillas de WhatsApp."
                )
            kwargs["from_"] = settings.TWILIO_MESSAGING_SERVICE_SID
            kwargs["content_sid"] = payload.template_sid
            if payload.template_variables:
                kwargs["content_variables"] = json.dumps(
                    dict(payload.template_variables)
                )
        else:
            kwargs["from_"] = origen
            kwargs["body"] = payload.body

        if payload.status_callback:
            kwargs["status_callback"] = payload.status_callback

        return kwargs

    # -- Envio --------------------------------------------------------------

    def send(self, payload: MessagingPayload) -> SendResult:
        try:
            self.check_configuration(payload.channel)
            kwargs = self._build_kwargs(payload)
        except ProviderConfigurationError as exc:
            logger.error("Twilio mal configurado: %s", exc)
            return SendResult.failure(
                self.name,
                error_code="provider_not_configured",
                error_message=str(exc),
                # Es un fallo de configuracion, no del mensaje: cuando se
                # corrija, el reintento saldra bien.
                retryable=True,
            )

        try:
            respuesta = self.client.messages.create(**kwargs)
        except TwilioRestException as exc:
            codigo = getattr(exc, "code", None)
            http = getattr(exc, "status", None)
            permanente = codigo in CODIGOS_PERMANENTES

            logger.warning(
                "Twilio rechazo el envio por %s (HTTP %s, codigo %s)",
                payload.channel,
                http,
                codigo,
                extra={
                    "channel": payload.channel,
                    "status_code": http,
                    "twilio_code": codigo,
                },
            )
            return SendResult.failure(
                self.name,
                error_code=f"twilio_{codigo}" if codigo else f"http_{http}",
                error_message=getattr(exc, "msg", None) or str(exc),
                status_code=http,
                retryable=(not permanente) and is_retryable_status(http),
            )
        except Exception as exc:  # noqa: BLE001 - red, DNS, TLS, timeout
            logger.warning(
                "Fallo de red al contactar con Twilio: %s", exc.__class__.__name__
            )
            return SendResult.failure(
                self.name,
                error_code="network_error",
                error_message=f"{exc.__class__.__name__}: {exc}",
                retryable=True,
            )

        sid = getattr(respuesta, "sid", "") or ""
        logger.info(
            "Mensaje aceptado por Twilio",
            extra={
                "channel": payload.channel,
                "provider_message_id": sid,
                "twilio_status": getattr(respuesta, "status", None),
            },
        )
        return SendResult.ok(self.name, sid, status_code=201)
