"""
Orquestacion del envio de SMS y WhatsApp.

Equivalente a `mailer.py` para el canal de mensajeria. Antes esta logica vivia
dentro de las vistas: instanciaban `TwilioUtil`, recorrian los numeros y
llamaban al proveedor en linea, dentro de la peticion HTTP.

Diferencia importante con el correo: en mensajeria cada destinatario es un
mensaje independiente. Cada SMS se factura por separado, puede fallar por su
cuenta y tiene su propio estado de entrega, asi que se crea una fila de
auditoria por numero. En correo, en cambio, un mensaje con veinte
destinatarios es un unico envio.
"""

from __future__ import annotations

import logging

from django.conf import settings
from django.db import transaction

from core.utils.http import get_client_ip
from notifications.channels.common import SendResult
from notifications.channels.messaging.base import MessagingPayload
from notifications.channels.messaging.registry import get_provider
from notifications.models.message import MessageStatus, NotificationMessage
from notifications.models.payload import MessagePayloadBlob
from notifications.services.serialization import (
    messaging_payload_to_bytes,
    payload_from_bytes,
)

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

#: Clave con la que viaja el identificador interno del envio.
CUSTOM_ARG_MESSAGE_ID = "api_message_id"


def build_payload(
    *,
    to: str,
    channel: str,
    body: str = "",
    template_sid: str = "",
    template_variables: dict | None = None,
    message_id: str,
) -> MessagingPayload:
    """Construye el payload neutro que entiende el proveedor."""
    return MessagingPayload(
        to=to,
        channel=channel,
        body=body,
        template_sid=template_sid,
        template_variables=template_variables or {},
        status_callback=_status_callback_url(message_id),
        custom_args={CUSTOM_ARG_MESSAGE_ID: message_id},
    )


def _status_callback_url(message_id: str) -> str:
    """
    URL a la que Twilio notifica los cambios de estado.

    Lleva el identificador del envio en la ruta para poder correlacionar el
    evento sin depender solo del SID del proveedor.
    """
    base = (settings.TWILIO_STATUS_CALLBACK_BASE_URL or "").rstrip("/")
    if not base:
        return ""
    return f"{base}/notifications/v1/webhooks/twilio/{message_id}"


@transaction.atomic
def register_message(
    *,
    channel: str,
    to: str,
    body: str = "",
    template_sid: str = "",
    template_variables: dict | None = None,
    provider_name: str,
    request=None,
    api_key=None,
) -> tuple[NotificationMessage, MessagingPayload]:
    """Crea la fila de auditoria y guarda el contenido pendiente de enviar."""
    message = NotificationMessage(
        channel=channel,
        api_key=api_key,
        provider=provider_name,
        status=MessageStatus.QUEUED,
        to=[to],
        recipient_count=1,
        sender=(
            settings.TWILIO_WHATSAPP_FROM
            if channel == "whatsapp"
            else settings.TWILIO_SMS_FROM
        ),
        template_id=template_sid,
        request_id=getattr(request, "request_id", "") if request else "",
        source_ip=get_client_ip(request) if request else None,
    )

    if settings.NOTIFICATIONS_STORE_BODY:
        message.body_text = body

    message.save()

    payload = build_payload(
        to=to,
        channel=channel,
        body=body,
        template_sid=template_sid,
        template_variables=template_variables,
        message_id=str(message.id),
    )
    MessagePayloadBlob.objects.create(
        message=message, data=messaging_payload_to_bytes(payload)
    )

    return message, payload


def dispatch(
    message: NotificationMessage, *, send_async: bool, request_id: str = ""
) -> None:
    """
    Entrega el mensaje al worker o lo envia en el acto.

    En modo asincrono la tarea se encola despues de confirmar la transaccion,
    para que el worker nunca vea una fila que aun no existe.
    """
    if send_async:
        from notifications.tasks import send_messaging_task

        transaction.on_commit(
            lambda: send_messaging_task.delay(
                str(message.id), request_id=request_id or message.request_id
            )
        )
        return

    deliver(message)


def deliver(message: NotificationMessage) -> SendResult:
    """Envia el mensaje a traves de su proveedor y actualiza la auditoria."""
    blob = MessagePayloadBlob.objects.filter(message=message).first()
    if blob is None:
        logger.error(
            "No se encontro el contenido del envio",
            extra={"message_id": str(message.id)},
        )
        message.mark_failed(
            code="payload_missing",
            message="El contenido del mensaje ya no esta disponible.",
        )
        return SendResult.failure(
            message.provider,
            error_code="payload_missing",
            error_message="Contenido no disponible.",
            retryable=False,
        )

    try:
        payload = payload_from_bytes(blob.data)
    except Exception as exc:  # noqa: BLE001 - contenido corrupto
        logger.exception(
            "El contenido guardado del envio no se pudo leer",
            extra={"message_id": str(message.id)},
        )
        message.mark_failed(code="payload_corrupt", message=str(exc))
        _discard_payload(message)
        return SendResult.failure(
            message.provider,
            error_code="payload_corrupt",
            error_message=str(exc),
            retryable=False,
        )

    message.mark_sending()

    provider = get_provider(message.provider)
    result = provider.send(payload)

    if result.success:
        message.mark_sent(result.message_id)
        _discard_payload(message)
        logger.info(
            "Envio completado",
            extra={
                "message_id": str(message.id),
                "channel": message.channel,
                "provider": message.provider,
                "provider_message_id": result.message_id,
            },
        )
    else:
        message.mark_failed(result.error_code, result.error_message)
        if not result.retryable:
            _discard_payload(message)
        logger.warning(
            "Envio fallido",
            extra={
                "message_id": str(message.id),
                "channel": message.channel,
                "provider": message.provider,
                "error_code": result.error_code,
                "retryable": result.retryable,
                "attempts": message.attempts,
            },
        )

    return result


def _discard_payload(message: NotificationMessage) -> None:
    """Borra el contenido temporal del envio."""
    MessagePayloadBlob.objects.filter(message=message).delete()


def send_many(
    *,
    channel: str,
    numbers: list[str],
    body: str = "",
    template_sid: str = "",
    template_variables: dict | None = None,
    provider_name: str,
    send_async: bool,
    request=None,
    api_key=None,
) -> list[NotificationMessage]:
    """
    Registra y despacha un mensaje por cada numero.

    Devuelve las filas de auditoria en el mismo orden que los numeros, para
    que la vista pueda informar del resultado de cada uno.
    """
    mensajes = []
    for numero in numbers:
        message, _payload = register_message(
            channel=channel,
            to=numero,
            body=body,
            template_sid=template_sid,
            template_variables=template_variables,
            provider_name=provider_name,
            request=request,
            api_key=api_key,
        )
        dispatch(
            message,
            send_async=send_async,
            request_id=getattr(request, "request_id", ""),
        )
        mensajes.append(message)
    return mensajes
