"""Game namespace event handlers for WebSocket communication.

This module implements all /game namespace event handling for real-time
game communication. It orchestrates between the WebSocket layer and
GameService to handle:
- Game joining and state synchronization
- Action execution and result broadcasting
- Resignation handling
- Disconnect notifications to opponents
- Automatic reconnection to active games

Architecture:
    Socket.IO Events -> GameNamespaceHandler -> GameService
                     -> ConnectionManager (for routing)
                     -> GameStateManager (for active game lookup)
                     -> Socket.IO Emits (responses)

The handler is designed with dependency injection for testability.
All game logic is delegated to GameService; this module only handles
the WebSocket communication layer.

Example:
    from app.socketio.game_namespace import game_namespace_handler

    @sio.on("game:join", namespace="/game")
    async def on_game_join(sid: str, data: dict) -> dict:
        return await game_namespace_handler.handle_join(sio, sid, data)
"""

import logging
from typing import TYPE_CHECKING, Any
from uuid import UUID

from pydantic import ValidationError

from app.core.models.actions import parse_action
from app.core.visibility import get_spectator_state, get_visible_state
from app.schemas.ws_messages import (
    ConnectionStatus,
    GameOverMessage,
    GameStateMessage,
    OpponentStatusMessage,
    WSErrorCode,
)
from app.services.connection_manager import ConnectionManager, connection_manager
from app.services.game_service import (
    CannotSpectateOwnGameError,
    ForcedActionRequiredError,
    GameAlreadyEndedError,
    GameNotFoundError,
    GameService,
    InvalidActionError,
    NotPlayerTurnError,
    PlayerNotInGameError,
    game_service,
)
from app.services.game_state_manager import GameStateManager, game_state_manager

if TYPE_CHECKING:
    import socketio

logger = logging.getLogger(__name__)


