from __future__ import annotations

from collections.abc import Iterable
from typing import Any

import structlog
from django.contrib.auth import get_user_model
from django.utils import timezone

from simorgh.apps.notifications.channels import get_backend
from simorgh.apps.notifications.models import (
    Notification,
    NotificationDeliveryStatus,
    NotificationPreference,
)
from simorgh.apps.notifications.templates import get_template, render
from simorgh.core.context import current_request_context

_log = structlog.get_logger("simorgh.notifications")

User = get_user_model()


def _user_locale(user_obj: Any) -> str:
    """Return the user's preferred locale (Phase 11.I), falling back to 'en'."""
    if user_obj is None:
        return "en"
    try:
        from simorgh.core.locale import get_user_locale

        return get_user_locale(user_obj) or "en"
    except Exception:
        return "en"


def is_channel_enabled(*, tenant_id: int, user_id: int, kind: str, channel: str) -> bool:
    """Resolve a user's per-(kind,channel) opt-in. Default: enabled."""
    pref = NotificationPreference.objects.filter(
        tenant_id=tenant_id,
        user_id=user_id,
        kind=kind,
        channel=channel,
    ).first()
    return pref.enabled if pref is not None else True


def set_preference(
    *,
    tenant_id: int,
    organization_node_id: int,
    user_id: int,
    kind: str,
    channel: str,
    enabled: bool,
) -> NotificationPreference:
    pref, _created = NotificationPreference.objects.update_or_create(
        tenant_id=tenant_id,
        user_id=user_id,
        kind=kind,
        channel=channel,
        defaults={"enabled": enabled, "organization_node_id": organization_node_id},
    )
    return pref


def deliver(notification: Notification) -> None:
    """Attempt one delivery, updating status / attempt / last_error."""
    backend = get_backend(notification.channel)
    notification.attempt = (notification.attempt or 0) + 1
    try:
        backend.send(notification)
    except Exception as exc:
        notification.last_error = str(exc)
        if notification.attempt >= notification.max_attempts:
            notification.status = NotificationDeliveryStatus.DEAD
        else:
            notification.status = NotificationDeliveryStatus.FAILED
        notification.save(
            update_fields=("status", "attempt", "last_error")
        )
        _log.warning(
            "notifications.delivery_failed",
            notification_id=str(notification.public_id),
            channel=notification.channel,
            attempt=notification.attempt,
            error=str(exc),
        )
        return

    notification.status = NotificationDeliveryStatus.DELIVERED
    notification.last_error = ""
    if notification.delivered_at is None:
        notification.delivered_at = timezone.now()
    notification.save(
        update_fields=("status", "attempt", "last_error", "delivered_at")
    )

    # Push to WebSocket channel layer for real-time delivery
    if notification.channel == "inbox":
        _broadcast_notification(notification)


def _broadcast_notification(notification: Notification) -> None:
    """Send notification.new + updated count over the channel layer (fire-and-forget)."""
    try:
        from asgiref.sync import async_to_sync
        from channels.layers import get_channel_layer
        from simorgh.apps.notifications.consumer import notification_group_name
        from simorgh.apps.notifications.selectors import count_unread

        layer = get_channel_layer()
        if layer is None:
            return

        group = notification_group_name(notification.recipient_id)
        unread = count_unread(notification.recipient_id, notification.tenant_id)

        payload = {
            "id": notification.pk,
            "public_id": str(notification.public_id),
            "kind": notification.kind,
            "title": notification.title,
            "body": notification.body,
            "created_at": notification.created_at.isoformat(),
        }
        send = async_to_sync(layer.group_send)
        send(group, {"type": "notification.new", "payload": payload})
        send(group, {"type": "notification.count", "payload": {"unread": unread}})
    except Exception as exc:
        _log.warning("notifications.broadcast_failed", error=str(exc))



def _is_in_quiet_hours(*, user_id: int, tenant_id: int, kind: str) -> bool:
    """Return True if the user's quiet-hours window is active right now.

    Critical notification types are never deferred.
    """
    from simorgh.apps.notifications.registry import get_notification_type

    spec = get_notification_type(kind)
    if spec and spec.is_critical:
        return False

    try:
        from simorgh.apps.notifications.models import UserQuietHours
        import zoneinfo
        import datetime as _dt

        qh = UserQuietHours.objects.filter(
            user_id=user_id, tenant_id=tenant_id, is_enabled=True
        ).first()
        if qh is None:
            return False

        try:
            tz = zoneinfo.ZoneInfo(qh.timezone or "UTC")
        except Exception:
            tz = zoneinfo.ZoneInfo("UTC")

        now_time = timezone.now().astimezone(tz).time()
        start, end = qh.start, qh.end

        # Handles overnight windows (e.g. 23:00–07:00)
        if start <= end:
            return start <= now_time < end
        else:
            return now_time >= start or now_time < end
    except Exception:
        return False


