# retoor <retoor@molodetz.nl>

from __future__ import annotations

import logging

logger = logging.getLogger(__name__)


def topic_matches(pattern: str, topic: str) -> bool:
    if pattern == topic or pattern == "*":
        return True
    if pattern.endswith(".*"):
        prefix = pattern[:-1]
        return topic == pattern[:-2] or topic.startswith(prefix)
    return False


class PubSubHub:
    def __init__(self) -> None:
        self._subscriptions: dict[str, set] = {}

    def subscribe(self, pattern: str, socket) -> None:
        self._subscriptions.setdefault(pattern, set()).add(socket)

    def unsubscribe(self, pattern: str, socket) -> None:
        sockets = self._subscriptions.get(pattern)
        if not sockets:
            return
        sockets.discard(socket)
        if not sockets:
            self._subscriptions.pop(pattern, None)

    def drop_socket(self, socket) -> None:
        for pattern in list(self._subscriptions):
            self.unsubscribe(pattern, socket)

    def _targets(self, topic: str) -> set:
        targets: set = set()
        for pattern, sockets in self._subscriptions.items():
            if topic_matches(pattern, topic):
                targets.update(sockets)
        return targets

    async def publish(self, topic: str, frame: dict) -> int:
        targets = self._targets(topic)
        delivered = 0
        for socket in targets:
            try:
                await socket.send_json(frame)
                delivered += 1
            except Exception:
                logger.debug("pubsub dropping dead socket for %s", topic)
        return delivered

    def topics(self) -> list[dict]:
        return [
            {"topic": pattern, "subscribers": len(sockets)}
            for pattern, sockets in sorted(self._subscriptions.items())
        ]


pubsub = PubSubHub()
