"""
Async (Celery) or sync Stripe webhook processing.

When Celery is installed, the webhook view enqueues a task so the HTTP response
is fast and Stripe does not retry. When Celery is not installed, the same logic
runs synchronously so the package works without a broker (e.g. in small setups).

Retry policy (Celery only)
--------------------------
- ``TransientWebhookError`` → retry with exponential back-off
  (30 s, 60 s, 120 s, 240 s, 480 s).
- Any other exception → mark as permanent error, no retry.
"""
from __future__ import annotations

import json
import logging

from .models import StripeWebhookEvent
from .webhooks import TransientWebhookError, registry

logger = logging.getLogger(__name__)

try:
    from celery import shared_task
except ImportError:
    # No-op decorator when Celery is not installed; view will call the function directly.
    def shared_task(*args, **kwargs):
        def decorator(f):
            return f
        return decorator


def process_webhook_event_sync(webhook_event_id: int) -> None:
    """
    Process a persisted StripeWebhookEvent (load, dispatch handler, update status).

    Raises TransientWebhookError when the handler signals a retryable failure;
    callers (Celery task or view) should retry or mark error accordingly.
    """
    try:
        webhook_event = StripeWebhookEvent.objects.get(pk=webhook_event_id)
    except StripeWebhookEvent.DoesNotExist:
        logger.error('[WEBHOOK TASK] StripeWebhookEvent id=%s not found', webhook_event_id)
        return

    if webhook_event.status == 'success':
        logger.info(
            '[WEBHOOK TASK] Event %s already processed — skipping',
            webhook_event.stripe_event_id,
        )
        return

    handler = registry.get(webhook_event.event_type)
    if handler is None:
        webhook_event.mark_skipped(
            reason=f'No handler registered for {webhook_event.event_type}'
        )
        logger.info('[WEBHOOK TASK] No handler for %s', webhook_event.event_type)
        return

    webhook_event.mark_processing()

    try:
        payload = json.loads(webhook_event.payload)
        data_object = payload.get('data', {}).get('object', {})
    except (json.JSONDecodeError, AttributeError) as exc:
        webhook_event.mark_error(f'Payload parse error: {exc}')
        logger.error(
            '[WEBHOOK TASK] Cannot parse payload for %s: %s',
            webhook_event.stripe_event_id, exc,
        )
        return

    try:
        handler(data_object)
        webhook_event.mark_success()
        logger.info(
            '[WEBHOOK TASK] Successfully processed %s', webhook_event.stripe_event_id
        )
    except TransientWebhookError as exc:
        # Do not mark error here; let Celery task retry. If sync path, view will mark_error.
        logger.warning(
            '[WEBHOOK TASK] Transient error for %s: %s',
            webhook_event.stripe_event_id, exc,
        )
        raise
    except Exception as exc:  # noqa: BLE001
        webhook_event.mark_error(str(exc))
        logger.error(
            '[WEBHOOK TASK] Permanent error for %s: %s',
            webhook_event.stripe_event_id, exc,
            exc_info=True,
        )


@shared_task(
    bind=True,
    max_retries=5,
    default_retry_delay=30,
    acks_late=True,
)
def process_stripe_webhook_event(self, webhook_event_id: int) -> None:
    """
    Process a persisted StripeWebhookEvent asynchronously (when Celery is used).

    When Celery is not installed, this is the same as process_webhook_event_sync
    and the view calls it without .delay().
    """
    try:
        process_webhook_event_sync(webhook_event_id)
    except TransientWebhookError as exc:
        raise self.retry(exc=exc, countdown=30 * (2 ** self.request.retries))