def dispatch(
    kind: str,
    *,
    recipients: Iterable[Any],
    context: dict[str, Any] | None = None,
    channels: Iterable[str] | None = None,
    tenant_id: int | None = None,
    organization_node_id: int | None = None,
) -> list[Notification]:
    """Render and deliver a notification to every recipient on every channel.

    `recipients` may be user PKs or User instances. `context` feeds template
    string-formatting. Channel delivery failures are tracked on the row
    (status, attempt, last_error) but never bubble.
    """
    template = get_template(kind)
    ctx = current_request_context()
    if tenant_id is None and ctx.tenant is not None:
        tenant_id = ctx.tenant.pk
    if tenant_id is None:
        raise ValueError(
            "notifications.dispatch requires a tenant (bind RequestContext or pass tenant_id)"
        )
    if organization_node_id is None and ctx.org_node_ids:
        organization_node_id = next(iter(ctx.org_node_ids))
    if organization_node_id is None:
        raise ValueError("notifications.dispatch requires organization_node_id")

    chans = tuple(channels) if channels is not None else template.default_channels
    rendered_per_lang: dict[str, tuple[str, str]] = {}
    created: list[Notification] = []

    def _render_for_lang(lang: str) -> tuple[str, str]:
        """Try DB MessageTemplate first, fall back to in-code registry."""
        try:
            from simorgh.apps.platform_core.models import MessageTemplate as MsgTmpl
            db_tmpl = (
                MsgTmpl.objects.filter(
                    tenant_id=tenant_id,
                    code=kind,
                    channel="inapp",
                    language=lang,
                    is_active=True,
                ).first()
                or MsgTmpl.objects.filter(
                    tenant_id=tenant_id,
                    code=kind,
                    channel="inapp",
                    language="en",
                    is_active=True,
                ).first()
            )
            if db_tmpl is not None:
                from jinja2.sandbox import SandboxedEnvironment
                env = SandboxedEnvironment(autoescape=False)
                rendered_body = env.from_string(db_tmpl.body).render(**(context or {}))
                rendered_subject = (
                    env.from_string(db_tmpl.subject).render(**(context or {}))
                    if db_tmpl.subject
                    else ""
                )
                return rendered_subject, rendered_body
        except Exception:
            pass
        return render(template, context or {}, lang=lang)

    for recipient in recipients:
        if isinstance(recipient, User):
            recipient_obj = recipient
            recipient_id = recipient.pk
        else:
            recipient_obj = None
            recipient_id = recipient
        lang = _user_locale(recipient_obj)
        if lang not in rendered_per_lang:
            rendered_per_lang[lang] = _render_for_lang(lang)
        title, body = rendered_per_lang[lang]

        for channel in chans:
            if not is_channel_enabled(
                tenant_id=tenant_id,
                user_id=recipient_id,
                kind=kind,
                channel=channel,
            ):
                _log.info(
                    "notifications.skipped_by_preference",
                    kind=kind,
                    channel=channel,
                    user_id=recipient_id,
                )
                continue
            notification = Notification.objects.create(
                tenant_id=tenant_id,
                organization_node_id=organization_node_id,
                recipient_id=recipient_id,
                kind=kind,
                channel=channel,
                title=title,
                body=body,
                payload=context or {},
            )
            # Check quiet hours — skip delivery now; leave as PENDING for retry
            if _is_in_quiet_hours(user_id=recipient_id, tenant_id=tenant_id, kind=kind):
                _log.info(
                    "notifications.deferred_by_quiet_hours",
                    kind=kind,
                    channel=channel,
                    user_id=recipient_id,
                )
                created.append(notification)
                continue
            deliver(notification)
            created.append(notification)

    return created


def retry_failed(*, limit: int = 100) -> dict[str, int]:
    """Re-attempt delivery for notifications stuck in FAILED status."""
    qs = Notification.objects.filter(status=NotificationDeliveryStatus.FAILED).order_by(
        "created_at"
    )[:limit]
    delivered = 0
    failed = 0
    dead = 0
    for n in qs:
        deliver(n)
        if n.status == NotificationDeliveryStatus.DELIVERED:
            delivered += 1
        elif n.status == NotificationDeliveryStatus.DEAD:
            dead += 1
        else:
            failed += 1
    return {"delivered": delivered, "failed": failed, "dead": dead}
