"""Channel abstraction. Concrete channels are pluggable.

Only the inbox channel is "real" out of the box (it persists the row to the
DB so the user's inbox can render it). Email/SMS/WebSocket channels log and
stub — Phase 8 wires them to real transports.
"""

from __future__ import annotations

from typing import Protocol

import structlog
from django.utils import timezone

from simorgh.apps.notifications.models import Notification, SmsDeliveryStatus

_log = structlog.get_logger("simorgh.notifications")


class NotificationChannelBackend(Protocol):
    name: str

    def send(self, notification: Notification) -> None: ...


class InboxChannel:
    name = "inbox"

    def send(self, notification: Notification) -> None:
        # The row was already persisted by the dispatcher; mark delivered.
        notification.delivered_at = timezone.now()
        notification.save(update_fields=("delivered_at",))


class EmailChannel:
    name = "email"

    def send(self, notification: Notification) -> None:
        _log.info(
            "notifications.email.stub",
            notification_id=str(notification.public_id),
            recipient_id=notification.recipient_id,
            title=notification.title,
        )
        notification.delivered_at = timezone.now()
        notification.save(update_fields=("delivered_at",))


class SmsChannel:
    """SMS channel — routes through the registered provider registry.

    The provider is selected automatically based on the recipient's mobile
    number dial code.  A delivery log (``SmsDeliveryLog``) is written for
    every attempt regardless of outcome.

    Falls back gracefully to a stub log entry when no provider is registered
    or no provider matches the number (e.g. during initial setup / tests).
    """

    name = "sms"

    def send(self, notification: Notification) -> None:
        from simorgh.apps.notifications.sms import get_provider_for_mobile
        from simorgh.apps.notifications.sms.base import SmsProviderNotFound
        from simorgh.apps.notifications.sms.rate_limiter import check_rate_limit
        from simorgh.apps.notifications.models import SmsDeliveryLog, SmsProvider, TenantSmsRateLimit

        # Resolve recipient's mobile number from payload or body
        mobile: str = (
            notification.payload.get("mobile")
            or notification.payload.get("phone")
            or ""
        )
        if not mobile:
            _log.warning(
                "notifications.sms.no_mobile",
                notification_id=str(notification.public_id),
                recipient_id=notification.recipient_id,
            )
            return

        tenant_id: int | None = (
            notification.tenant_id if hasattr(notification, "tenant_id") else None
        )

        # --- Rate limit check ---
        if tenant_id:
            try:
                rate_cfg = TenantSmsRateLimit.objects.filter(
                    tenant_id=tenant_id, is_active=True
                ).first()
                if rate_cfg:
                    rl = check_rate_limit(
                        tenant_id=tenant_id,
                        max_per_minute=rate_cfg.max_per_minute,
                        max_per_hour=rate_cfg.max_per_hour,
                        max_per_day=rate_cfg.max_per_day,
                    )
                    if not rl.allowed:
                        _log.warning(
                            "notifications.sms.rate_limited",
                            tenant_id=tenant_id,
                            mobile=mobile,
                            limit=rl.limit_name,
                            current=rl.current_count,
                            max=rl.limit_value,
                        )
                        raise RuntimeError(
                            f"SMS rate limit exceeded for tenant {tenant_id}: "
                            f"{rl.current_count}/{rl.limit_value} {rl.limit_name}"
                        )
            except RuntimeError:
                raise
            except Exception as exc:
                _log.warning(
                    "notifications.sms.rate_limit_check_error",
                    error=str(exc),
                    tenant_id=tenant_id,
                )

        # --- Provider selection ---
        try:
            provider = get_provider_for_mobile(mobile)
        except SmsProviderNotFound:
            _log.warning(
                "notifications.sms.no_provider",
                mobile=mobile,
                notification_id=str(notification.public_id),
            )
            return

        # --- Determine provider DB record for logging ---
        try:
            provider_record = SmsProvider.objects.filter(
                slug=provider.slug, is_active=True
            ).first()
        except Exception:
            provider_record = None

        # --- Send ---
        # 1. If a SmsProviderTemplate is registered for this notification kind,
        #    use send_template (sms.ir / Kavenegar pattern-based API).
        # 2. Fall back to OTP path if kind looks like an OTP.
        # 3. Otherwise send plain text via bulk API.
        result = None
        if provider_record:
            from simorgh.apps.notifications.models import SmsProviderTemplate
            tmpl = SmsProviderTemplate.objects.filter(
                provider=provider_record,
                name=notification.kind,
            ).first()
            if tmpl and hasattr(provider, "send_template"):
                # Pass all payload keys except the mobile itself as template params.
                params = {
                    k: str(v)
                    for k, v in notification.payload.items()
                    if k not in ("mobile", "phone")
                }
                result = provider.send_template(
                    mobile=mobile,
                    template_id=tmpl.provider_template_id,
                    params=params,
                    tenant_id=tenant_id,
                )

        if result is None:
            # OTP shortcut
            is_otp = "otp" in notification.kind.lower() or "verify" in notification.kind.lower()
            otp_code = notification.payload.get("code") or notification.payload.get("otp")
            if is_otp and otp_code:
                result = provider.send_otp(
                    mobile=mobile,
                    code=str(otp_code),
                    tenant_id=tenant_id,
                )
            else:
                result = provider.send(
                    mobile=mobile,
                    text=notification.body or notification.title,
                    tenant_id=tenant_id,
                )

        # --- Log delivery ---
        if provider_record:
            try:
                SmsDeliveryLog.objects.create(
                    provider=provider_record,
                    tenant_id=tenant_id,
                    mobile=mobile,
                    message_text=notification.body or notification.title,
                    template_id=str(notification.payload.get("template_id", "")),
                    status=SmsDeliveryStatus.SENT if result.success else SmsDeliveryStatus.FAILED,
                    provider_message_id=result.provider_message_id,
                    cost=result.cost,
                    error_message=result.error,
                    raw_response=result.raw_response,
                )
            except Exception as log_exc:
                _log.warning(
                    "notifications.sms.log_error",
                    error=str(log_exc),
                    mobile=mobile,
                )

        if not result.success:
            raise RuntimeError(f"SMS provider {provider.slug} error: {result.error}")

        notification.delivered_at = timezone.now()
        notification.save(update_fields=("delivered_at",))

        _log.info(
            "notifications.sms.sent",
            provider=provider.slug,
            mobile=mobile,
            message_id=result.provider_message_id,
            tenant_id=tenant_id,
        )


