"""
Tareas asincronas.

Enviar dentro de la peticion HTTP tiene dos problemas: el cliente se queda
esperando a un tercero sobre el que no tenemos control, y un fallo temporal
del proveedor se convierte en una notificacion perdida. Con una cola, la
peticion responde en milisegundos y los fallos temporales se reintentan solos.
"""

from __future__ import annotations

import logging

from celery import shared_task
from celery.exceptions import SoftTimeLimitExceeded

from notifications.models.event import NotificationEvent
from notifications.models.message import MessageStatus, NotificationMessage

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

#: Espera entre reintentos, en segundos. Crece para no insistir sobre un
#: proveedor que ya esta teniendo problemas.
RETRY_BACKOFF = (30, 120, 600, 1800, 3600)


@shared_task(
    bind=True,
    name="notifications.send_email",
    max_retries=len(RETRY_BACKOFF),
    acks_late=True,
)
def send_email_task(self, message_id: str, request_id: str = ""):
    """
    Envia un correo previamente registrado.

    Solo se reintenta cuando el proveedor indica que el fallo es temporal
    (limite de velocidad, 5xx, corte de red). Un mensaje rechazado por su
    contenido daria el mismo error en cada intento.
    """
    from notifications.services.mailer import deliver

    message = NotificationMessage.objects.filter(pk=message_id).first()
    if message is None:
        logger.error(
            "La tarea recibio un envio inexistente", extra={"message_id": message_id}
        )
        return {"status": "not_found", "message_id": message_id}

    if message.status in {MessageStatus.SENT, MessageStatus.DELIVERED}:
        # Puede ocurrir si la tarea se reencola tras un `acks_late` y el envio
        # ya se habia completado. Reenviar duplicaria el correo.
        logger.info(
            "El envio ya estaba completado; no se reintenta",
            extra={"message_id": message_id, "status": message.status},
        )
        return {"status": message.status, "message_id": message_id}

    try:
        result = deliver(message)
    except SoftTimeLimitExceeded:
        logger.error(
            "El envio agoto el tiempo limite de la tarea",
            extra={"message_id": message_id},
        )
        message.mark_failed(
            code="task_timeout", message="El envio supero el tiempo limite."
        )
        raise

    if result.success:
        return {
            "status": "sent",
            "message_id": message_id,
            "provider_message_id": result.message_id,
        }

    if result.retryable and self.request.retries < self.max_retries:
        countdown = RETRY_BACKOFF[self.request.retries]
        logger.info(
            "Reintentando el envio en %ss (intento %s de %s)",
            countdown,
            self.request.retries + 1,
            self.max_retries,
            extra={"message_id": message_id, "error_code": result.error_code},
        )
        raise self.retry(countdown=countdown)

    return {
        "status": "failed",
        "message_id": message_id,
        "error_code": result.error_code,
    }


@shared_task(
    bind=True,
    name="notifications.send_messaging",
    max_retries=len(RETRY_BACKOFF),
    acks_late=True,
)
def send_messaging_task(self, message_id: str, request_id: str = ""):
    """
    Envia un SMS o un mensaje de WhatsApp previamente registrado.

    Mismo criterio que en correo: solo se reintenta si el proveedor indica que
    el fallo es temporal. Un numero invalido o un destinatario dado de baja
    darian el mismo error en cada intento, y cada intento se factura.
    """
    from notifications.services.messenger import deliver

    message = NotificationMessage.objects.filter(pk=message_id).first()
    if message is None:
        logger.error(
            "La tarea recibio un envio inexistente", extra={"message_id": message_id}
        )
        return {"status": "not_found", "message_id": message_id}

    if message.status in {MessageStatus.SENT, MessageStatus.DELIVERED}:
        # Con `acks_late` una tarea puede reencolarse tras completarse.
        # Reenviar duplicaria el mensaje y volveria a facturarlo.
        logger.info(
            "El envio ya estaba completado; no se reintenta",
            extra={"message_id": message_id, "status": message.status},
        )
        return {"status": message.status, "message_id": message_id}

    try:
        result = deliver(message)
    except SoftTimeLimitExceeded:
        logger.error(
            "El envio agoto el tiempo limite de la tarea",
            extra={"message_id": message_id},
        )
        message.mark_failed(
            code="task_timeout", message="El envio supero el tiempo limite."
        )
        raise

    if result.success:
        return {
            "status": "sent",
            "message_id": message_id,
            "provider_message_id": result.message_id,
        }

    if result.retryable and self.request.retries < self.max_retries:
        countdown = RETRY_BACKOFF[self.request.retries]
        logger.info(
            "Reintentando el envio en %ss (intento %s de %s)",
            countdown,
            self.request.retries + 1,
            self.max_retries,
            extra={"message_id": message_id, "error_code": result.error_code},
        )
        raise self.retry(countdown=countdown)

    return {
        "status": "failed",
        "message_id": message_id,
        "error_code": result.error_code,
    }


