"""
Stripe webhook handler registry for django_stripe_billing.

Architecture
------------
Each handler receives the ``data.object`` dict from the Stripe event and
should perform business logic that is **common across all projects** (e.g.
firing outgoing webhooks, emitting Django signals).

App-specific logic (e.g. updating UserProfile, deducting credits) should
live in the consuming app's signal receivers — NOT here.

Registering a custom handler
-----------------------------
You can replace or extend the built-in dispatch by connecting to the
appropriate Django signal emitted by each handler, or by fully overriding
the handler registry::

    from django_stripe_billing.webhooks import registry

    @registry.handler('invoice.payment_succeeded')
    def my_handler(data_object):
        ...

Transient failures
------------------
Raise ``TransientWebhookError`` inside a handler to signal that the failure
is recoverable.  The Celery task will retry with exponential back-off instead
of marking the event as a permanent error.
"""
from __future__ import annotations

import logging

from .utils import fire_outgoing_webhook

logger = logging.getLogger(__name__)


class TransientWebhookError(Exception):
    """
    Raise inside a handler to signal a retryable / transient failure.

    The Celery task will retry with exponential back-off up to ``max_retries``
    times instead of marking the event as a permanent error.
    """


class HandlerRegistry:
    """
    Registry that maps Stripe event type strings to handler callables.

    Use the :meth:`handler` decorator to register a handler::

        from django_stripe_billing.webhooks import registry

        @registry.handler('invoice.payment_succeeded')
        def my_handler(data_object):
            ...
    """

    def __init__(self) -> None:
        self._handlers: dict[str, callable] = {}

    def handler(self, event_type: str):
        """Decorator: register *func* as the handler for *event_type*."""
        def decorator(func):
            self._handlers[event_type] = func
            return func
        return decorator

    def register(self, event_type: str, func) -> None:
        """Programmatically register *func* for *event_type*."""
        self._handlers[event_type] = func

    def get(self, event_type: str):
        """Return the handler for *event_type*, or ``None`` if not registered."""
        return self._handlers.get(event_type)

    def dispatch(self, event_type: str, data_object: dict) -> None:
        """
        Dispatch *data_object* to the registered handler for *event_type*.

        Raises:
            TransientWebhookError: if the handler signals a retryable failure.
            KeyError: if no handler is registered (callers should check with
                :meth:`get` first and mark the event as skipped).
        """
        handler = self._handlers.get(event_type)
        if handler is None:
            raise KeyError(f'No handler registered for event type: {event_type!r}')
        handler(data_object)


# ---------------------------------------------------------------------------
# Global registry — populated with built-in handlers below
# ---------------------------------------------------------------------------
registry = HandlerRegistry()


# ---------------------------------------------------------------------------
# Built-in handlers
# ---------------------------------------------------------------------------

@registry.handler('checkout.session.completed')
def handle_checkout_completed(session: dict) -> None:
    """
    Handle checkout.session.completed.

    Fires the ``checkout_completed`` signal so apps can persist the
    checkout session and link it to a payment record.

    Emits outgoing webhook: ``checkout.session.completed``
    """
    from .signals import checkout_completed

    session_id = session.get('id')
    logger.info('[WEBHOOK] checkout.session.completed (session=%s)', session_id)

    checkout_completed.send(
        sender=handle_checkout_completed,
        session=session,
        user_payment=None,
    )

    fire_outgoing_webhook('checkout.session.completed', {
        'session_id': session_id,
        'customer_id': session.get('customer'),
        'customer_email': session.get('customer_email'),
        'subscription': session.get('subscription'),
    })


@registry.handler('invoice.payment_succeeded')
def handle_invoice_payment_succeeded(invoice: dict) -> None:
    """
    Handle invoice.payment_succeeded.

    Fires the ``payment_succeeded`` signal so apps can update the user's
    plan, reset usage counters, etc.

    Emits outgoing webhook: ``invoice.payment_succeeded``
    """
    from .signals import payment_succeeded

    customer_id = invoice.get('customer')
    logger.info(
        '[WEBHOOK] invoice.payment_succeeded (customer=%s, amount=%s)',
        customer_id, invoice.get('amount_paid'),
    )

    payment_succeeded.send(
        sender=handle_invoice_payment_succeeded,
        invoice=invoice,
        user=None,
        user_payment=None,
    )

    fire_outgoing_webhook('invoice.payment_succeeded', {
        'customer_id': customer_id,
        'amount_paid': invoice.get('amount_paid'),
        'currency': invoice.get('currency', 'gbp'),
        'subscription': invoice.get('subscription'),
    })


@registry.handler('invoice.payment_failed')
def handle_invoice_payment_failed(invoice: dict) -> None:
    """
    Handle invoice.payment_failed.

    Fires the ``payment_failed`` signal so apps can send failure emails
    with escalating urgency.

    Emits outgoing webhook: ``invoice.payment_failed``
    """
    from .signals import payment_failed

    customer_id = invoice.get('customer')
    attempt_count = invoice.get('attempt_count', 1)
    next_attempt = invoice.get('next_payment_attempt')

    logger.warning(
        '[WEBHOOK] invoice.payment_failed (customer=%s, attempt=%s, next=%s)',
        customer_id, attempt_count, 'scheduled' if next_attempt else 'NONE',
    )

    payment_failed.send(
        sender=handle_invoice_payment_failed,
        invoice=invoice,
        user=None,
        attempt_count=attempt_count,
    )

    fire_outgoing_webhook('invoice.payment_failed', {
        'customer_id': customer_id,
        'customer_email': invoice.get('customer_email'),
        'attempt_count': attempt_count,
        'has_next_attempt': bool(next_attempt),
    })


