"""Realtime / pub-sub abstraction.

We don't ship Django Channels yet — the goal is a stable seam so a module
can say "notify everyone in this workspace" without caring about the
transport. The default :class:`LoggingRealtimeBackend` only writes to the
structured log; tests use :class:`MemoryRealtimeBackend`.

Switch backends via ``settings.REALTIME_BACKEND`` (dotted path).
"""

from __future__ import annotations

from collections import defaultdict
from typing import Any, Protocol, runtime_checkable

import structlog
from django.conf import settings
from django.utils.module_loading import import_string

_log = structlog.get_logger("simorgh.realtime")


@runtime_checkable
class RealtimeBackend(Protocol):
    """Minimal interface every realtime transport must satisfy."""

    def publish(self, room: str, event: str, payload: dict[str, Any]) -> None: ...


class LoggingRealtimeBackend:
    """Default backend — just logs publishes. Safe for prod until Channels arrives."""

    def publish(self, room: str, event: str, payload: dict[str, Any]) -> None:
        _log.info("realtime.publish", room=room, event_name=event, payload=payload)


class MemoryRealtimeBackend:
    """In-memory recorder for tests.

    Exposes ``messages`` as a dict ``{room: [(event, payload), ...]}``.
    """

    def __init__(self) -> None:
        self.messages: dict[str, list[tuple[str, dict[str, Any]]]] = defaultdict(list)

    def publish(self, room: str, event: str, payload: dict[str, Any]) -> None:
        self.messages[room].append((event, payload))

    def reset(self) -> None:
        self.messages.clear()


class ChannelsRealtimeBackend:
    """Realtime backend that broadcasts via Django Channels channel layer.

    Uses ``async_to_sync`` so it can be called from synchronous signal handlers.
    Channel group names are derived from the room string by replacing characters
    that are invalid in channel group names (colons) with underscores.
    """

    def publish(self, room: str, event: str, payload: dict[str, Any]) -> None:
        try:
            from asgiref.sync import async_to_sync
            from channels.layers import get_channel_layer

            channel_layer = get_channel_layer()
            if channel_layer is None:
                _log.warning("realtime.channels.no_layer", room=room, event=event)
                return
            # Channel group names must be ASCII word chars + dashes/periods only.
            group = room.replace(":", "_")
            async_to_sync(channel_layer.group_send)(
                group,
                {"type": "broadcast.message", "event": event, "payload": payload},
            )
        except Exception:
            _log.warning("realtime.channels.publish_error", room=room, event=event, exc_info=True)


# Module-level singleton resolution. Tests can override with set_backend().
_backend: RealtimeBackend | None = None


def get_backend() -> RealtimeBackend:
    global _backend
    if _backend is None:
        dotted = getattr(
            settings,
            "REALTIME_BACKEND",
            "simorgh.core.realtime.LoggingRealtimeBackend",
        )
        cls = import_string(dotted)
        _backend = cls()
    return _backend


def set_backend(backend: RealtimeBackend | None) -> None:
    """Override the active backend (testing seam)."""
    global _backend
    _backend = backend


# ---------------------------------------------------------------------------
# Room helpers — keep the naming consistent across modules.
# ---------------------------------------------------------------------------


def user_room(user_id: int) -> str:
    return f"user:{user_id}"


def workspace_room(workspace_id: int) -> str:
    return f"workspace:{workspace_id}"


def org_node_room(node_id: int) -> str:
    return f"org_node:{node_id}"


def publish_to_user(user_id: int, event: str, payload: dict[str, Any]) -> None:
    get_backend().publish(user_room(user_id), event, payload)


def publish_to_workspace(workspace_id: int, event: str, payload: dict[str, Any]) -> None:
    get_backend().publish(workspace_room(workspace_id), event, payload)


def publish_to_org_node(node_id: int, event: str, payload: dict[str, Any]) -> None:
    get_backend().publish(org_node_room(node_id), event, payload)


__all__ = [
    "ChannelsRealtimeBackend",
    "LoggingRealtimeBackend",
    "MemoryRealtimeBackend",
    "RealtimeBackend",
    "get_backend",
    "org_node_room",
    "publish_to_org_node",
    "publish_to_user",
    "publish_to_workspace",
    "set_backend",
    "user_room",
    "workspace_room",
]
