"""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

Architecture:
    Socket.IO Events -> GameNamespaceHandler -> GameService
                     -> ConnectionManager (for routing)
                     -> 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 pydantic import ValidationError

from app.core.models.actions import parse_action
from app.core.visibility import 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 (
    ForcedActionRequiredError,
    GameAlreadyEndedError,
    GameNotFoundError,
    GameService,
    InvalidActionError,
    NotPlayerTurnError,
    PlayerNotInGameError,
    game_service,
)

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.
    """

    def __init__(
        self,
        game_svc: GameService | None = None,
        conn_manager: ConnectionManager | 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.
        """
        self._game_service = game_svc or game_service
        self._connection_manager = conn_manager or connection_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,
                }

            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,
                }

            # 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.

        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

        # 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}")

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

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

        Each player receives their own visibility-filtered view of the game.

        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 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,
                    )
                    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

        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.

        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

        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}")

    # =========================================================================
    # 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()