@registry.handler('customer.subscription.updated')
def handle_subscription_updated(subscription: dict) -> None:
    """
    Handle customer.subscription.updated.

    Fires the ``subscription_updated`` signal.

    Emits outgoing webhook: ``customer.subscription.updated``
    """
    from .signals import subscription_updated

    customer_id = subscription.get('customer')
    logger.info('[WEBHOOK] customer.subscription.updated (customer=%s)', customer_id)

    subscription_updated.send(
        sender=handle_subscription_updated,
        subscription=subscription,
        user=None,
    )

    fire_outgoing_webhook('customer.subscription.updated', {
        'customer_id': customer_id,
        'status': subscription.get('status'),
        'cancel_at_period_end': subscription.get('cancel_at_period_end', False),
    })


@registry.handler('customer.subscription.deleted')
def handle_subscription_deleted(subscription: dict) -> None:
    """
    Handle customer.subscription.deleted.

    Fires the ``subscription_deleted`` signal so apps can downgrade the
    user to the free plan and send a cancellation email.

    Emits outgoing webhook: ``customer.subscription.deleted``
    """
    from .signals import subscription_deleted

    customer_id = subscription.get('customer')
    logger.info('[WEBHOOK] customer.subscription.deleted (customer=%s)', customer_id)

    subscription_deleted.send(
        sender=handle_subscription_deleted,
        subscription=subscription,
        user=None,
        old_plan='',
    )

    fire_outgoing_webhook('customer.subscription.deleted', {
        'customer_id': customer_id,
        'status': subscription.get('status'),
    })


@registry.handler('charge.refunded')
def handle_charge_refunded(charge: dict) -> None:
    """
    Handle charge.refunded.

    Fires the ``charge_refunded`` signal so apps can notify the user.

    Emits outgoing webhook: ``charge.refunded``
    """
    from .signals import charge_refunded
    from .utils import number_to_currency

    customer_id = charge.get('customer')
    amount_refunded = charge.get('amount_refunded', 0)
    currency = charge.get('currency', 'gbp')
    amount_display = number_to_currency(str(amount_refunded), currency)

    logger.info(
        '[WEBHOOK] charge.refunded (customer=%s, amount=%s %s)',
        customer_id, amount_refunded, currency,
    )

    charge_refunded.send(
        sender=handle_charge_refunded,
        charge=charge,
        user=None,
        amount_display=amount_display,
    )

    fire_outgoing_webhook('charge.refunded', {
        'customer_id': customer_id,
        'amount_refunded': amount_refunded,
        'currency': currency,
        'amount_display': amount_display,
    })


@registry.handler('customer.subscription.trial_will_end')
def handle_trial_will_end(subscription: dict) -> None:
    """
    Handle customer.subscription.trial_will_end (3 days before trial end).

    Fires the ``trial_will_end`` signal so apps can warn the user.

    Emits outgoing webhook: ``customer.subscription.trial_will_end``
    """
    import datetime as dt

    from .signals import trial_will_end

    customer_id = subscription.get('customer')
    trial_end_ts = subscription.get('trial_end')
    trial_end_date = 'soon'
    if trial_end_ts:
        trial_end_date = dt.datetime.fromtimestamp(
            trial_end_ts, tz=dt.timezone.utc
        ).strftime('%d %B %Y')

    logger.info(
        '[WEBHOOK] customer.subscription.trial_will_end (customer=%s, ends=%s)',
        customer_id, trial_end_date,
    )

    trial_will_end.send(
        sender=handle_trial_will_end,
        subscription=subscription,
        user=None,
        trial_end_date=trial_end_date,
    )

    fire_outgoing_webhook('customer.subscription.trial_will_end', {
        'customer_id': customer_id,
        'trial_end': trial_end_ts,
        'trial_end_date': trial_end_date,
    })


@registry.handler('customer.subscription.paused')
def handle_subscription_paused(subscription: dict) -> None:
    """
    Handle customer.subscription.paused.

    Fires the ``subscription_paused`` signal.

    Emits outgoing webhook: ``customer.subscription.paused``
    """
    from .signals import subscription_paused

    customer_id = subscription.get('customer')
    logger.info('[WEBHOOK] customer.subscription.paused (customer=%s)', customer_id)

    subscription_paused.send(
        sender=handle_subscription_paused,
        subscription=subscription,
        user=None,
    )

    fire_outgoing_webhook('customer.subscription.paused', {
        'customer_id': customer_id,
    })


@registry.handler('payment_method.attached')
def handle_payment_method_attached(payment_method: dict) -> None:
    """
    Handle payment_method.attached.

    Fires the ``payment_method_attached`` signal so apps can notify the
    user their payment method was updated.

    Emits outgoing webhook: ``payment_method.attached``
    """
    from .signals import payment_method_attached

    customer_id = payment_method.get('customer')
    logger.info(
        '[WEBHOOK] payment_method.attached (customer=%s, type=%s)',
        customer_id, payment_method.get('type'),
    )

    payment_method_attached.send(
        sender=handle_payment_method_attached,
        payment_method=payment_method,
        user=None,
    )

    fire_outgoing_webhook('payment_method.attached', {
        'customer_id': customer_id,
        'payment_method_type': payment_method.get('type'),
    })
