"""Celery tasks for the event outbox."""

from __future__ import annotations

from datetime import timedelta

import structlog
from celery import shared_task
from django.db import transaction
from django.utils import timezone

from simorgh.apps.events.models import EventOutboxEntry, OutboxStatus
from simorgh.core.locks import with_row_lock

_log = structlog.get_logger("simorgh.events.outbox")

# Exponential backoff in seconds: 5, 25, 125, 625, 3125
BACKOFF_BASE_SECONDS = 5


def _backoff_for(attempt: int) -> timedelta:
    return timedelta(seconds=BACKOFF_BASE_SECONDS * (5 ** max(attempt - 1, 0)))


@shared_task(name="simorgh.events.flush_outbox")
def flush_outbox(batch_size: int = 100) -> dict[str, int]:
    """Pick up pending outbox entries and dispatch them synchronously.

    Uses :func:`with_row_lock` so two workers can run in parallel without
    double-delivery (the lock is a no-op on SQLite but still safe because
    a single worker delivers everything in dev/test).
    """

    # Late import to avoid circulars during Django app loading.
    from simorgh.apps.events import bus

    stats = {"dispatched": 0, "failed": 0, "dead": 0}
    now = timezone.now()

    with transaction.atomic():
        qs = (
            EventOutboxEntry.objects.filter(
                status__in=(OutboxStatus.PENDING, OutboxStatus.FAILED),
                next_retry_at__lte=now,
            )
            .order_by("next_retry_at")[:batch_size]
        )
        entries = list(with_row_lock(qs))

    for entry in entries:
        try:
            bus._dispatch_sync(entry.name, entry.payload, audit=False)
        except Exception as exc:
            entry.attempt += 1
            entry.last_error = str(exc)
            if entry.attempt >= entry.max_attempts:
                entry.status = OutboxStatus.DEAD
                stats["dead"] += 1
            else:
                entry.status = OutboxStatus.FAILED
                entry.next_retry_at = timezone.now() + _backoff_for(entry.attempt)
                stats["failed"] += 1
            entry.save(
                update_fields=[
                    "attempt",
                    "last_error",
                    "status",
                    "next_retry_at",
                    "updated_at",
                ]
            )
            _log.warning(
                "events.outbox.dispatch_failed",
                event_name=entry.name,
                attempt=entry.attempt,
                error=str(exc),
            )
            continue

        entry.status = OutboxStatus.DISPATCHED
        entry.dispatched_at = timezone.now()
        entry.attempt += 1
        entry.save(
            update_fields=["status", "dispatched_at", "attempt", "updated_at"],
        )
        stats["dispatched"] += 1

    return stats
