import asyncio
from typing import Any

from fastapi import WebSocket
import orjson


def _dumps(obj: Any) -> str:
    return orjson.dumps(
        obj,
        option=orjson.OPT_PASSTHROUGH_DATETIME | orjson.OPT_SERIALIZE_NUMPY,
        default=str,
    ).decode("utf-8")


class ConnectionManager:
    """
    Manages active WebSocket connections organized by canal (channel).

    Typical canal names:
    - "signalements"         — all new reports (GESTIONNAIRE+)
    - "signalement:{id}"     — specific report updates
    - "discussion:{id}"      — messages in a specific discussion
    - "calendriers"          — event updates (all users)
    - "dashboard"            — admin dashboard updates
    - "admin"                — system-wide admin alerts
    - "user:{user_id}"       — personal notifications
    """

    def __init__(self) -> None:
        # canal -> list of (WebSocket, user_id) tuples
        self._connections: dict[str, list[tuple[WebSocket, str]]] = {}

    async def connect(self, canal: str, websocket: WebSocket, user_id: str) -> None:
        await websocket.accept()
        if canal not in self._connections:
            self._connections[canal] = []
        self._connections[canal].append((websocket, user_id))

    def disconnect(self, canal: str, websocket: WebSocket) -> None:
        if canal in self._connections:
            self._connections[canal] = [
                (ws, uid) for ws, uid in self._connections[canal] if ws is not websocket
            ]
            if not self._connections[canal]:
                del self._connections[canal]

    async def broadcast(self, canal: str, event: dict[str, Any]) -> None:
        """Send an event concurrently to all connections subscribed to a canal."""
        connections = self._connections.get(canal)
        if not connections:
            return

        payload = _dumps(event)
        targets = list(connections)

        async def _safe_send(ws: WebSocket) -> tuple[WebSocket, bool]:
            try:
                await ws.send_text(payload)
                return ws, True
            except Exception:
                return ws, False

        results = await asyncio.gather(*[_safe_send(ws) for ws, _ in targets])
        for ws, success in results:
            if not success:
                self.disconnect(canal, ws)

    async def broadcast_to_user(self, user_id: str, event: dict[str, Any]) -> None:
        """Send an event concurrently to all connections belonging to a specific user."""
        target_ws_canals: list[tuple[str, WebSocket]] = []
        for canal, connections in list(self._connections.items()):
            for websocket, uid in list(connections):
                if uid == user_id:
                    target_ws_canals.append((canal, websocket))

        if not target_ws_canals:
            return

        payload = _dumps(event)

        async def _safe_send_user(c: str, ws: WebSocket) -> tuple[str, WebSocket, bool]:
            try:
                await ws.send_text(payload)
                return c, ws, True
            except Exception:
                return c, ws, False

        results = await asyncio.gather(*[_safe_send_user(c, ws) for c, ws in target_ws_canals])
        for c, ws, success in results:
            if not success:
                self.disconnect(c, ws)

    def connection_count(self, canal: str) -> int:
        return len(self._connections.get(canal, []))

    def all_canals(self) -> list[str]:
        return list(self._connections.keys())


# Singleton instance used across the application
manager = ConnectionManager()