class GameNamespaceHandler:
    """Handler for /game namespace WebSocket events.

    This class encapsulates all game-related event handling logic with
    dependencies injected via constructor for testability.

    Attributes:
        _game_service: GameService for game operations.
        _connection_manager: ConnectionManager for connection tracking.
        _state_manager: GameStateManager for active game lookup.
    """

    def __init__(
        self,
        game_svc: GameService | None = None,
        conn_manager: ConnectionManager | None = None,
        state_manager: GameStateManager | None = None,
    ) -> None:
        """Initialize the GameNamespaceHandler.

        Args:
            game_svc: GameService instance. Uses global if not provided.
            conn_manager: ConnectionManager instance. Uses global if not provided.
            state_manager: GameStateManager instance. Uses global if not provided.
        """
        self._game_service = game_svc or game_service
        self._connection_manager = conn_manager or connection_manager
        self._state_manager = state_manager or game_state_manager

    # =========================================================================
    # Event Handlers
    # =========================================================================

    async def handle_join(
        self,
        sio: "socketio.AsyncServer",
        sid: str,
        user_id: str,
        data: dict[str, Any],
    ) -> dict[str, Any]:
        """Handle game:join event.

        Joins or rejoins a player to a game session. On success, the player
        is added to the game room and receives the current game state.

        Args:
            sio: Socket.IO server instance.
            sid: Socket session ID.
            user_id: Authenticated user's ID.
            data: Message data with game_id and optional last_event_id.

        Returns:
            Response dict with success status and game state or error.
        """
        game_id = data.get("game_id")
        last_event_id = data.get("last_event_id")
        message_id = data.get("message_id", "")

        if not game_id:
            logger.warning(f"game:join missing game_id from {sid}")
            return self._error_response(
                WSErrorCode.INVALID_MESSAGE,
                "game_id is required",
                message_id,
            )

        try:
            # Join the game via GameService
            result = await self._game_service.join_game(
                game_id=game_id,
                player_id=user_id,
                last_event_id=last_event_id,
            )

            if not result.success:
                error_code = WSErrorCode.GAME_NOT_FOUND
                if "not a participant" in result.message:
                    error_code = WSErrorCode.NOT_IN_GAME
                return self._error_response(error_code, result.message, message_id)

            # Register this connection with the game
            await self._connection_manager.join_game(sid, game_id)

            # Join the Socket.IO room for this game
            await sio.enter_room(sid, f"game:{game_id}", namespace="/game")

            # Notify opponent that player connected
            await self._notify_opponent_status(
                sio, sid, game_id, user_id, ConnectionStatus.CONNECTED
            )

            # Build response with game state
            response: dict[str, Any] = {
                "success": True,
                "game_id": game_id,
                "is_your_turn": result.is_your_turn,
                "game_over": result.game_over,
            }

            if result.visible_state:
                response["state"] = result.visible_state.model_dump(mode="json")

            if result.pending_forced_action:
                response["pending_forced_action"] = {
                    "player_id": result.pending_forced_action.player_id,
                    "action_type": result.pending_forced_action.action_type,
                    "reason": result.pending_forced_action.reason,
                    "params": result.pending_forced_action.params,
                }

            # Include turn timer info if enabled
            if result.turn_timeout_seconds is not None:
                response["turn_timeout_seconds"] = result.turn_timeout_seconds
                response["turn_deadline"] = result.turn_deadline

            logger.info(f"Player {user_id} joined game {game_id}")
            return response

        except Exception as e:
            logger.exception(f"Error joining game {game_id}: {e}")
            return self._error_response(
                WSErrorCode.INTERNAL_ERROR,
                "Failed to join game",
                message_id,
            )

    async def handle_action(
        self,
        sio: "socketio.AsyncServer",
        sid: str,
        user_id: str,
        data: dict[str, Any],
    ) -> dict[str, Any]:
        """Handle game:action event.

        Executes a game action and broadcasts results to all participants.

        Args:
            sio: Socket.IO server instance.
            sid: Socket session ID.
            user_id: Authenticated user's ID.
            data: Message data with game_id and action.

        Returns:
            Response dict with action result or error.
        """
        game_id = data.get("game_id")
        action_data = data.get("action")
        message_id = data.get("message_id", "")

        if not game_id:
            return self._error_response(
                WSErrorCode.INVALID_MESSAGE,
                "game_id is required",
                message_id,
            )

        if not action_data:
            return self._error_response(
                WSErrorCode.INVALID_MESSAGE,
                "action is required",
                message_id,
            )

        # Parse the action
        try:
            action = parse_action(action_data)
        except (ValidationError, ValueError) as e:
            logger.warning(f"Invalid action from {sid}: {e}")
            return self._error_response(
                WSErrorCode.INVALID_ACTION,
                f"Invalid action format: {e}",
                message_id,
            )

        # Execute the action
        try:
            result = await self._game_service.execute_action(
                game_id=game_id,
                player_id=user_id,
                action=action,
            )

            # Broadcast state to all participants
            await self._broadcast_game_state(sio, game_id)

            # Build response
            response: dict[str, Any] = {
                "success": True,
                "game_id": game_id,
                "action_type": result.action_type,
                "message": result.message,
                "turn_changed": result.turn_changed,
                "current_player_id": result.current_player_id,
            }

            if result.state_changes:
                response["changes"] = result.state_changes

            if result.pending_forced_action:
                response["pending_forced_action"] = {
                    "player_id": result.pending_forced_action.player_id,
                    "action_type": result.pending_forced_action.action_type,
                    "reason": result.pending_forced_action.reason,
                    "params": result.pending_forced_action.params,
                }

            # Include turn timer info if enabled and turn changed
            if result.turn_timeout_seconds is not None:
                response["turn_timeout_seconds"] = result.turn_timeout_seconds
                response["turn_deadline"] = result.turn_deadline

            # Handle game over
            if result.game_over:
                response["game_over"] = True
                response["winner_id"] = result.winner_id
                response["end_reason"] = result.end_reason.value if result.end_reason else None

                # End the game and archive to history
                if result.end_reason:
                    await self._game_service.end_game(
                        game_id=game_id,
                        winner_id=result.winner_id,
                        end_reason=result.end_reason,
                    )

                # Broadcast game over to room
                await self._broadcast_game_over(sio, game_id, result.winner_id, result.end_reason)

            logger.debug(f"Action executed: game={game_id}, player={user_id}, type={action.type}")
            return response

        except GameNotFoundError:
            return self._error_response(
                WSErrorCode.GAME_NOT_FOUND,
                f"Game {game_id} not found",
                message_id,
            )
        except PlayerNotInGameError:
            return self._error_response(
                WSErrorCode.NOT_IN_GAME,
                "You are not a participant in this game",
                message_id,
            )
        except GameAlreadyEndedError:
            return self._error_response(
                WSErrorCode.GAME_ENDED,
                "Game has already ended",
                message_id,
            )
        except NotPlayerTurnError as e:
            return self._error_response(
                WSErrorCode.NOT_YOUR_TURN,
                f"Not your turn. Current player: {e.current_player_id}",
                message_id,
            )
        except ForcedActionRequiredError as e:
            return self._error_response(
                WSErrorCode.ACTION_NOT_ALLOWED,
                f"Forced action required: {e.required_action_type}",
                message_id,
            )
        except InvalidActionError as e:
            return self._error_response(
                WSErrorCode.INVALID_ACTION,
                e.reason,
                message_id,
            )
        except Exception as e:
            logger.exception(f"Error executing action in game {game_id}: {e}")
            return self._error_response(
                WSErrorCode.INTERNAL_ERROR,
                "Failed to execute action",
                message_id,
            )

    async def handle_resign(
        self,
        sio: "socketio.AsyncServer",
        sid: str,
        user_id: str,
        data: dict[str, Any],
    ) -> dict[str, Any]:
        """Handle game:resign event.

        Processes a player's resignation from the game.

        Args:
            sio: Socket.IO server instance.
            sid: Socket session ID.
            user_id: Authenticated user's ID.
            data: Message data with game_id.

        Returns:
            Response dict with resignation result or error.
        """
        game_id = data.get("game_id")
        message_id = data.get("message_id", "")

        if not game_id:
            return self._error_response(
                WSErrorCode.INVALID_MESSAGE,
                "game_id is required",
                message_id,
            )

        try:
            result = await self._game_service.resign_game(
                game_id=game_id,
                player_id=user_id,
            )

            # End the game and archive
            if result.game_over and result.end_reason:
                await self._game_service.end_game(
                    game_id=game_id,
                    winner_id=result.winner_id,
                    end_reason=result.end_reason,
                )

                # Broadcast game over
                await self._broadcast_game_over(sio, game_id, result.winner_id, result.end_reason)

            logger.info(f"Player {user_id} resigned from game {game_id}")

            return {
                "success": True,
                "game_id": game_id,
                "game_over": True,
                "winner_id": result.winner_id,
                "end_reason": result.end_reason.value if result.end_reason else None,
            }

        except GameNotFoundError:
            return self._error_response(
                WSErrorCode.GAME_NOT_FOUND,
                f"Game {game_id} not found",
                message_id,
            )
        except PlayerNotInGameError:
            return self._error_response(
                WSErrorCode.NOT_IN_GAME,
                "You are not a participant in this game",
                message_id,
            )
        except GameAlreadyEndedError:
            return self._error_response(
                WSErrorCode.GAME_ENDED,
                "Game has already ended",
                message_id,
            )
        except Exception as e:
            logger.exception(f"Error resigning from game {game_id}: {e}")
            return self._error_response(
                WSErrorCode.INTERNAL_ERROR,
                "Failed to resign from game",
                message_id,
            )

    async def handle_disconnect(
        self,
        sio: "socketio.AsyncServer",
        sid: str,
        user_id: str,
    ) -> None:
        """Handle disconnect event.

        Notifies opponents in any active game that the player disconnected.
        Note: In production, consider debouncing disconnect notifications
        to handle rapid disconnect/reconnect cycles (e.g., network hiccups).

        Args:
            sio: Socket.IO server instance.
            sid: Socket session ID.
            user_id: Authenticated user's ID.
        """
        # Get connection info to find active game
        conn_info = await self._connection_manager.get_connection(sid)
        if conn_info is None or conn_info.game_id is None:
            return

        game_id = conn_info.game_id

        # Check if spectating (game_id format: "spectating:{actual_game_id}")
        if game_id.startswith("spectating:"):
            actual_game_id = game_id[len("spectating:") :]
            # Unregister spectator and broadcast updated count
            await self._connection_manager.unregister_spectator(sid, actual_game_id)
            await self._broadcast_spectator_count(sio, actual_game_id)
            logger.info(f"Spectator {user_id} left game {actual_game_id}")
            return

        # Notify opponent of disconnect
        await self._notify_opponent_status(
            sio, sid, game_id, user_id, ConnectionStatus.DISCONNECTED
        )

        logger.info(f"Player {user_id} disconnected from game {game_id}")

    async def handle_spectate(
        self,
        sio: "socketio.AsyncServer",
        sid: str,
        user_id: str,
        data: dict[str, Any],
    ) -> dict[str, Any]:
        """Handle game:spectate event.

        Allows a user to spectate a game they are not participating in.
        Spectators receive a filtered view with no hands visible.

        Args:
            sio: Socket.IO server instance.
            sid: Socket session ID.
            user_id: Authenticated user's ID.
            data: Message data with game_id.

        Returns:
            Response dict with success status and spectator state or error.
        """
        game_id = data.get("game_id")
        message_id = data.get("message_id", "")

        if not game_id:
            logger.warning(f"game:spectate missing game_id from {sid}")
            return self._error_response(
                WSErrorCode.INVALID_MESSAGE,
                "game_id is required",
                message_id,
            )

        try:
            # Get spectator view via GameService
            result = await self._game_service.spectate_game(
                game_id=game_id,
                user_id=user_id,
            )

            if not result.success:
                return self._error_response(
                    WSErrorCode.INTERNAL_ERROR,
                    result.message,
                    message_id,
                )

            # Register this connection as a spectator
            await self._connection_manager.register_spectator(sid, user_id, game_id)

            # Join the spectators room for this game
            await sio.enter_room(sid, f"spectators:{game_id}", namespace="/game")

            # Also join the game room to receive general updates
            await sio.enter_room(sid, f"game:{game_id}", namespace="/game")

            # Broadcast updated spectator count to players
            await self._broadcast_spectator_count(sio, game_id)

            # Build response with spectator state
            response: dict[str, Any] = {
                "success": True,
                "game_id": game_id,
                "game_over": result.game_over,
                "spectator_count": await self._connection_manager.get_spectator_count(game_id),
            }

            if result.visible_state:
                response["state"] = result.visible_state.model_dump(mode="json")

            logger.info(f"User {user_id} started spectating game {game_id}")
            return response

        except GameNotFoundError:
            return self._error_response(
                WSErrorCode.GAME_NOT_FOUND,
                f"Game {game_id} not found",
                message_id,
            )
        except CannotSpectateOwnGameError:
            return self._error_response(
                WSErrorCode.ACTION_NOT_ALLOWED,
                "You cannot spectate a game you are playing in",
                message_id,
            )
        except Exception as e:
            logger.exception(f"Error spectating game {game_id}: {e}")
            return self._error_response(
                WSErrorCode.INTERNAL_ERROR,
                "Failed to spectate game",
                message_id,
            )

    async def handle_leave_spectate(
        self,
        sio: "socketio.AsyncServer",
        sid: str,
        user_id: str,
        data: dict[str, Any],
    ) -> dict[str, Any]:
        """Handle game:leave_spectate event.

        Allows a spectator to stop watching a game.

        Args:
            sio: Socket.IO server instance.
            sid: Socket session ID.
            user_id: Authenticated user's ID.
            data: Message data with game_id.

        Returns:
            Response dict with success status.
        """
        game_id = data.get("game_id")
        message_id = data.get("message_id", "")

        if not game_id:
            return self._error_response(
                WSErrorCode.INVALID_MESSAGE,
                "game_id is required",
                message_id,
            )

        # Unregister spectator
        await self._connection_manager.unregister_spectator(sid, game_id)

        # Leave the rooms
        await sio.leave_room(sid, f"spectators:{game_id}", namespace="/game")
        await sio.leave_room(sid, f"game:{game_id}", namespace="/game")

        # Broadcast updated spectator count to players
        await self._broadcast_spectator_count(sio, game_id)

        logger.info(f"User {user_id} stopped spectating game {game_id}")

        return {
            "success": True,
            "game_id": game_id,
        }

    async def handle_reconnect(
        self,
        sio: "socketio.AsyncServer",
        sid: str,
        user_id: str,
    ) -> dict[str, Any] | None:
        """Handle automatic reconnection to active games on connect.

        Called after successful authentication to check if the user has
        an active game and automatically rejoin them.

        This method:
        1. Queries for active games via GameStateManager
        2. If found, auto-joins the game room
        3. Sends full game state to reconnecting player
        4. Extends turn timer if it's their turn
        5. Notifies opponent of reconnection

        Args:
            sio: Socket.IO server instance.
            sid: Socket session ID.
            user_id: Authenticated user's ID (as string, may be UUID).

        Returns:
            Dict with reconnection info if rejoined, None if no active game.
            The dict contains:
            - game_id: The game that was rejoined
            - is_your_turn: Whether it's the player's turn
            - state: The visible game state
            - pending_forced_action: Any pending forced action
            - turn_timeout_seconds: Remaining turn time if applicable
            - turn_deadline: Unix timestamp when turn expires
        """
        # Try to get active games from database
        try:
            player_uuid = UUID(user_id)
        except ValueError:
            # User ID is not a valid UUID (e.g., NPC) - no active games
            return None

        active_games = await self._state_manager.get_player_active_games(player_uuid)

        if not active_games:
            logger.debug(f"No active games for user {user_id}")
            return None

        # If multiple active games, use the most recent one
        # (sorted by last_action_at descending)
        active_games.sort(key=lambda g: g.last_action_at or g.started_at, reverse=True)
        active_game = active_games[0]
        game_id = str(active_game.id)

        logger.info(f"Auto-rejoining user {user_id} to game {game_id}")

        # Join the game via GameService (handles timer extension)
        result = await self._game_service.join_game(
            game_id=game_id,
            player_id=user_id,
            last_event_id=None,  # Full state refresh on reconnect
        )

        if not result.success:
            logger.warning(f"Failed to auto-rejoin game {game_id}: {result.message}")
            return None

        # Register connection with game
        await self._connection_manager.join_game(sid, game_id)

        # Join the Socket.IO room
        await sio.enter_room(sid, f"game:{game_id}", namespace="/game")

        # Notify opponent of reconnection
        await self._notify_opponent_status(sio, sid, game_id, user_id, ConnectionStatus.CONNECTED)

        # Build reconnection response
        response: dict[str, Any] = {
            "game_id": game_id,
            "is_your_turn": result.is_your_turn,
            "game_over": result.game_over,
        }

        if result.visible_state:
            response["state"] = result.visible_state.model_dump(mode="json")

        if result.pending_forced_action:
            response["pending_forced_action"] = {
                "player_id": result.pending_forced_action.player_id,
                "action_type": result.pending_forced_action.action_type,
                "reason": result.pending_forced_action.reason,
                "params": result.pending_forced_action.params,
            }

        if result.turn_timeout_seconds is not None:
            response["turn_timeout_seconds"] = result.turn_timeout_seconds
            response["turn_deadline"] = result.turn_deadline

        logger.info(f"Player {user_id} auto-rejoined game {game_id}")

        return response

    # =========================================================================
    # Broadcast Helpers
    # =========================================================================

    async def _broadcast_game_state(
        self,
        sio: "socketio.AsyncServer",
        game_id: str,
    ) -> None:
        """Broadcast filtered game state to all participants and spectators.

        Each player receives their own visibility-filtered view of the game.
        Spectators receive a view with no hands visible.

        Args:
            sio: Socket.IO server instance.
            game_id: The game to broadcast.
        """
        try:
            # Get full game state
            state = await self._game_service.get_game_state(game_id)

            # Get spectator count for inclusion in player messages
            spectator_count = await self._connection_manager.get_spectator_count(game_id)

            # Get all connected players for this game
            user_sids = await self._connection_manager.get_game_user_sids(game_id)

            # Send filtered state to each player
            for player_id, player_sid in user_sids.items():
                try:
                    visible_state = get_visible_state(state, player_id)
                    message = GameStateMessage(
                        game_id=game_id,
                        state=visible_state,
                        spectator_count=spectator_count,
                    )
                    await sio.emit(
                        "game:state",
                        message.model_dump(mode="json"),
                        to=player_sid,
                        namespace="/game",
                    )
                except ValueError:
                    # Player not in game - skip
                    logger.warning(f"Player {player_id} not in game {game_id}")
                    continue

            # Send spectator state to all spectators
            spectator_sids = await self._connection_manager.get_game_spectators(game_id)
            if spectator_sids:
                spectator_state = get_spectator_state(state)
                spectator_message = GameStateMessage(
                    game_id=game_id,
                    state=spectator_state,
                    spectator_count=spectator_count,
                )
                for spectator_sid in spectator_sids:
                    await sio.emit(
                        "game:state",
                        spectator_message.model_dump(mode="json"),
                        to=spectator_sid,
                        namespace="/game",
                    )

        except GameNotFoundError:
            logger.warning(f"Cannot broadcast state: game {game_id} not found")
        except Exception as e:
            logger.exception(f"Error broadcasting state for game {game_id}: {e}")

    async def _broadcast_game_over(
        self,
        sio: "socketio.AsyncServer",
        game_id: str,
        winner_id: str | None,
        end_reason: Any,
    ) -> None:
        """Broadcast game over notification to all participants and spectators.

        Args:
            sio: Socket.IO server instance.
            game_id: The game that ended.
            winner_id: The winner's player ID, or None for draw.
            end_reason: The GameEndReason for why the game ended.
        """
        try:
            # Get final state for each player
            state = await self._game_service.get_game_state(game_id)
            user_sids = await self._connection_manager.get_game_user_sids(game_id)

            for player_id, player_sid in user_sids.items():
                try:
                    visible_state = get_visible_state(state, player_id)
                    message = GameOverMessage(
                        game_id=game_id,
                        winner_id=winner_id,
                        end_reason=end_reason,
                        final_state=visible_state,
                    )
                    await sio.emit(
                        "game:game_over",
                        message.model_dump(mode="json"),
                        to=player_sid,
                        namespace="/game",
                    )
                except ValueError:
                    continue

            # Send to spectators
            spectator_sids = await self._connection_manager.get_game_spectators(game_id)
            if spectator_sids:
                spectator_state = get_spectator_state(state)
                spectator_message = GameOverMessage(
                    game_id=game_id,
                    winner_id=winner_id,
                    end_reason=end_reason,
                    final_state=spectator_state,
                )
                for spectator_sid in spectator_sids:
                    await sio.emit(
                        "game:game_over",
                        spectator_message.model_dump(mode="json"),
                        to=spectator_sid,
                        namespace="/game",
                    )

        except GameNotFoundError:
            # Game already archived - just emit to room without state
            message = GameOverMessage(
                game_id=game_id,
                winner_id=winner_id,
                end_reason=end_reason,
                final_state=None,  # type: ignore[arg-type]
            )
            await sio.emit(
                "game:game_over",
                {"game_id": game_id, "winner_id": winner_id, "end_reason": end_reason.value},
                room=f"game:{game_id}",
                namespace="/game",
            )
        except Exception as e:
            logger.exception(f"Error broadcasting game over for {game_id}: {e}")

    async def _notify_opponent_status(
        self,
        sio: "socketio.AsyncServer",
        sender_sid: str,
        game_id: str,
        user_id: str,
        status: ConnectionStatus,
    ) -> None:
        """Notify opponent of connection status change.

        Args:
            sio: Socket.IO server instance.
            sender_sid: The sid of the player whose status changed.
            game_id: The game ID.
            user_id: The player whose status changed.
            status: The new connection status.
        """
        try:
            # Find opponent's sid
            opponent_sid = await self._connection_manager.get_opponent_sid(game_id, user_id)

            if opponent_sid:
                message = OpponentStatusMessage(
                    game_id=game_id,
                    opponent_id=user_id,
                    status=status,
                )
                await sio.emit(
                    "game:opponent_status",
                    message.model_dump(mode="json"),
                    to=opponent_sid,
                    namespace="/game",
                )
                logger.debug(f"Notified opponent of {user_id} status: {status}")

        except Exception as e:
            logger.exception(f"Error notifying opponent status: {e}")

    async def _broadcast_spectator_count(
        self,
        sio: "socketio.AsyncServer",
        game_id: str,
    ) -> None:
        """Broadcast spectator count to all players in a game.

        Called when spectators join or leave to keep players informed.

        Args:
            sio: Socket.IO server instance.
            game_id: The game ID.
        """
        try:
            spectator_count = await self._connection_manager.get_spectator_count(game_id)
            user_sids = await self._connection_manager.get_game_user_sids(game_id)

            for player_sid in user_sids.values():
                await sio.emit(
                    "game:spectator_count",
                    {"game_id": game_id, "spectator_count": spectator_count},
                    to=player_sid,
                    namespace="/game",
                )

            logger.debug(f"Broadcast spectator count for game {game_id}: {spectator_count}")

        except Exception as e:
            logger.exception(f"Error broadcasting spectator count for {game_id}: {e}")

    # =========================================================================
    # Helper Methods
    # =========================================================================

    def _error_response(
        self,
        code: WSErrorCode,
        message: str,
        request_message_id: str = "",
    ) -> dict[str, Any]:
        """Create a standardized error response.

        Args:
            code: The error code.
            message: Human-readable error message.
            request_message_id: The original request's message_id.

        Returns:
            Dict with error information.
        """
        return {
            "success": False,
            "error": {
                "code": code.value,
                "message": message,
            },
            "request_message_id": request_message_id,
        }


# Global singleton instance
game_namespace_handler = GameNamespaceHandler()