@shared_task(name="notifications.process_twilio_status")
def process_twilio_status_task(message_id: str, payload: dict, request_id: str = ""):
    """Procesa un aviso de cambio de estado de Twilio."""
    from notifications.services.events import ingest_twilio_status

    guardado = ingest_twilio_status(message_id, payload)
    logger.info(
        "Estado de Twilio procesado",
        extra={"message_id": message_id, "guardado": guardado},
    )
    return {"message_id": message_id, "stored": guardado}


@shared_task(name="notifications.process_sendgrid_events")
def process_sendgrid_events_task(events: list[dict], request_id: str = ""):
    """
    Procesa un lote de eventos del webhook de SendGrid.

    Se hace fuera de la peticion para poder responder de inmediato: SendGrid
    reintenta la entrega si tardamos demasiado, y eso genera duplicados.
    """
    from notifications.services.events import ingest_sendgrid_events

    stored = ingest_sendgrid_events(events)
    logger.info(
        "Eventos de SendGrid procesados",
        extra={"received": len(events), "stored": stored},
    )
    return {"received": len(events), "stored": stored}


@shared_task(name="notifications.purge_expired_tokens")
def purge_expired_tokens_task():
    """
    Borra los tokens de acceso caducados.

    No es una medida de seguridad —un token caducado ya no autentica— sino de
    higiene: los tokens se emiten en cada peticion de envio y solo se borran
    solos al usarse, asi que los que nunca llegan a usarse se acumulan.
    Programar con Celery beat o con un cron diario.
    """
    from keys.models.access_token import AccessToken

    borrados = AccessToken.purge_expired()
    logger.info("Purga de tokens caducados", extra={"deleted": borrados})
    return {"tokens": borrados}


@shared_task(name="notifications.purge_old_messages")
def purge_old_messages_task(days: int | None = None):
    """
    Borra la auditoria mas antigua que `NOTIFICATIONS_RETENTION_DAYS`.

    Los registros contienen direcciones de correo y numeros de telefono, que
    son datos personales: conservarlos indefinidamente no tiene justificacion
    operativa. Programar con Celery beat o con un cron diario.
    """
    from datetime import timedelta

    from django.conf import settings
    from django.utils import timezone

    days = days if days is not None else settings.NOTIFICATIONS_RETENTION_DAYS
    cutoff = timezone.now() - timedelta(days=days)

    events_deleted, _ = NotificationEvent.objects.filter(
        occurred_at__lt=cutoff
    ).delete()
    messages_deleted, _ = NotificationMessage.objects.filter(
        created_at__lt=cutoff
    ).delete()

    logger.info(
        "Purga de auditoria completada",
        extra={
            "cutoff": cutoff.isoformat(),
            "messages_deleted": messages_deleted,
            "events_deleted": events_deleted,
        },
    )
    return {"messages": messages_deleted, "events": events_deleted}
