import logging
from typing import Optional
from uuid import UUID
from socketio import AsyncServer
from pydantic import ValidationError

from app.websocket.connection_manager import ConnectionManager
from app.utils.auth import verify_token
from app.models.game_models import ManualOutcomeSubmission
from app.core.dice import dice_system
from app.core.state_manager import state_manager
from app.core.game_engine import game_engine
from app.core.substitution_manager import SubstitutionManager
from app.core.validators import ValidationError as GameValidationError
from app.config.result_charts import PlayOutcome
from app.database.operations import DatabaseOperations

logger = logging.getLogger(f'{__name__}.handlers')


def register_handlers(sio: AsyncServer, manager: ConnectionManager) -> None:
    """Register all WebSocket event handlers"""

    @sio.event
    async def connect(sid, environ, auth):
        """Handle new connection"""
        try:
            # Verify JWT token
            token = auth.get("token")
            if not token:
                logger.warning(f"Connection {sid} rejected: no token")
                return False

            user_data = verify_token(token)
            user_id = user_data.get("user_id")

            if not user_id:
                logger.warning(f"Connection {sid} rejected: invalid token")
                return False

            await manager.connect(sid, user_id)
            await sio.emit("connected", {"user_id": user_id}, room=sid)

            logger.info(f"Connection {sid} accepted for user {user_id}")
            return True

        except Exception as e:
            logger.error(f"Connection error: {e}")
            return False

    @sio.event
    async def disconnect(sid):
        """Handle disconnection"""
        await manager.disconnect(sid)

    @sio.event
    async def join_game(sid, data):
        """Handle join game request"""
        try:
            game_id = data.get("game_id")
            role = data.get("role", "player")

            if not game_id:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": "Missing game_id"}
                )
                return

            # TODO: Verify user has access to game

            await manager.join_game(sid, game_id, role)
            await manager.emit_to_user(
                sid,
                "game_joined",
                {"game_id": game_id, "role": role}
            )

        except Exception as e:
            logger.error(f"Join game error: {e}")
            await manager.emit_to_user(
                sid,
                "error",
                {"message": str(e)}
            )

    @sio.event
    async def leave_game(sid, data):
        """Handle leave game request"""
        try:
            game_id = data.get("game_id")
            if game_id:
                await manager.leave_game(sid, game_id)

        except Exception as e:
            logger.error(f"Leave game error: {e}")

    @sio.event
    async def heartbeat(sid):
        """Handle heartbeat ping"""
        await sio.emit("heartbeat_ack", {}, room=sid)

    @sio.event
    async def roll_dice(sid, data):
        """
        Roll dice for manual outcome selection.

        Server rolls dice and broadcasts to all players in game room.
        Players then read their physical cards and submit outcomes.

        Event data:
            game_id: UUID of the game

        Emits:
            dice_rolled: Broadcast to game room with dice results
            error: To requester if validation fails
        """
        try:
            # Extract and validate game_id
            game_id_str = data.get("game_id")
            if not game_id_str:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": "Missing game_id"}
                )
                return

            try:
                game_id = UUID(game_id_str)
            except (ValueError, AttributeError):
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": "Invalid game_id format"}
                )
                return

            # Get game state
            state = state_manager.get_state(game_id)
            if not state:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": f"Game {game_id} not found"}
                )
                return

            # TODO: Verify user is participant in this game
            # user_id = manager.user_sessions.get(sid)
            # if not is_game_participant(game_id, user_id):
            #     await manager.emit_to_user(sid, "error", {"message": "Not authorized"})
            #     return

            # Roll dice
            ab_roll = dice_system.roll_ab(
                league_id=state.league_id,
                game_id=game_id
            )

            logger.info(
                f"Dice rolled for game {game_id}: "
                f"d6={ab_roll.d6_one}, 2d6={ab_roll.d6_two_total}, "
                f"chaos={ab_roll.chaos_d20}, resolution={ab_roll.resolution_d20}"
            )

            # Store roll in game state for manual outcome validation
            state.pending_manual_roll = ab_roll
            state_manager.update_state(game_id, state)

            # Broadcast dice results to all players in game
            await manager.broadcast_to_game(
                str(game_id),
                "dice_rolled",
                {
                    "game_id": str(game_id),
                    "roll_id": ab_roll.roll_id,
                    "d6_one": ab_roll.d6_one,
                    "d6_two_total": ab_roll.d6_two_total,
                    "chaos_d20": ab_roll.chaos_d20,
                    "resolution_d20": ab_roll.resolution_d20,
                    "check_wild_pitch": ab_roll.check_wild_pitch,
                    "check_passed_ball": ab_roll.check_passed_ball,
                    "timestamp": ab_roll.timestamp.to_iso8601_string(),
                    "message": "Dice rolled - read your card and submit outcome"
                }
            )

        except Exception as e:
            logger.error(f"Roll dice error: {e}", exc_info=True)
            await manager.emit_to_user(
                sid,
                "error",
                {"message": f"Failed to roll dice: {str(e)}"}
            )

    @sio.event
    async def submit_manual_outcome(sid, data):
        """
        Submit manually-selected play outcome.

        After dice are rolled, players read their physical cards and
        submit the outcome they see. System validates and processes.

        Event data:
            game_id: UUID of the game
            outcome: PlayOutcome enum value (e.g., "groundball_c")
            hit_location: Optional position string (e.g., "SS")

        Emits:
            outcome_accepted: To requester if valid
            play_resolved: Broadcast to game room with play result
            outcome_rejected: To requester if validation fails
            error: To requester if processing fails
        """
        try:
            # Extract and validate game_id
            game_id_str = data.get("game_id")
            if not game_id_str:
                await manager.emit_to_user(
                    sid,
                    "outcome_rejected",
                    {"message": "Missing game_id", "field": "game_id"}
                )
                return

            try:
                game_id = UUID(game_id_str)
            except (ValueError, AttributeError):
                await manager.emit_to_user(
                    sid,
                    "outcome_rejected",
                    {"message": "Invalid game_id format", "field": "game_id"}
                )
                return

            # Get game state
            state = state_manager.get_state(game_id)
            if not state:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": f"Game {game_id} not found"}
                )
                return

            # TODO: Verify user is active batter or authorized to submit
            # user_id = manager.user_sessions.get(sid)

            # Extract outcome data
            outcome_str = data.get("outcome")
            hit_location = data.get("hit_location")

            if not outcome_str:
                await manager.emit_to_user(
                    sid,
                    "outcome_rejected",
                    {"message": "Missing outcome", "field": "outcome"}
                )
                return

            # Validate using ManualOutcomeSubmission model
            try:
                submission = ManualOutcomeSubmission(
                    outcome=outcome_str,
                    hit_location=hit_location
                )
            except ValidationError as e:
                # Extract first error for user-friendly message
                first_error = e.errors()[0]
                field = first_error['loc'][0] if first_error['loc'] else 'unknown'
                message = first_error['msg']

                await manager.emit_to_user(
                    sid,
                    "outcome_rejected",
                    {
                        "message": message,
                        "field": field,
                        "errors": e.errors()
                    }
                )
                logger.warning(
                    f"Manual outcome validation failed for game {game_id}: {message}"
                )
                return

            # Convert to PlayOutcome enum
            outcome = PlayOutcome(submission.outcome)

            # Validate hit location is provided when required
            if outcome.requires_hit_location() and not submission.hit_location:
                await manager.emit_to_user(
                    sid,
                    "outcome_rejected",
                    {
                        "message": f"Outcome {outcome.value} requires hit_location",
                        "field": "hit_location"
                    }
                )
                return

            # Check for pending roll BEFORE accepting outcome
            if not state.pending_manual_roll:
                await manager.emit_to_user(
                    sid,
                    "outcome_rejected",
                    {
                        "message": "No pending dice roll - call roll_dice first",
                        "field": "game_state"
                    }
                )
                return

            ab_roll = state.pending_manual_roll

            logger.info(
                f"Manual outcome submitted for game {game_id}: "
                f"{outcome.value}" + (f" to {submission.hit_location}" if submission.hit_location else "")
            )

            # Confirm acceptance to submitter
            await manager.emit_to_user(
                sid,
                "outcome_accepted",
                {
                    "game_id": str(game_id),
                    "outcome": outcome.value,
                    "hit_location": submission.hit_location
                }
            )


            logger.info(
                f"Processing manual outcome with roll {ab_roll.roll_id}: "
                f"d6={ab_roll.d6_one}, 2d6={ab_roll.d6_two_total}, "
                f"chaos={ab_roll.chaos_d20}"
            )

            # Clear pending roll (one-time use)
            state.pending_manual_roll = None
            state_manager.update_state(game_id, state)

            # Process manual outcome through game engine
            try:
                result = await game_engine.resolve_manual_play(
                    game_id=game_id,
                    ab_roll=ab_roll,
                    outcome=outcome,
                    hit_location=submission.hit_location
                )

                # Build play result data
                play_result_data = {
                    "game_id": str(game_id),
                    "play_number": state.play_count,
                    "outcome": result.outcome.value,  # Use resolved outcome, not submitted
                    "hit_location": submission.hit_location,
                    "description": result.description,
                    "outs_recorded": result.outs_recorded,
                    "runs_scored": result.runs_scored,
                    "batter_result": result.batter_result,
                    "runners_advanced": result.runners_advanced,
                    "is_hit": result.is_hit,
                    "is_out": result.is_out,
                    "is_walk": result.is_walk,
                    "roll_id": ab_roll.roll_id
                }

                # Include X-Check details if present (Phase 3E-Final)
                if result.x_check_details:
                    xcheck = result.x_check_details
                    play_result_data["x_check_details"] = {
                        "position": xcheck.position,
                        "d20_roll": xcheck.d20_roll,
                        "d6_roll": xcheck.d6_roll,
                        "defender_range": xcheck.defender_range,
                        "defender_error_rating": xcheck.defender_error_rating,
                        "defender_id": xcheck.defender_id,
                        "base_result": xcheck.base_result,
                        "converted_result": xcheck.converted_result,
                        "error_result": xcheck.error_result,
                        "final_outcome": xcheck.final_outcome.value,
                        "hit_type": xcheck.hit_type,
                        # Optional SPD test details
                        "spd_test_roll": xcheck.spd_test_roll,
                        "spd_test_target": xcheck.spd_test_target,
                        "spd_test_passed": xcheck.spd_test_passed
                    }

                # Broadcast play result to game room
                await manager.broadcast_to_game(
                    str(game_id),
                    "play_resolved",
                    play_result_data
                )

                logger.info(
                    f"Manual play resolved for game {game_id}: {result.description}"
                )

            except GameValidationError as e:
                # Game engine validation error (e.g., missing hit location)
                await manager.emit_to_user(
                    sid,
                    "outcome_rejected",
                    {
                        "message": str(e),
                        "field": "validation"
                    }
                )
                logger.warning(f"Manual play validation failed: {e}")
                return

            except Exception as e:
                # Unexpected error during resolution
                logger.error(f"Error resolving manual play: {e}", exc_info=True)
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": f"Failed to resolve play: {str(e)}"}
                )
                return

        except Exception as e:
            logger.error(f"Submit manual outcome error: {e}", exc_info=True)
            await manager.emit_to_user(
                sid,
                "error",
                {"message": f"Failed to process outcome: {str(e)}"}
            )

    # ===== SUBSTITUTION EVENTS =====

    @sio.event
    async def request_pinch_hitter(sid, data):
        """
        Request pinch hitter substitution.

        Replaces current batter with a player from the bench. The substitute
        takes the batting order position of the replaced player.

        Event data:
            game_id: UUID of the game
            player_out_lineup_id: int - lineup ID of player being removed
            player_in_card_id: int - card/player ID of substitute
            team_id: int - team making substitution

        Emits:
            player_substituted: Broadcast to game room on success
            substitution_confirmed: To requester with new lineup_id
            substitution_error: To requester if validation fails
            error: To requester if processing fails
        """
        try:
            # Extract and validate game_id
            game_id_str = data.get("game_id")
            if not game_id_str:
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Missing game_id", "code": "MISSING_FIELD"}
                )
                return

            try:
                game_id = UUID(game_id_str)
            except (ValueError, AttributeError):
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Invalid game_id format", "code": "INVALID_FORMAT"}
                )
                return

            # Get game state
            state = state_manager.get_state(game_id)
            if not state:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": f"Game {game_id} not found"}
                )
                return

            # Extract substitution data
            player_out_lineup_id = data.get("player_out_lineup_id")
            player_in_card_id = data.get("player_in_card_id")
            team_id = data.get("team_id")

            if player_out_lineup_id is None:
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Missing player_out_lineup_id", "code": "MISSING_FIELD"}
                )
                return

            if player_in_card_id is None:
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Missing player_in_card_id", "code": "MISSING_FIELD"}
                )
                return

            if team_id is None:
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Missing team_id", "code": "MISSING_FIELD"}
                )
                return

            # TODO: Verify user is authorized to make substitution for this team
            # user_id = manager.user_sessions.get(sid)

            logger.info(
                f"Pinch hitter request for game {game_id}: "
                f"Replacing {player_out_lineup_id} with card {player_in_card_id}"
            )

            # Create SubstitutionManager instance
            db_ops = DatabaseOperations()
            sub_manager = SubstitutionManager(db_ops)

            # Execute pinch hitter substitution
            result = await sub_manager.pinch_hit(
                game_id=game_id,
                player_out_lineup_id=player_out_lineup_id,
                player_in_card_id=player_in_card_id,
                team_id=team_id
            )

            if result.success:
                # Broadcast to all clients in game
                await manager.broadcast_to_game(
                    str(game_id),
                    "player_substituted",
                    {
                        "type": "pinch_hitter",
                        "player_out_lineup_id": result.player_out_lineup_id,
                        "player_in_card_id": result.player_in_card_id,
                        "new_lineup_id": result.new_lineup_id,
                        "position": result.new_position,
                        "batting_order": result.new_batting_order,
                        "team_id": team_id,
                        "message": f"Pinch hitter: #{result.new_batting_order} now batting"
                    }
                )

                # Send confirmation to requester
                await manager.emit_to_user(
                    sid,
                    "substitution_confirmed",
                    {
                        "type": "pinch_hitter",
                        "new_lineup_id": result.new_lineup_id,
                        "success": True
                    }
                )

                logger.info(
                    f"Pinch hitter successful for game {game_id}: "
                    f"New lineup ID {result.new_lineup_id}"
                )
            else:
                # Send error to requester with error code
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {
                        "message": result.error_message,
                        "code": result.error_code,
                        "type": "pinch_hitter"
                    }
                )
                logger.warning(
                    f"Pinch hitter failed for game {game_id}: {result.error_message}"
                )

        except Exception as e:
            logger.error(f"Pinch hitter request error: {e}", exc_info=True)
            await manager.emit_to_user(
                sid,
                "error",
                {"message": f"Failed to process pinch hitter: {str(e)}"}
            )

    @sio.event
    async def request_defensive_replacement(sid, data):
        """
        Request defensive replacement substitution.

        Replaces a defensive player with a better fielder. Player can be
        swapped at any position. If player is in batting order, substitute
        takes their batting order spot.

        Event data:
            game_id: UUID of the game
            player_out_lineup_id: int - lineup ID of player being removed
            player_in_card_id: int - card/player ID of substitute
            new_position: str - defensive position for substitute (e.g., "SS")
            team_id: int - team making substitution

        Emits:
            player_substituted: Broadcast to game room on success
            substitution_confirmed: To requester with new lineup_id
            substitution_error: To requester if validation fails
            error: To requester if processing fails
        """
        try:
            # Extract and validate game_id
            game_id_str = data.get("game_id")
            if not game_id_str:
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Missing game_id", "code": "MISSING_FIELD"}
                )
                return

            try:
                game_id = UUID(game_id_str)
            except (ValueError, AttributeError):
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Invalid game_id format", "code": "INVALID_FORMAT"}
                )
                return

            # Get game state
            state = state_manager.get_state(game_id)
            if not state:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": f"Game {game_id} not found"}
                )
                return

            # Extract substitution data
            player_out_lineup_id = data.get("player_out_lineup_id")
            player_in_card_id = data.get("player_in_card_id")
            new_position = data.get("new_position")
            team_id = data.get("team_id")

            if player_out_lineup_id is None:
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Missing player_out_lineup_id", "code": "MISSING_FIELD"}
                )
                return

            if player_in_card_id is None:
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Missing player_in_card_id", "code": "MISSING_FIELD"}
                )
                return

            if not new_position:
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Missing new_position", "code": "MISSING_FIELD"}
                )
                return

            if team_id is None:
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Missing team_id", "code": "MISSING_FIELD"}
                )
                return

            # TODO: Verify user is authorized to make substitution for this team
            # user_id = manager.user_sessions.get(sid)

            logger.info(
                f"Defensive replacement request for game {game_id}: "
                f"Replacing {player_out_lineup_id} with card {player_in_card_id} at {new_position}"
            )

            # Create SubstitutionManager instance
            db_ops = DatabaseOperations()
            sub_manager = SubstitutionManager(db_ops)

            # Execute defensive replacement
            result = await sub_manager.defensive_replace(
                game_id=game_id,
                player_out_lineup_id=player_out_lineup_id,
                player_in_card_id=player_in_card_id,
                new_position=new_position,
                team_id=team_id
            )

            if result.success:
                # Broadcast to all clients in game
                await manager.broadcast_to_game(
                    str(game_id),
                    "player_substituted",
                    {
                        "type": "defensive_replacement",
                        "player_out_lineup_id": result.player_out_lineup_id,
                        "player_in_card_id": result.player_in_card_id,
                        "new_lineup_id": result.new_lineup_id,
                        "position": result.new_position,
                        "batting_order": result.new_batting_order,
                        "team_id": team_id,
                        "message": f"Defensive replacement: {result.new_position}"
                    }
                )

                # Send confirmation to requester
                await manager.emit_to_user(
                    sid,
                    "substitution_confirmed",
                    {
                        "type": "defensive_replacement",
                        "new_lineup_id": result.new_lineup_id,
                        "success": True
                    }
                )

                logger.info(
                    f"Defensive replacement successful for game {game_id}: "
                    f"New lineup ID {result.new_lineup_id}"
                )
            else:
                # Send error to requester with error code
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {
                        "message": result.error_message,
                        "code": result.error_code,
                        "type": "defensive_replacement"
                    }
                )
                logger.warning(
                    f"Defensive replacement failed for game {game_id}: {result.error_message}"
                )

        except Exception as e:
            logger.error(f"Defensive replacement request error: {e}", exc_info=True)
            await manager.emit_to_user(
                sid,
                "error",
                {"message": f"Failed to process defensive replacement: {str(e)}"}
            )

    @sio.event
    async def request_pitching_change(sid, data):
        """
        Request pitching change substitution.

        Replaces current pitcher with a reliever. Pitcher must have faced
        at least 1 batter unless injury. New pitcher takes mound immediately.

        Event data:
            game_id: UUID of the game
            player_out_lineup_id: int - lineup ID of pitcher being removed
            player_in_card_id: int - card/player ID of relief pitcher
            team_id: int - team making substitution

        Emits:
            player_substituted: Broadcast to game room on success
            substitution_confirmed: To requester with new lineup_id
            substitution_error: To requester if validation fails
            error: To requester if processing fails
        """
        try:
            # Extract and validate game_id
            game_id_str = data.get("game_id")
            if not game_id_str:
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Missing game_id", "code": "MISSING_FIELD"}
                )
                return

            try:
                game_id = UUID(game_id_str)
            except (ValueError, AttributeError):
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Invalid game_id format", "code": "INVALID_FORMAT"}
                )
                return

            # Get game state
            state = state_manager.get_state(game_id)
            if not state:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": f"Game {game_id} not found"}
                )
                return

            # Extract substitution data
            player_out_lineup_id = data.get("player_out_lineup_id")
            player_in_card_id = data.get("player_in_card_id")
            team_id = data.get("team_id")

            if player_out_lineup_id is None:
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Missing player_out_lineup_id", "code": "MISSING_FIELD"}
                )
                return

            if player_in_card_id is None:
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Missing player_in_card_id", "code": "MISSING_FIELD"}
                )
                return

            if team_id is None:
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {"message": "Missing team_id", "code": "MISSING_FIELD"}
                )
                return

            # TODO: Verify user is authorized to make substitution for this team
            # user_id = manager.user_sessions.get(sid)

            logger.info(
                f"Pitching change request for game {game_id}: "
                f"Replacing {player_out_lineup_id} with card {player_in_card_id}"
            )

            # Create SubstitutionManager instance
            db_ops = DatabaseOperations()
            sub_manager = SubstitutionManager(db_ops)

            # Execute pitching change
            result = await sub_manager.change_pitcher(
                game_id=game_id,
                player_out_lineup_id=player_out_lineup_id,
                player_in_card_id=player_in_card_id,
                team_id=team_id
            )

            if result.success:
                # Broadcast to all clients in game
                await manager.broadcast_to_game(
                    str(game_id),
                    "player_substituted",
                    {
                        "type": "pitching_change",
                        "player_out_lineup_id": result.player_out_lineup_id,
                        "player_in_card_id": result.player_in_card_id,
                        "new_lineup_id": result.new_lineup_id,
                        "position": result.new_position,  # Should be "P"
                        "batting_order": result.new_batting_order,
                        "team_id": team_id,
                        "message": f"Pitching change: New pitcher entering"
                    }
                )

                # Send confirmation to requester
                await manager.emit_to_user(
                    sid,
                    "substitution_confirmed",
                    {
                        "type": "pitching_change",
                        "new_lineup_id": result.new_lineup_id,
                        "success": True
                    }
                )

                logger.info(
                    f"Pitching change successful for game {game_id}: "
                    f"New lineup ID {result.new_lineup_id}"
                )
            else:
                # Send error to requester with error code
                await manager.emit_to_user(
                    sid,
                    "substitution_error",
                    {
                        "message": result.error_message,
                        "code": result.error_code,
                        "type": "pitching_change"
                    }
                )
                logger.warning(
                    f"Pitching change failed for game {game_id}: {result.error_message}"
                )

        except Exception as e:
            logger.error(f"Pitching change request error: {e}", exc_info=True)
            await manager.emit_to_user(
                sid,
                "error",
                {"message": f"Failed to process pitching change: {str(e)}"}
            )

    @sio.event
    async def get_lineup(sid, data):
        """
        Get current active lineup for a team.

        Returns all active players in the lineup with their positions
        and batting orders. Used by UI to refresh lineup display.

        Event data:
            game_id: UUID of the game
            team_id: int - team to get lineup for

        Emits:
            lineup_data: To requester with active lineup
            error: To requester if validation fails
        """
        try:
            # Extract and validate game_id
            game_id_str = data.get("game_id")
            if not game_id_str:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": "Missing game_id"}
                )
                return

            try:
                game_id = UUID(game_id_str)
            except (ValueError, AttributeError):
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": "Invalid game_id format"}
                )
                return

            # Extract team_id
            team_id = data.get("team_id")
            if team_id is None:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": "Missing team_id"}
                )
                return

            # TODO: Verify user has access to view this lineup
            # user_id = manager.user_sessions.get(sid)

            # Get lineup from state manager cache (fast O(1) lookup)
            lineup = state_manager.get_lineup(game_id, team_id)

            if lineup:
                # Send lineup data
                await manager.emit_to_user(
                    sid,
                    "lineup_data",
                    {
                        "game_id": str(game_id),
                        "team_id": team_id,
                        "players": [
                            {
                                "lineup_id": p.lineup_id,
                                "card_id": p.card_id,
                                "position": p.position,
                                "batting_order": p.batting_order,
                                "is_active": p.is_active,
                                "is_starter": p.is_starter
                            }
                            for p in lineup.players if p.is_active
                        ]
                    }
                )
                logger.info(f"Lineup data sent for game {game_id}, team {team_id}")
            else:
                # Lineup not in cache - try to load from database
                db_ops = DatabaseOperations()
                lineup_entries = await db_ops.get_active_lineup(game_id, team_id)

                if lineup_entries:
                    await manager.emit_to_user(
                        sid,
                        "lineup_data",
                        {
                            "game_id": str(game_id),
                            "team_id": team_id,
                            "players": [
                                {
                                    "lineup_id": entry.id,
                                    "card_id": entry.card_id or entry.player_id,
                                    "position": entry.position,
                                    "batting_order": entry.batting_order,
                                    "is_active": entry.is_active,
                                    "is_starter": entry.is_starter
                                }
                                for entry in lineup_entries
                            ]
                        }
                    )
                    logger.info(f"Lineup data loaded from DB for game {game_id}, team {team_id}")
                else:
                    await manager.emit_to_user(
                        sid,
                        "error",
                        {"message": f"Lineup not found for team {team_id}"}
                    )

        except Exception as e:
            logger.error(f"Get lineup error: {e}", exc_info=True)
            await manager.emit_to_user(
                sid,
                "error",
                {"message": f"Failed to get lineup: {str(e)}"}
            )

    @sio.event
    async def submit_defensive_decision(sid, data):
        """
        Submit defensive team decision.

        Event data:
            game_id: UUID of the game
            alignment: Defensive alignment (normal, shifted_left, shifted_right, extreme_shift)
            infield_depth: Infield positioning (in, normal, back, double_play)
            outfield_depth: Outfield positioning (in, normal, back)
            hold_runners: List of bases to hold runners (e.g., [1, 3])

        Emits:
            defensive_decision_submitted: To requester and broadcast to game room
            error: To requester if validation fails
        """
        try:
            # Extract and validate game_id
            game_id_str = data.get("game_id")
            if not game_id_str:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": "Missing game_id"}
                )
                return

            try:
                game_id = UUID(game_id_str)
            except (ValueError, AttributeError):
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": "Invalid game_id format"}
                )
                return

            # Get game state
            state = state_manager.get_state(game_id)
            if not state:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": f"Game {game_id} not found"}
                )
                return

            # TODO: Verify user is authorized (fielding team manager)
            # user_id = manager.user_sessions.get(sid)

            # Extract decision data
            alignment = data.get("alignment", "normal")
            infield_depth = data.get("infield_depth", "normal")
            outfield_depth = data.get("outfield_depth", "normal")
            hold_runners = data.get("hold_runners", [])

            # Create defensive decision
            from app.models.game_models import DefensiveDecision

            decision = DefensiveDecision(
                alignment=alignment,
                infield_depth=infield_depth,
                outfield_depth=outfield_depth,
                hold_runners=hold_runners
            )

            # Submit decision through game engine
            updated_state = await game_engine.submit_defensive_decision(game_id, decision)

            logger.info(
                f"Defensive decision submitted for game {game_id}: "
                f"alignment={alignment}, infield={infield_depth}, outfield={outfield_depth}"
            )

            # Broadcast to game room
            await manager.broadcast_to_game(
                str(game_id),
                "defensive_decision_submitted",
                {
                    "game_id": str(game_id),
                    "decision": {
                        "alignment": alignment,
                        "infield_depth": infield_depth,
                        "outfield_depth": outfield_depth,
                        "hold_runners": hold_runners
                    },
                    "pending_decision": updated_state.pending_decision
                }
            )

        except Exception as e:
            logger.error(f"Submit defensive decision error: {e}", exc_info=True)
            await manager.emit_to_user(
                sid,
                "error",
                {"message": f"Failed to submit defensive decision: {str(e)}"}
            )

    @sio.event
    async def submit_offensive_decision(sid, data):
        """
        Submit offensive team decision.

        Event data:
            game_id: UUID of the game
            approach: Batting approach (normal, contact, power, patient)
            steal_attempts: List of bases for steal attempts (e.g., [2, 3])
            hit_and_run: Boolean - enable hit-and-run play
            bunt_attempt: Boolean - attempt bunt

        Emits:
            offensive_decision_submitted: To requester and broadcast to game room
            error: To requester if validation fails
        """
        try:
            # Extract and validate game_id
            game_id_str = data.get("game_id")
            if not game_id_str:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": "Missing game_id"}
                )
                return

            try:
                game_id = UUID(game_id_str)
            except (ValueError, AttributeError):
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": "Invalid game_id format"}
                )
                return

            # Get game state
            state = state_manager.get_state(game_id)
            if not state:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": f"Game {game_id} not found"}
                )
                return

            # TODO: Verify user is authorized (batting team manager)
            # user_id = manager.user_sessions.get(sid)

            # Extract decision data
            approach = data.get("approach", "normal")
            steal_attempts = data.get("steal_attempts", [])
            hit_and_run = data.get("hit_and_run", False)
            bunt_attempt = data.get("bunt_attempt", False)

            # Create offensive decision
            from app.models.game_models import OffensiveDecision

            decision = OffensiveDecision(
                approach=approach,
                steal_attempts=steal_attempts,
                hit_and_run=hit_and_run,
                bunt_attempt=bunt_attempt
            )

            # Submit decision through game engine
            updated_state = await game_engine.submit_offensive_decision(game_id, decision)

            logger.info(
                f"Offensive decision submitted for game {game_id}: "
                f"approach={approach}, steal={steal_attempts}, hit_and_run={hit_and_run}"
            )

            # Broadcast to game room
            await manager.broadcast_to_game(
                str(game_id),
                "offensive_decision_submitted",
                {
                    "game_id": str(game_id),
                    "decision": {
                        "approach": approach,
                        "steal_attempts": steal_attempts,
                        "hit_and_run": hit_and_run,
                        "bunt_attempt": bunt_attempt
                    },
                    "pending_decision": updated_state.pending_decision
                }
            )

        except Exception as e:
            logger.error(f"Submit offensive decision error: {e}", exc_info=True)
            await manager.emit_to_user(
                sid,
                "error",
                {"message": f"Failed to submit offensive decision: {str(e)}"}
            )

    @sio.event
    async def get_box_score(sid, data):
        """
        Get box score using materialized views.

        Event data:
            game_id: UUID of the game

        Emits:
            box_score_data: To requester with box score
            error: To requester if validation fails
        """
        try:
            # Extract and validate game_id
            game_id_str = data.get("game_id")
            if not game_id_str:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": "Missing game_id"}
                )
                return

            try:
                game_id = UUID(game_id_str)
            except (ValueError, AttributeError):
                await manager.emit_to_user(
                    sid,
                    "error",
                    {"message": "Invalid game_id format"}
                )
                return

            # TODO: Verify user has access to view this game's box score
            # user_id = manager.user_sessions.get(sid)

            # Get box score from materialized views
            from app.services import box_score_service

            box_score = await box_score_service.get_box_score(game_id)

            if box_score:
                # Send box score data to requester
                await manager.emit_to_user(
                    sid,
                    "box_score_data",
                    {
                        "game_id": str(game_id),
                        "box_score": box_score
                    }
                )
                logger.info(f"Box score data sent for game {game_id}")
            else:
                await manager.emit_to_user(
                    sid,
                    "error",
                    {
                        "message": "No box score found for game",
                        "hint": "Run migration (alembic upgrade head) and refresh views"
                    }
                )

        except Exception as e:
            logger.error(f"Get box score error: {e}", exc_info=True)
            await manager.emit_to_user(
                sid,
                "error",
                {"message": f"Failed to get box score: {str(e)}"}
            )
