"""
nodelink.room
~~~~~~~~~~~~~
Room — a named group of players for targeted broadcasts.
"""

import asyncio
import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Any

if TYPE_CHECKING:
    from .player import Player

logger = logging.getLogger("nodelink.room")


class Room:
    """
    A named group of players.  Players can join/leave rooms at any time.
    Rooms support targeted broadcasts to only their members.

    Rooms are created and managed through the server::

        lobby = server.create_room("lobby")

        @server.on("connect")
        def on_connect(player):
            lobby.add(player)
            lobby.broadcast("joined", {"id": player.id})

        @server.on("disconnect")
        def on_disconnect(player):
            # Players are removed from ALL rooms automatically on disconnect,
            # but you may also remove them manually at any time.
            pass

    Attributes:
        name: the name this room was created with
    """

    def __init__(self, name: str) -> None:
        self.name = name
        self._players: Dict[str, "Player"] = {}

    # ------------------------------------------------------------------
    # Player management
    # ------------------------------------------------------------------

    def add(self, player: "Player") -> None:
        """Add *player* to this room."""
        self._players[player.id] = player

    def remove(self, player: "Player") -> None:
        """Remove *player* from this room (no-op if they're not in it)."""
        self._players.pop(player.id, None)

    def _remove_by_id(self, player_id: str) -> None:
        """Internal — used by the server to clean up on disconnect."""
        self._players.pop(player_id, None)

    def has(self, player: "Player") -> bool:
        """Return ``True`` if *player* is currently in this room."""
        return player.id in self._players

    def get_players(self) -> List["Player"]:
        """Return a snapshot list of all players currently in this room."""
        return list(self._players.values())

    @property
    def player_count(self) -> int:
        """Number of players currently in this room."""
        return len(self._players)

    # ------------------------------------------------------------------
    # Broadcasting
    # ------------------------------------------------------------------

    def broadcast(
        self,
        event: str,
        data: Any = None,
        exclude: Optional["Player"] = None,
    ) -> None:
        """
        Send *event* / *data* to every player in this room.

        Args:
            event:   event name
            data:    payload (anything JSON-serialisable)
            exclude: optional player to skip — handy for skipping the sender

        Example::

            @server.on("chat")
            def on_chat(player, data):
                room = server.get_room("lobby")
                if room:
                    room.broadcast("chat_msg",
                                   {"from": player.id, "text": data["text"]},
                                   exclude=player)
        """
        asyncio.ensure_future(self._broadcast_async(event, data, exclude))

    async def _broadcast_async(
        self,
        event: str,
        data: Any,
        exclude: Optional["Player"],
    ) -> None:
        for player in list(self._players.values()):
            if exclude and player.id == exclude.id:
                continue
            try:
                await player.send(event, data)
            except Exception as e:
                logger.error(f"Room '{self.name}' broadcast error to {player.id}: {e}")

    # ------------------------------------------------------------------
    # Repr
    # ------------------------------------------------------------------

    def __repr__(self) -> str:
        return f"<Room name={self.name!r} players={self.player_count}>"
