"""
Substitution Manager - Orchestrates player substitutions with DB-first pattern.

Follows the established pattern:
1. Validate (in-memory)
2. Update DATABASE FIRST
3. Update in-memory state SECOND
4. WebSocket broadcasts happen in handler

Author: Claude
Date: 2025-11-03
"""

import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING
from uuid import UUID

from sqlalchemy.exc import IntegrityError, OperationalError, SQLAlchemyError

from app.core.state_manager import state_manager
from app.core.substitution_rules import SubstitutionRules
from app.models.game_models import LineupPlayerState, TeamLineupState
from app.services.lineup_service import lineup_service

if TYPE_CHECKING:
    from app.database.operations import DatabaseOperations

logger = logging.getLogger(f"{__name__}.SubstitutionManager")


@dataclass
class SubstitutionResult:
    """Result of a substitution operation"""

    success: bool
    new_lineup_id: int | None = None
    player_out_lineup_id: int | None = None
    player_in_card_id: int | None = None
    new_position: str | None = None
    new_batting_order: int | None = None
    updated_lineup: TeamLineupState | None = None
    error_message: str | None = None
    error_code: str | None = None


class SubstitutionManager:
    """
    Manages player substitutions with state + database sync.

    Pattern: DB-first → State-second
    1. Validate using in-memory state
    2. Update database FIRST
    3. Update in-memory state SECOND
    4. Return result (WebSocket broadcast happens in handler)
    """

    def __init__(self, db_ops: "DatabaseOperations"):
        """
        Initialize substitution manager.

        Args:
            db_ops: Database operations instance
        """
        self.db_ops = db_ops
        self.rules = SubstitutionRules()

    async def pinch_hit(
        self,
        game_id: UUID,
        player_out_lineup_id: int,
        player_in_card_id: int,
        team_id: int,
    ) -> SubstitutionResult:
        """
        Execute pinch hitter substitution.

        Flow:
        1. Load game state + roster (in-memory)
        2. Validate substitution (rules check)
        3. Update database FIRST (create new lineup entry, mark old inactive)
        4. Update in-memory state SECOND (update lineup cache)
        5. Return result

        Args:
            game_id: Game identifier
            player_out_lineup_id: Lineup ID of player being replaced (must be current batter)
            player_in_card_id: Card ID of incoming player
            team_id: Team identifier

        Returns:
            SubstitutionResult with success status and details
        """
        logger.info(
            f"Pinch hit request: game={game_id}, "
            f"out={player_out_lineup_id}, in={player_in_card_id}, team={team_id}"
        )

        # STEP 1: Load game state and roster
        state = state_manager.get_state(game_id)
        if not state:
            return SubstitutionResult(
                success=False,
                error_message=f"Game {game_id} not found",
                error_code="GAME_NOT_FOUND",
            )

        roster = state_manager.get_lineup(game_id, team_id)
        if not roster:
            return SubstitutionResult(
                success=False,
                error_message=f"Roster not found for team {team_id}",
                error_code="ROSTER_NOT_FOUND",
            )

        player_out = roster.get_player_by_lineup_id(player_out_lineup_id)
        if not player_out:
            return SubstitutionResult(
                success=False,
                error_message=f"Player with lineup_id {player_out_lineup_id} not found",
                error_code="PLAYER_NOT_FOUND",
            )

        # STEP 2: Validate substitution
        validation = self.rules.validate_pinch_hitter(
            state=state,
            player_out=player_out,
            player_in_card_id=player_in_card_id,
            roster=roster,
        )

        if not validation.is_valid:
            return SubstitutionResult(
                success=False,
                error_message=validation.error_message,
                error_code=validation.error_code,
            )

        # STEP 3: Update DATABASE FIRST
        try:
            new_lineup_id = await self.db_ops.create_substitution(
                game_id=game_id,
                team_id=team_id,
                player_out_lineup_id=player_out_lineup_id,
                player_in_card_id=player_in_card_id,
                position=player_out.position,  # Pinch hitter takes same defensive position
                batting_order=player_out.batting_order,  # Takes same spot in order
                inning=state.inning,
                play_number=state.play_count,
            )
        except IntegrityError as e:
            logger.error(f"Integrity error during pinch hit substitution: {e}")
            return SubstitutionResult(
                success=False,
                error_message="Player substitution conflict - player may already be active",
                error_code="DB_INTEGRITY_ERROR",
            )
        except OperationalError as e:
            logger.error(f"Database connection error during pinch hit: {e}")
            return SubstitutionResult(
                success=False,
                error_message="Database connection error - please retry",
                error_code="DB_CONNECTION_ERROR",
            )
        except SQLAlchemyError as e:
            logger.error(
                f"Database error during pinch hit substitution: {e}", exc_info=True
            )
            return SubstitutionResult(
                success=False,
                error_message="Database error occurred",
                error_code="DB_ERROR",
            )

        # STEP 4: Update IN-MEMORY STATE SECOND
        try:
            # Mark old player inactive
            player_out.is_active = False

            # Fetch player data for SBA league
            player_name = f"Player #{player_in_card_id}"
            player_image = ""
            if state.league_id == "sba":
                player_name, player_image = await lineup_service.get_sba_player_data(
                    player_in_card_id
                )

            # Create new player entry
            new_player = LineupPlayerState(
                lineup_id=new_lineup_id,
                card_id=player_in_card_id,
                position=player_out.position,
                batting_order=player_out.batting_order,
                is_active=True,
                is_starter=False,
                player_name=player_name,
                player_image=player_image,
            )

            # Add to roster
            roster.players.append(new_player)

            # Update cache
            state_manager.set_lineup(game_id, team_id, roster)

            # Update current_batter if this is the current batter
            if (
                state.current_batter
                and state.current_batter.lineup_id == player_out_lineup_id
            ):
                state.current_batter = new_player  # Update object reference
                state_manager.update_state(game_id, state)

            logger.info(
                f"Pinch hit successful: {player_out.card_id} (lineup {player_out_lineup_id}) → "
                f"{player_in_card_id} (new lineup {new_lineup_id})"
            )

            return SubstitutionResult(
                success=True,
                new_lineup_id=new_lineup_id,
                player_out_lineup_id=player_out_lineup_id,
                player_in_card_id=player_in_card_id,
                new_position=player_out.position,
                new_batting_order=player_out.batting_order,
                updated_lineup=roster,
            )

        except (KeyError, AttributeError) as e:
            # Missing expected data in state - indicates corrupted state
            logger.error(
                f"State corruption during pinch hit substitution: {e}", exc_info=True
            )
            return SubstitutionResult(
                success=False,
                new_lineup_id=new_lineup_id,
                error_message="State corruption detected - game state may need recovery",
                error_code="STATE_CORRUPTION_ERROR",
            )
        except ValueError as e:
            # Invalid value during state update
            logger.error(f"Invalid value during pinch hit state update: {e}")
            return SubstitutionResult(
                success=False,
                new_lineup_id=new_lineup_id,
                error_message=f"Invalid state update: {str(e)}",
                error_code="STATE_VALIDATION_ERROR",
            )

    async def defensive_replace(
        self,
        game_id: UUID,
        player_out_lineup_id: int,
        player_in_card_id: int,
        new_position: str,
        team_id: int,
        new_batting_order: int | None = None,
        allow_mid_inning: bool = False,
    ) -> SubstitutionResult:
        """
        Execute defensive replacement.

        Flow: Same as pinch_hit (validate → DB → state)

        Args:
            game_id: Game identifier
            player_out_lineup_id: Lineup ID of player being replaced
            player_in_card_id: Card ID of incoming player
            new_position: Position for incoming player (can differ from player_out)
            team_id: Team identifier
            new_batting_order: Optional - if provided, changes batting order spot
            allow_mid_inning: If True, allows mid-inning substitution (for injuries)

        Returns:
            SubstitutionResult with success status and details
        """
        logger.info(
            f"Defensive replacement request: game={game_id}, "
            f"out={player_out_lineup_id}, in={player_in_card_id}, "
            f"position={new_position}, team={team_id}"
        )

        # STEP 1: Load game state and roster
        state = state_manager.get_state(game_id)
        if not state:
            return SubstitutionResult(
                success=False,
                error_message=f"Game {game_id} not found",
                error_code="GAME_NOT_FOUND",
            )

        roster = state_manager.get_lineup(game_id, team_id)
        if not roster:
            return SubstitutionResult(
                success=False,
                error_message=f"Roster not found for team {team_id}",
                error_code="ROSTER_NOT_FOUND",
            )

        player_out = roster.get_player_by_lineup_id(player_out_lineup_id)
        if not player_out:
            return SubstitutionResult(
                success=False,
                error_message=f"Player with lineup_id {player_out_lineup_id} not found",
                error_code="PLAYER_NOT_FOUND",
            )

        # STEP 2: Validate substitution
        validation = self.rules.validate_defensive_replacement(
            state=state,
            player_out=player_out,
            player_in_card_id=player_in_card_id,
            new_position=new_position,
            roster=roster,
            allow_mid_inning=allow_mid_inning,
        )

        if not validation.is_valid:
            return SubstitutionResult(
                success=False,
                error_message=validation.error_message,
                error_code=validation.error_code,
            )

        # Determine batting order (keep old if not specified)
        batting_order = (
            new_batting_order
            if new_batting_order is not None
            else player_out.batting_order
        )

        # STEP 3: Update DATABASE FIRST
        try:
            new_lineup_id = await self.db_ops.create_substitution(
                game_id=game_id,
                team_id=team_id,
                player_out_lineup_id=player_out_lineup_id,
                player_in_card_id=player_in_card_id,
                position=new_position,
                batting_order=batting_order,
                inning=state.inning,
                play_number=state.play_count,
            )
        except IntegrityError as e:
            logger.error(f"Integrity error during defensive replacement: {e}")
            return SubstitutionResult(
                success=False,
                error_message="Player substitution conflict - player may already be active",
                error_code="DB_INTEGRITY_ERROR",
            )
        except OperationalError as e:
            logger.error(f"Database connection error during defensive replacement: {e}")
            return SubstitutionResult(
                success=False,
                error_message="Database connection error - please retry",
                error_code="DB_CONNECTION_ERROR",
            )
        except SQLAlchemyError as e:
            logger.error(
                f"Database error during defensive replacement: {e}", exc_info=True
            )
            return SubstitutionResult(
                success=False,
                error_message="Database error occurred",
                error_code="DB_ERROR",
            )

        # STEP 4: Update IN-MEMORY STATE SECOND
        try:
            # Mark old player inactive
            player_out.is_active = False

            # Fetch player data for SBA league
            player_name = f"Player #{player_in_card_id}"
            player_image = ""
            if state.league_id == "sba":
                player_name, player_image = await lineup_service.get_sba_player_data(
                    player_in_card_id
                )

            # Create new player entry
            new_player = LineupPlayerState(
                lineup_id=new_lineup_id,
                card_id=player_in_card_id,
                position=new_position,
                batting_order=batting_order,
                is_active=True,
                is_starter=False,
                player_name=player_name,
                player_image=player_image,
            )

            # Add to roster
            roster.players.append(new_player)

            # Update cache
            state_manager.set_lineup(game_id, team_id, roster)

            # Update current pitcher/catcher if this affects them
            if (
                player_out.position == "P"
                and state.current_pitcher
                and state.current_pitcher.lineup_id == player_out_lineup_id
            ):
                state.current_pitcher = new_player
                state_manager.update_state(game_id, state)
            elif (
                player_out.position == "C"
                and state.current_catcher
                and state.current_catcher.lineup_id == player_out_lineup_id
            ):
                state.current_catcher = new_player
                state_manager.update_state(game_id, state)

            logger.info(
                f"Defensive replacement successful: {player_out.card_id} ({player_out.position}) → "
                f"{player_in_card_id} ({new_position})"
            )

            return SubstitutionResult(
                success=True,
                new_lineup_id=new_lineup_id,
                player_out_lineup_id=player_out_lineup_id,
                player_in_card_id=player_in_card_id,
                new_position=new_position,
                new_batting_order=batting_order,
                updated_lineup=roster,
            )

        except (KeyError, AttributeError) as e:
            # Missing expected data in state - indicates corrupted state
            logger.error(
                f"State corruption during defensive replacement: {e}", exc_info=True
            )
            return SubstitutionResult(
                success=False,
                new_lineup_id=new_lineup_id,
                error_message="State corruption detected - game state may need recovery",
                error_code="STATE_CORRUPTION_ERROR",
            )
        except ValueError as e:
            # Invalid value during state update
            logger.error(f"Invalid value during defensive replacement state update: {e}")
            return SubstitutionResult(
                success=False,
                new_lineup_id=new_lineup_id,
                error_message=f"Invalid state update: {str(e)}",
                error_code="STATE_VALIDATION_ERROR",
            )

    async def change_pitcher(
        self,
        game_id: UUID,
        pitcher_out_lineup_id: int,
        pitcher_in_card_id: int,
        team_id: int,
        force_change: bool = False,
    ) -> SubstitutionResult:
        """
        Execute pitching change.

        Special case of defensive replacement for pitchers.
        Always changes position 'P', maintains batting order.

        Flow: Same as pinch_hit (validate → DB → state)

        Args:
            game_id: Game identifier
            pitcher_out_lineup_id: Lineup ID of current pitcher
            pitcher_in_card_id: Card ID of incoming pitcher
            team_id: Team identifier
            force_change: If True, allows immediate change (for injuries)

        Returns:
            SubstitutionResult with success status and details
        """
        logger.info(
            f"Pitching change request: game={game_id}, "
            f"out={pitcher_out_lineup_id}, in={pitcher_in_card_id}, team={team_id}"
        )

        # STEP 1: Load game state and roster
        state = state_manager.get_state(game_id)
        if not state:
            return SubstitutionResult(
                success=False,
                error_message=f"Game {game_id} not found",
                error_code="GAME_NOT_FOUND",
            )

        roster = state_manager.get_lineup(game_id, team_id)
        if not roster:
            return SubstitutionResult(
                success=False,
                error_message=f"Roster not found for team {team_id}",
                error_code="ROSTER_NOT_FOUND",
            )

        pitcher_out = roster.get_player_by_lineup_id(pitcher_out_lineup_id)
        if not pitcher_out:
            return SubstitutionResult(
                success=False,
                error_message=f"Player with lineup_id {pitcher_out_lineup_id} not found",
                error_code="PLAYER_NOT_FOUND",
            )

        # STEP 2: Validate substitution
        validation = self.rules.validate_pitching_change(
            state=state,
            pitcher_out=pitcher_out,
            pitcher_in_card_id=pitcher_in_card_id,
            roster=roster,
            force_change=force_change,
        )

        if not validation.is_valid:
            return SubstitutionResult(
                success=False,
                error_message=validation.error_message,
                error_code=validation.error_code,
            )

        # STEP 3: Update DATABASE FIRST
        try:
            new_lineup_id = await self.db_ops.create_substitution(
                game_id=game_id,
                team_id=team_id,
                player_out_lineup_id=pitcher_out_lineup_id,
                player_in_card_id=pitcher_in_card_id,
                position="P",  # Always pitcher
                batting_order=pitcher_out.batting_order,  # Maintains batting order
                inning=state.inning,
                play_number=state.play_count,
            )
        except IntegrityError as e:
            logger.error(f"Integrity error during pitching change: {e}")
            return SubstitutionResult(
                success=False,
                error_message="Pitching change conflict - pitcher may already be active",
                error_code="DB_INTEGRITY_ERROR",
            )
        except OperationalError as e:
            logger.error(f"Database connection error during pitching change: {e}")
            return SubstitutionResult(
                success=False,
                error_message="Database connection error - please retry",
                error_code="DB_CONNECTION_ERROR",
            )
        except SQLAlchemyError as e:
            logger.error(f"Database error during pitching change: {e}", exc_info=True)
            return SubstitutionResult(
                success=False,
                error_message="Database error occurred",
                error_code="DB_ERROR",
            )

        # STEP 4: Update IN-MEMORY STATE SECOND
        try:
            # Mark old pitcher inactive
            pitcher_out.is_active = False

            # Fetch player data for SBA league
            player_name = f"Player #{pitcher_in_card_id}"
            player_image = ""
            if state.league_id == "sba":
                player_name, player_image = await lineup_service.get_sba_player_data(
                    pitcher_in_card_id
                )

            # Create new pitcher entry
            new_pitcher = LineupPlayerState(
                lineup_id=new_lineup_id,
                card_id=pitcher_in_card_id,
                position="P",
                batting_order=pitcher_out.batting_order,
                is_active=True,
                is_starter=False,
                player_name=player_name,
                player_image=player_image,
            )

            # Add to roster
            roster.players.append(new_pitcher)

            # Update cache
            state_manager.set_lineup(game_id, team_id, roster)

            # Update current pitcher in game state
            state.current_pitcher = new_pitcher
            state_manager.update_state(game_id, state)

            logger.info(
                f"Pitching change successful: {pitcher_out.card_id} (lineup {pitcher_out_lineup_id}) → "
                f"{pitcher_in_card_id} (new lineup {new_lineup_id})"
            )

            return SubstitutionResult(
                success=True,
                new_lineup_id=new_lineup_id,
                player_out_lineup_id=pitcher_out_lineup_id,
                player_in_card_id=pitcher_in_card_id,
                new_position="P",
                new_batting_order=pitcher_out.batting_order,
                updated_lineup=roster,
            )

        except (KeyError, AttributeError) as e:
            # Missing expected data in state - indicates corrupted state
            logger.error(
                f"State corruption during pitching change: {e}", exc_info=True
            )
            return SubstitutionResult(
                success=False,
                new_lineup_id=new_lineup_id,
                error_message="State corruption detected - game state may need recovery",
                error_code="STATE_CORRUPTION_ERROR",
            )
        except ValueError as e:
            # Invalid value during state update
            logger.error(f"Invalid value during pitching change state update: {e}")
            return SubstitutionResult(
                success=False,
                new_lineup_id=new_lineup_id,
                error_message=f"Invalid state update: {str(e)}",
                error_code="STATE_VALIDATION_ERROR",
            )
