"""
WebSocketBackend – push notifications over Django Channels groups.

Safe to import without Channels installed; dependency is imported lazily during
setup so projects that don't use websockets are unaffected.
"""
from __future__ import annotations

import importlib
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any

from asgiref.sync import async_to_sync

from django_notify_hub.backends.base import AbstractNotificationBackend
from django_notify_hub.conf import app_settings
from django_notify_hub.exceptions import BackendError
from django_notify_hub.types import NotificationChannel

if TYPE_CHECKING:
    from django_notify_hub.types import NotificationPayload


class WebSocketBackend(AbstractNotificationBackend):
    """Deliver notifications to websocket groups through Django Channels."""

    channel = NotificationChannel.WEBSOCKET

    def setup(self, **kwargs: Any) -> None:
        self._event_type = app_settings.WEBSOCKET_EVENT_TYPE
        self._group_template = app_settings.WEBSOCKET_USER_GROUP_TEMPLATE
        self._group_resolver = self._load_callable(app_settings.WEBSOCKET_GROUP_RESOLVER)
        self._channel_layer = self._load_channel_layer()
        self._group_send = async_to_sync(self._channel_layer.group_send)

    def _deliver(self, payload: "NotificationPayload", recipient: Any) -> bool:
        groups = self._resolve_groups(recipient, payload)
        if not groups:
            return False

        recipient_id = getattr(recipient, "pk", recipient)
        message = self._build_message(payload, [recipient_id])
        for group in groups:
            self._group_send(group, message)
        return True

    def _deliver_many(self, payload: "NotificationPayload", recipients: Any) -> int:
        delivered = 0
        grouped_recipient_ids: dict[str, list[Any]] = {}
        seen_ids_by_group: dict[str, set[Any]] = {}

        for recipient in recipients:
            groups = self._resolve_groups(recipient, payload)
            if not groups:
                continue

            recipient_id = getattr(recipient, "pk", recipient)
            for group in groups:
                group_seen_ids = seen_ids_by_group.setdefault(group, set())
                if recipient_id in group_seen_ids:
                    continue

                group_seen_ids.add(recipient_id)
                grouped_recipient_ids.setdefault(group, []).append(recipient_id)
            delivered += 1

        for group, recipient_ids in grouped_recipient_ids.items():
            self._group_send(group, self._build_message(payload, recipient_ids))

        return delivered

    def _load_channel_layer(self) -> Any:
        try:
            layers = importlib.import_module("channels.layers")
        except ImportError as exc:
            raise ImportError(
                "WebSocketBackend requires Django Channels. Install it in the host project "
                "or package extra before enabling this backend."
            ) from exc

        layer = layers.get_channel_layer(alias=app_settings.WEBSOCKET_LAYER_ALIAS)
        if layer is None:
            raise BackendError("WebSocketBackend", "No Django Channels layer is configured.")
        return layer

    def _load_callable(self, value: Any) -> Any:
        if callable(value) or value is None:
            return value

        try:
            module_path, attr_name = value.rsplit(".", 1)
            module = importlib.import_module(module_path)
            return getattr(module, attr_name)
        except (AttributeError, ImportError, ValueError) as exc:
            raise BackendError(
                "WebSocketBackend",
                f"Invalid WEBSOCKET_GROUP_RESOLVER: {value}",
                exc,
            ) from exc

    def _resolve_groups(self, recipient: Any, payload: "NotificationPayload") -> list[str]:
        raw_groups = (
            self._group_resolver(recipient, payload)
            if self._group_resolver is not None
            else self._group_template.format(user_id=getattr(recipient, "pk", recipient))
        )
        if isinstance(raw_groups, str):
            groups = [raw_groups]
        elif isinstance(raw_groups, Iterable):
            groups = [str(group) for group in raw_groups if group]
        else:
            groups = []
        return list(dict.fromkeys(groups))

    def _build_message(
        self,
        payload: "NotificationPayload",
        recipient_ids: list[Any],
    ) -> dict[str, Any]:
        notification = {
            "channel": self.channel.value,
            "recipient_ids": recipient_ids,
            "title": payload.title,
            "body": payload.body,
            "data": payload.data,
            "priority": payload.priority.value,
        }
        if len(recipient_ids) == 1:
            notification["recipient_id"] = recipient_ids[0]

        return {
            "type": self._event_type,
            "notification": notification,
        }