class WebSocketChannel:
    name = "websocket"

    def send(self, notification: Notification) -> None:
        _log.info(
            "notifications.websocket.stub",
            notification_id=str(notification.public_id),
        )


class WebPushChannel:
    """Web Push (RFC 8030) channel.

    Requires ``pywebpush`` and VAPID keys configured in settings::

        NOTIFICATIONS_VAPID_PRIVATE_KEY = "..."
        NOTIFICATIONS_VAPID_CLAIMS_EMAIL = "mailto:admin@example.com"

    Falls back to a stub log when keys are not configured so the app still
    starts in development.
    """

    name = "webpush"

    def send(self, notification: Notification) -> None:
        from django.conf import settings

        from simorgh.apps.notifications.models import PushSubscription

        subs = PushSubscription.objects.filter(
            user_id=notification.recipient_id,
            is_active=True,
        )
        if not subs.exists():
            return

        private_key: str | None = getattr(settings, "NOTIFICATIONS_VAPID_PRIVATE_KEY", None)
        claims_email: str = getattr(
            settings, "NOTIFICATIONS_VAPID_CLAIMS_EMAIL", "mailto:admin@example.com"
        )

        import json

        payload_bytes = json.dumps(
            {
                "title": notification.title,
                "body": notification.body,
                "data": {"id": str(notification.public_id), "kind": notification.kind},
            }
        ).encode()

        for sub in subs:
            try:
                if private_key:
                    from pywebpush import webpush, WebPushException  # type: ignore[import]

                    webpush(
                        subscription_info={
                            "endpoint": sub.endpoint,
                            "keys": {
                                "p256dh": sub.p256dh_key,
                                "auth": sub.auth_key,
                            },
                        },
                        data=payload_bytes,
                        vapid_private_key=private_key,
                        vapid_claims={"sub": claims_email},
                    )
                    sub.last_used_at = timezone.now()
                    sub.save(update_fields=("last_used_at",))
                    _log.info(
                        "notifications.webpush.sent",
                        sub_id=str(sub.pk),
                        recipient_id=notification.recipient_id,
                    )
                else:
                    _log.info(
                        "notifications.webpush.stub_no_vapid_key",
                        sub_id=str(sub.pk),
                    )
            except Exception as exc:
                _log.warning(
                    "notifications.webpush.failed",
                    sub_id=str(sub.pk),
                    error=str(exc),
                )
                # Mark inactive on 410 Gone (subscription expired)
                error_str = str(exc)
                if "410" in error_str or "404" in error_str:
                    sub.is_active = False
                    sub.save(update_fields=("is_active",))

        notification.delivered_at = timezone.now()
        notification.save(update_fields=("delivered_at",))


_DEFAULT_BACKENDS: dict[str, NotificationChannelBackend] = {
    "inbox": InboxChannel(),
    "email": EmailChannel(),
    "sms": SmsChannel(),
    "websocket": WebSocketChannel(),
    "webpush": WebPushChannel(),
}


def get_backend(name: str) -> NotificationChannelBackend:
    try:
        return _DEFAULT_BACKENDS[name]
    except KeyError as exc:
        raise LookupError(f"no notification channel registered as {name!r}") from exc


def register_backend(backend: NotificationChannelBackend) -> None:
    _DEFAULT_BACKENDS[backend.name] = backend
