"""
Runner advancement logic for groundball and flyball outcomes.

This module implements the complete runner advancement system based on:

GROUNDBALLS (GROUNDBALL_A, GROUNDBALL_B, GROUNDBALL_C):
- Base situation (0-7 on-base code)
- Defensive positioning (infield_in, corners_in, normal)
- Hit location (1B, 2B, SS, 3B, P, C)
- Result numbers (1-13) match official rulebook charts exactly

FLYBALLS (FLYOUT_A, FLYOUT_B, FLYOUT_BQ, FLYOUT_C):
- FLYOUT_A (Deep): All runners tag up and advance one base
- FLYOUT_B (Medium): R3 scores, R2 may attempt tag-up (DECIDE), R1 holds
- FLYOUT_BQ (Medium-shallow fly(b)?): R3 may attempt to score (DECIDE), R2 holds, R1 holds
- FLYOUT_C (Shallow): No advancement, all runners hold
- Defender arm strength factors into tag-up decisions (for DECIDE plays)
"""

import logging
from dataclasses import dataclass
from enum import IntEnum

from app.config.result_charts import PlayOutcome
from app.models.game_models import DefensiveDecision, GameState

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

# pyright: reportOptionalMemberAccess=false


class GroundballResultType(IntEnum):
    """
    Groundball advancement results matching rulebook chart.

    These numbered results correspond exactly to the official rulebook
    advancement charts (Infield Back and Infield In).
    """

    BATTER_OUT_RUNNERS_HOLD = 1
    """Result 1: Batter out, all runners hold."""

    DOUBLE_PLAY_AT_SECOND = 2
    """Result 2: Batter out, runner on 1st out at 2nd (double play on empty/1 out). Other runners advance 1 base."""

    BATTER_OUT_RUNNERS_ADVANCE = 3
    """Result 3: Batter out, all runners advance 1 base."""

    BATTER_SAFE_FORCE_OUT_AT_SECOND = 4
    """Result 4: Batter safe at 1st, runner on 1st forced out at 2nd. Other runners advance 1 base."""

    CONDITIONAL_ON_MIDDLE_INFIELD = 5
    """Result 5: Hit to 2B/SS = batter out, runners advance 1 base. Hit anywhere else = batter out, runners hold."""

    CONDITIONAL_ON_RIGHT_SIDE = 6
    """Result 6: Hit to 1B/2B = batter out, runners advance 1 base. Hit anywhere else = batter out, runners hold."""

    BATTER_OUT_FORCED_ONLY = 7
    """Result 7: Batter out, runners advance 1 base (only if forced)."""

    BATTER_OUT_FORCED_ONLY_ALT = 8
    """Result 8: Same as Result 7 (batter out, forced advancement only)."""

    LEAD_HOLDS_TRAIL_ADVANCES = 9
    """Result 9: Batter out, runner on 3rd holds, runner on 1st advances to 2nd (bases loaded scenario)."""

    DOUBLE_PLAY_HOME_TO_FIRST = 10
    """Result 10: Double play at home and 1st (runner on 3rd out at home, batter out). Otherwise batter out, runners advance."""

    BATTER_SAFE_LEAD_OUT = 11
    """Result 11: Batter safe at 1st, lead runner is out. Other runners advance 1 base."""

    DECIDE_OPPORTUNITY = 12
    """Result 12: DECIDE - Lead runner can attempt to advance. Offense chooses, defense responds."""

    CONDITIONAL_DOUBLE_PLAY = 13
    """Result 13: Hit to C/3B = double play at 3rd and 2nd (batter safe). Hit anywhere else = same as Result 2."""

    SAFE_ALL_ADVANCE_ONE = 14
    """Result 14: Batter safe at 1st, all runners advance 1 base (error E1)."""

    SAFE_ALL_ADVANCE_TWO = 15
    """Result 15: Batter safe at 2nd, all runners advance 2 bases (error E2)."""

    SAFE_ALL_ADVANCE_THREE = 16
    """Result 16: Batter safe at 3rd, all runners advance 3 bases (error E3)."""


@dataclass
class RunnerMovement:
    """
    Represents the movement of a single runner during a play.

    Attributes:
        lineup_id: The lineup position ID of the runner (1-9)
        from_base: Starting base (0=batter, 1-3=bases)
        to_base: Ending base (0=out, 1-3=bases, 4=scored)
        is_out: Whether the runner was thrown out
    """

    lineup_id: int
    from_base: int
    to_base: int
    is_out: bool = False

    def __repr__(self) -> str:
        if self.is_out:
            return f"RunnerMovement(#{self.lineup_id}: {self.from_base}→OUT)"
        if self.to_base == 4:
            return f"RunnerMovement(#{self.lineup_id}: {self.from_base}→HOME)"
        return f"RunnerMovement(#{self.lineup_id}: {self.from_base}→{self.to_base})"


@dataclass
class AdvancementResult:
    """
    Complete result of a runner advancement resolution.

    Attributes:
        movements: List of all runner movements (including batter)
        outs_recorded: Number of outs on the play (0-3)
        runs_scored: Number of runs scored on the play
        result_type: The groundball result type (1-13), or None for flyballs
        description: Human-readable description of the result
    """

    movements: list[RunnerMovement]
    outs_recorded: int
    runs_scored: int
    result_type: GroundballResultType | None
    description: str


class RunnerAdvancement:
    """
    Handles runner advancement logic for groundball and flyball outcomes.

    GROUNDBALLS:
    - Infield Back chart (normal defensive positioning)
    - Infield In chart (runners on 3rd, defense playing in)
    - Corners In positioning (hybrid approach)
    - Double play mechanics
    - DECIDE mechanic (lead runner advancement attempts)

    FLYBALLS:
    - Deep (FLYOUT_A): All runners tag up and advance one base
    - Medium (FLYOUT_B): R3 scores, R2 may attempt tag (DECIDE), R1 holds
    - Medium-shallow (FLYOUT_BQ): R3 may attempt to score (DECIDE), R2 holds, R1 holds
    - Shallow (FLYOUT_C): No advancement
    """

    def __init__(self):
        self.logger = logging.getLogger(f"{__name__}.RunnerAdvancement")

    def advance_runners(
        self,
        outcome: PlayOutcome,
        hit_location: str,
        state: GameState,
        defensive_decision: DefensiveDecision,
    ) -> AdvancementResult:
        """
        Calculate runner advancement for groundball or flyball outcome.

        Args:
            outcome: The play outcome (GROUNDBALL_A/B/C or FLYOUT_A/B/BQ/C)
            hit_location: Where the ball was hit (1B, 2B, SS, 3B, P, C for GB; LF, CF, RF for FB)
            state: Current game state
            defensive_decision: Defensive team's positioning decisions

        Returns:
            AdvancementResult with all runner movements and outs

        Raises:
            ValueError: If outcome is not a groundball or flyball type
        """
        # Check if groundball
        if outcome in [
            PlayOutcome.GROUNDBALL_A,
            PlayOutcome.GROUNDBALL_B,
            PlayOutcome.GROUNDBALL_C,
        ]:
            return self._advance_runners_groundball(
                outcome, hit_location, state, defensive_decision
            )

        # Check if flyball
        if outcome in [
            PlayOutcome.FLYOUT_A,
            PlayOutcome.FLYOUT_B,
            PlayOutcome.FLYOUT_BQ,
            PlayOutcome.FLYOUT_C,
        ]:
            return self._advance_runners_flyball(
                outcome, hit_location, state, defensive_decision
            )

        raise ValueError(
            f"advance_runners only handles groundballs and flyballs, got {outcome}"
        )

    def _advance_runners_groundball(
        self,
        outcome: PlayOutcome,
        hit_location: str,
        state: GameState,
        defensive_decision: DefensiveDecision,
    ) -> AdvancementResult:
        """
        Calculate runner advancement for groundball outcome.

        Args:
            outcome: The play outcome (GROUNDBALL_A, B, or C)
            hit_location: Where the ball was hit (1B, 2B, SS, 3B, P, C)
            state: Current game state
            defensive_decision: Defensive team's positioning decisions

        Returns:
            AdvancementResult with all runner movements and outs
        """

        # Determine which result to apply
        result_type = self._determine_groundball_result(
            outcome=outcome,
            on_base_code=state.current_on_base_code,
            starting_outs=state.outs,
            defensive_decision=defensive_decision,
            hit_location=hit_location,
        )

        self.logger.info(
            f"Groundball {outcome.name} with bases {state.current_on_base_code}, "
            f"{state.outs} outs → Result {result_type}"
        )

        # Execute the result
        return self._execute_result(
            result_type=result_type,
            state=state,
            hit_location=hit_location,
            defensive_decision=defensive_decision,
        )

    def _determine_groundball_result(
        self,
        outcome: PlayOutcome,
        on_base_code: int,
        starting_outs: int,
        defensive_decision: DefensiveDecision,
        hit_location: str,
    ) -> GroundballResultType:
        """
        Determine which groundball result applies based on situation.

        This implements the logic from both Infield Back and Infield In charts.

        Args:
            outcome: GROUNDBALL_A, B, or C
            on_base_code: 0-7 representing base runners (0=empty, 7=loaded)
            starting_outs: Number of outs before play (0-2)
            defensive_decision: Defensive positioning
            hit_location: Where ball was hit

        Returns:
            GroundballResultType (1-13)
        """
        # Special case: 2 outs always results in batter out, runners hold
        if starting_outs == 2:
            return GroundballResultType.BATTER_OUT_RUNNERS_HOLD

        # Determine if playing in or corners in
        infield_in = defensive_decision.infield_depth == "infield_in"
        corners_in = defensive_decision.infield_depth == "corners_in"

        # Map hit location to position groups
        hit_to_mif = hit_location in ["2B", "SS"]  # Middle infield
        hit_to_cif = hit_location in ["1B", "3B", "P", "C"]  # Corner infield
        hit_to_right = hit_location in ["1B", "2B"]  # Right side

        # Convert outcome to letter
        gb_letter = outcome.name.split("_")[1]  # 'GROUNDBALL_A' → 'A'

        # ========================================
        # INFIELD IN CHART (Runner on 3rd scenarios)
        # ========================================
        if infield_in and on_base_code in [3, 5, 6, 7]:
            return self._apply_infield_in_chart(
                gb_letter=gb_letter,
                on_base_code=on_base_code,
                hit_location=hit_location,
                hit_to_mif=hit_to_mif,
                hit_to_cif=hit_to_cif,
            )

        # ========================================
        # CORNERS IN (Hybrid approach)
        # ========================================
        if corners_in and on_base_code in [3, 5, 6, 7] and hit_to_cif:
            # Apply Infield In rules for corners
            return self._apply_infield_in_chart(
                gb_letter=gb_letter,
                on_base_code=on_base_code,
                hit_location=hit_location,
                hit_to_mif=hit_to_mif,
                hit_to_cif=hit_to_cif,
            )

        # ========================================
        # INFIELD BACK CHART (Default positioning)
        # ========================================
        return self._apply_infield_back_chart(
            gb_letter=gb_letter,
            on_base_code=on_base_code,
            hit_location=hit_location,
            hit_to_mif=hit_to_mif,
            hit_to_right=hit_to_right,
        )

    def _apply_infield_in_chart(
        self,
        gb_letter: str,
        on_base_code: int,
        hit_location: str,
        hit_to_mif: bool,
        hit_to_cif: bool,
    ) -> GroundballResultType:
        """
        Apply Infield In chart logic.

        Chart reference:
        - 3rd only: GBA=7, GBB=1, GBC=8
        - 1st & 3rd: GBA=7, GBB=9, GBC=8, with DECIDE for GBC hit to SS/P/C
        - 2nd & 3rd: GBA=7, GBB=1, GBC=8
        - Loaded: GBA=10, GBB=11, GBC=11
        """
        # Bases loaded (on_base_code == 7)
        if on_base_code == 7:
            if gb_letter == "A":
                return GroundballResultType.DOUBLE_PLAY_HOME_TO_FIRST  # 10
            # B or C
            return GroundballResultType.BATTER_SAFE_LEAD_OUT  # 11

        # 1st & 3rd (on_base_code == 5)
        if on_base_code == 5:
            if gb_letter == "A":
                return GroundballResultType.BATTER_OUT_FORCED_ONLY  # 7
            if gb_letter == "B":
                return GroundballResultType.LEAD_HOLDS_TRAIL_ADVANCES  # 9
            # C
            # Check for DECIDE opportunity
            if hit_location in ["SS", "P", "C"]:
                return GroundballResultType.DECIDE_OPPORTUNITY  # 12
            return GroundballResultType.BATTER_OUT_FORCED_ONLY_ALT  # 8

        # 2nd & 3rd (on_base_code == 6)
        if on_base_code == 6:
            if gb_letter == "A":
                return GroundballResultType.BATTER_OUT_FORCED_ONLY  # 7
            if gb_letter == "B":
                return GroundballResultType.BATTER_OUT_RUNNERS_HOLD  # 1
            # C
            return GroundballResultType.BATTER_OUT_FORCED_ONLY_ALT  # 8

        # 3rd only (on_base_code == 3)
        if on_base_code == 3:
            if gb_letter == "A":
                return GroundballResultType.BATTER_OUT_FORCED_ONLY  # 7
            if gb_letter == "B":
                return GroundballResultType.BATTER_OUT_RUNNERS_HOLD  # 1
            # C
            # Check for DECIDE on certain hit locations
            if hit_location in ["1B", "2B"]:
                return GroundballResultType.DECIDE_OPPORTUNITY  # 12
            if hit_location == "3B":
                return GroundballResultType.BATTER_OUT_RUNNERS_HOLD  # 1
            # SS, P, C
            return GroundballResultType.DECIDE_OPPORTUNITY  # 12

        # Fallback (shouldn't reach here)
        self.logger.warning(
            f"Unexpected Infield In scenario: bases={on_base_code}, letter={gb_letter}"
        )
        return GroundballResultType.BATTER_OUT_RUNNERS_HOLD

    def _apply_infield_back_chart(
        self,
        gb_letter: str,
        on_base_code: int,
        hit_location: str,
        hit_to_mif: bool,
        hit_to_right: bool,
    ) -> GroundballResultType:
        """
        Apply Infield Back chart logic (default positioning).

        Chart reference:
        - Empty: GBA=1, GBB=1, GBC=1 (all batter out)
        - 1st: GBA=2, GBB=4, GBC=3
        - 2nd: GBA=6, GBB=6, GBC=3 (conditional on right side)
        - 3rd: GBA=5, GBB=5, GBC=3 (conditional on middle infield)
        - 1st & 2nd: GBA=2, GBB=4, GBC=3
        - 1st & 3rd: GBA=2, GBB=4, GBC=3
        - 2nd & 3rd: GBA=5, GBB=5, GBC=3
        - Loaded: GBA=2, GBB=4, GBC=3
        """
        # Empty bases (on_base_code == 0)
        if on_base_code == 0:
            return GroundballResultType.BATTER_OUT_RUNNERS_HOLD  # 1

        # Runner on 1st (includes 1, 4, 5, 7 - any scenario with runner on 1st)
        if on_base_code in [1, 4, 5, 7]:
            if gb_letter == "A":
                return GroundballResultType.DOUBLE_PLAY_AT_SECOND  # 2
            if gb_letter == "B":
                return GroundballResultType.BATTER_SAFE_FORCE_OUT_AT_SECOND  # 4
            # C
            return GroundballResultType.BATTER_OUT_RUNNERS_ADVANCE  # 3

        # Runner on 2nd only (on_base_code == 2)
        if on_base_code == 2:
            if gb_letter in ["A", "B"]:
                return GroundballResultType.CONDITIONAL_ON_RIGHT_SIDE  # 6
            # C
            return GroundballResultType.BATTER_OUT_RUNNERS_ADVANCE  # 3

        # Runner on 3rd (includes 3, 6 - scenarios with runner on 3rd but not 1st)
        if on_base_code in [3, 6]:
            if gb_letter in ["A", "B"]:
                return GroundballResultType.CONDITIONAL_ON_MIDDLE_INFIELD  # 5
            # C
            return GroundballResultType.BATTER_OUT_RUNNERS_ADVANCE  # 3

        # Fallback
        self.logger.warning(
            f"Unexpected Infield Back scenario: bases={on_base_code}, letter={gb_letter}"
        )
        return GroundballResultType.BATTER_OUT_RUNNERS_HOLD

    def _execute_result(
        self,
        result_type: GroundballResultType,
        state: GameState,
        hit_location: str,
        defensive_decision: DefensiveDecision | None = None,
    ) -> AdvancementResult:
        """
        Execute a specific groundball result and return movements.

        Args:
            result_type: The result type (1-13)
            state: Current game state
            hit_location: Where ball was hit
            defensive_decision: Defensive positioning (for DP probability)

        Returns:
            AdvancementResult with all movements
        """
        # Dispatch to appropriate handler
        if result_type == GroundballResultType.BATTER_OUT_RUNNERS_HOLD:
            return self._gb_result_1(state)
        if result_type == GroundballResultType.DOUBLE_PLAY_AT_SECOND:
            return self._gb_result_2(state, defensive_decision, hit_location)
        if result_type == GroundballResultType.BATTER_OUT_RUNNERS_ADVANCE:
            return self._gb_result_3(state)
        if result_type == GroundballResultType.BATTER_SAFE_FORCE_OUT_AT_SECOND:
            return self._gb_result_4(state)
        if result_type == GroundballResultType.CONDITIONAL_ON_MIDDLE_INFIELD:
            return self._gb_result_5(state, hit_location)
        if result_type == GroundballResultType.CONDITIONAL_ON_RIGHT_SIDE:
            return self._gb_result_6(state, hit_location)
        if result_type in [
            GroundballResultType.BATTER_OUT_FORCED_ONLY,
            GroundballResultType.BATTER_OUT_FORCED_ONLY_ALT,
        ]:
            return self._gb_result_7(state)
        if result_type == GroundballResultType.LEAD_HOLDS_TRAIL_ADVANCES:
            return self._gb_result_9(state)
        if result_type == GroundballResultType.DOUBLE_PLAY_HOME_TO_FIRST:
            return self._gb_result_10(state, defensive_decision, hit_location)
        if result_type == GroundballResultType.BATTER_SAFE_LEAD_OUT:
            return self._gb_result_11(state)
        if result_type == GroundballResultType.DECIDE_OPPORTUNITY:
            return self._gb_result_12(state, hit_location)
        if result_type == GroundballResultType.CONDITIONAL_DOUBLE_PLAY:
            return self._gb_result_13(state, defensive_decision, hit_location)
        raise ValueError(f"Unknown result type: {result_type}")

    # ========================================
    # Result Handlers (1-13)
    # ========================================

    def _gb_result_1(self, state: GameState) -> AdvancementResult:
        """
        Result 1: Batter out, all runners hold.
        """
        movements = []

        # Batter is out
        movements.append(
            RunnerMovement(
                lineup_id=state.current_batter.lineup_id,
                from_base=0,
                to_base=0,
                is_out=True,
            )
        )

        # All runners stay put
        if state.is_runner_on_first():
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_first.lineup_id,
                    from_base=1,
                    to_base=1,
                    is_out=False,
                )
            )

        if state.is_runner_on_second():
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_second.lineup_id,
                    from_base=2,
                    to_base=2,
                    is_out=False,
                )
            )

        if state.is_runner_on_third():
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_third.lineup_id,
                    from_base=3,
                    to_base=3,
                    is_out=False,
                )
            )

        return AdvancementResult(
            movements=movements,
            outs_recorded=1,
            runs_scored=0,
            result_type=GroundballResultType.BATTER_OUT_RUNNERS_HOLD,
            description="Batter out, runners hold",
        )

    def _gb_result_2(
        self,
        state: GameState,
        defensive_decision: DefensiveDecision | None,
        hit_location: str,
    ) -> AdvancementResult:
        """
        Result 2: Double play at 2nd and 1st (when possible).

        - With 0 or 1 out: Runner on 1st out at 2nd, batter out (DP)
        - With 2 outs: Only batter out
        - Other runners advance 1 base
        """
        movements = []
        outs = 0
        runs = 0

        # Check if double play is possible
        can_turn_dp = state.outs < 2 and state.is_runner_on_first()

        if can_turn_dp:
            # Runner on first out at second
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_first.lineup_id,
                    from_base=1,
                    to_base=0,
                    is_out=True,
                )
            )
            outs += 1

            # Batter out at first
            movements.append(
                RunnerMovement(
                    lineup_id=state.current_batter.lineup_id,
                    from_base=0,
                    to_base=0,
                    is_out=True,
                )
            )
            outs += 1

            description = "Double play: Runner out at 2nd, batter out at 1st"
        else:
            # Can't turn DP, just batter out
            movements.append(
                RunnerMovement(
                    lineup_id=state.current_batter.lineup_id,
                    from_base=0,
                    to_base=0,
                    is_out=True,
                )
            )
            outs += 1
            description = "Batter out"

        # Other runners advance if play doesn't end inning
        if state.outs + outs < 3:
            if state.is_runner_on_second():
                # Runner scores from second
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_second.lineup_id,
                        from_base=2,
                        to_base=4,
                        is_out=False,
                    )
                )
                runs += 1

            if state.is_runner_on_third():
                # Runner scores from third
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_third.lineup_id,
                        from_base=3,
                        to_base=4,
                        is_out=False,
                    )
                )
                runs += 1

        return AdvancementResult(
            movements=movements,
            outs_recorded=outs,
            runs_scored=runs,
            result_type=GroundballResultType.DOUBLE_PLAY_AT_SECOND,
            description=description,
        )

    def _gb_result_3(self, state: GameState) -> AdvancementResult:
        """
        Result 3: Batter out, all runners advance 1 base.
        """
        movements = []
        runs = 0

        # Batter is out
        movements.append(
            RunnerMovement(
                lineup_id=state.current_batter.lineup_id,
                from_base=0,
                to_base=0,
                is_out=True,
            )
        )

        # All runners advance 1 base (if less than 2 outs after play)
        if state.outs < 2:  # Play doesn't end inning
            if state.is_runner_on_first():
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_first.lineup_id,
                        from_base=1,
                        to_base=2,
                        is_out=False,
                    )
                )

            if state.is_runner_on_second():
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_second.lineup_id,  # type: ignore
                        from_base=2,
                        to_base=3,
                        is_out=False,
                    )
                )

            if state.is_runner_on_third():
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_third.lineup_id,
                        from_base=3,
                        to_base=4,
                        is_out=False,
                    )
                )
                runs += 1

        return AdvancementResult(
            movements=movements,
            outs_recorded=1,
            runs_scored=runs,
            result_type=GroundballResultType.BATTER_OUT_RUNNERS_ADVANCE,
            description="Batter out, runners advance 1 base",
        )

    def _gb_result_4(self, state: GameState) -> AdvancementResult:
        """
        Result 4: Batter safe at 1st, runner on 1st forced out at 2nd.
        Other runners advance 1 base.
        """
        movements = []
        runs = 0

        # Runner on first forced out at second
        if state.is_runner_on_first():
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_first.lineup_id,
                    from_base=1,
                    to_base=0,
                    is_out=True,
                )
            )

        # Batter safe at first
        movements.append(
            RunnerMovement(
                lineup_id=state.current_batter.lineup_id,
                from_base=0,
                to_base=1,
                is_out=False,
            )
        )

        # Other runners advance if play doesn't end inning
        if state.outs < 2:
            if state.is_runner_on_second():
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_second.lineup_id,
                        from_base=2,
                        to_base=3,
                        is_out=False,
                    )
                )

            if state.is_runner_on_third():
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_third.lineup_id,
                        from_base=3,
                        to_base=4,
                        is_out=False,
                    )
                )
                runs += 1

        return AdvancementResult(
            movements=movements,
            outs_recorded=1,
            runs_scored=runs,
            result_type=GroundballResultType.BATTER_SAFE_FORCE_OUT_AT_SECOND,
            description="Batter safe, force out at 2nd, other runners advance",
        )

    def _gb_result_5(self, state: GameState, hit_location: str) -> AdvancementResult:
        """
        Result 5: Conditional on middle infield (2B/SS).

        - Hit to 2B/SS: Batter out, runners advance 1 base (Result 3)
        - Hit anywhere else: Batter out, runners hold (Result 1)
        """
        hit_to_mif = hit_location in ["2B", "SS"]

        if hit_to_mif:
            return self._gb_result_3(state)
        return self._gb_result_1(state)

    def _gb_result_6(self, state: GameState, hit_location: str) -> AdvancementResult:
        """
        Result 6: Conditional on right side (1B/2B).

        - Hit to 1B/2B: Batter out, runners advance 1 base (Result 3)
        - Hit anywhere else: Batter out, runners hold (Result 1)
        """
        hit_to_right = hit_location in ["1B", "2B"]

        if hit_to_right:
            return self._gb_result_3(state)
        return self._gb_result_1(state)

    def _gb_result_7(self, state: GameState) -> AdvancementResult:
        """
        Result 7/8: Batter out, runners advance only if forced.
        """
        movements = []
        runs = 0

        # Batter is out
        movements.append(
            RunnerMovement(
                lineup_id=state.current_batter.lineup_id,
                from_base=0,
                to_base=0,
                is_out=True,
            )
        )

        # Check forced runners (only if play doesn't end inning)
        if state.outs < 2:
            # Runner on 3rd advances only if forced (bases loaded)
            if state.is_runner_on_third():
                forced = state.is_runner_on_first() and state.is_runner_on_second()
                if forced:
                    movements.append(
                        RunnerMovement(
                            lineup_id=state.on_third.lineup_id,
                            from_base=3,
                            to_base=4,
                            is_out=False,
                        )
                    )
                    runs += 1
                else:
                    # Holds
                    movements.append(
                        RunnerMovement(
                            lineup_id=state.on_third.lineup_id,
                            from_base=3,
                            to_base=3,
                            is_out=False,
                        )
                    )

            # Runner on 2nd advances only if forced (1st and 2nd occupied)
            if state.is_runner_on_second():
                forced = state.is_runner_on_first()
                if forced:
                    movements.append(
                        RunnerMovement(
                            lineup_id=state.on_second.lineup_id,
                            from_base=2,
                            to_base=3,
                            is_out=False,
                        )
                    )
                else:
                    # Holds
                    movements.append(
                        RunnerMovement(
                            lineup_id=state.on_second.lineup_id,
                            from_base=2,
                            to_base=2,
                            is_out=False,
                        )
                    )

            # Runner on 1st always forced
            if state.is_runner_on_first():
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_first.lineup_id,
                        from_base=1,
                        to_base=2,
                        is_out=False,
                    )
                )

        return AdvancementResult(
            movements=movements,
            outs_recorded=1,
            runs_scored=runs,
            result_type=GroundballResultType.BATTER_OUT_FORCED_ONLY,
            description="Batter out, forced runners advance",
        )

    def _gb_result_9(self, state: GameState) -> AdvancementResult:
        """
        Result 9: Batter out, runner on 3rd holds, runner on 1st advances to 2nd.

        Specific to 1st & 3rd situation with Infield In positioning.
        """
        movements = []

        # Batter is out
        movements.append(
            RunnerMovement(
                lineup_id=state.current_batter.lineup_id,
                from_base=0,
                to_base=0,
                is_out=True,
            )
        )

        # Runner on 3rd holds
        if state.is_runner_on_third():
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_third.lineup_id,
                    from_base=3,
                    to_base=3,
                    is_out=False,
                )
            )

        # Runner on 1st advances to 2nd
        if state.is_runner_on_first():
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_first.lineup_id,
                    from_base=1,
                    to_base=2,
                    is_out=False,
                )
            )

        return AdvancementResult(
            movements=movements,
            outs_recorded=1,
            runs_scored=0,
            result_type=GroundballResultType.LEAD_HOLDS_TRAIL_ADVANCES,
            description="Batter out, R3 holds, R1 to 2nd",
        )

    def _gb_result_10(
        self,
        state: GameState,
        defensive_decision: DefensiveDecision | None,
        hit_location: str,
    ) -> AdvancementResult:
        """
        Result 10: Double play at home and 1st (bases loaded).

        - With 0 or 1 out: Runner on 3rd out at home, batter out (DP)
        - With 2 outs: Only batter out
        - Runners on 2nd and 1st advance
        """
        movements = []
        outs = 0
        runs = 0

        # Check if double play is possible
        can_turn_dp = state.outs < 2

        if can_turn_dp:
            # Runner on third out at home
            if state.is_runner_on_third():
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_third.lineup_id,
                        from_base=3,
                        to_base=0,
                        is_out=True,
                    )
                )
                outs += 1

            # Batter out at first
            movements.append(
                RunnerMovement(
                    lineup_id=state.current_batter.lineup_id,
                    from_base=0,
                    to_base=0,
                    is_out=True,
                )
            )
            outs += 1

            description = "Double play: Runner out at home, batter out at 1st"
        else:
            # Can't turn DP, just batter out
            movements.append(
                RunnerMovement(
                    lineup_id=state.current_batter.lineup_id,
                    from_base=0,
                    to_base=0,
                    is_out=True,
                )
            )
            outs += 1
            description = "Batter out"

        # Other runners advance if play doesn't end inning
        if state.outs + outs < 3:
            if state.is_runner_on_second():
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_second.lineup_id,
                        from_base=2,
                        to_base=3,
                        is_out=False,
                    )
                )

            if state.is_runner_on_first():
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_first.lineup_id,
                        from_base=1,
                        to_base=2,
                        is_out=False,
                    )
                )

        return AdvancementResult(
            movements=movements,
            outs_recorded=outs,
            runs_scored=runs,
            result_type=GroundballResultType.DOUBLE_PLAY_HOME_TO_FIRST,
            description=description,
        )

    def _gb_result_11(self, state: GameState) -> AdvancementResult:
        """
        Result 11: Batter safe at 1st, lead runner is out.
        Other runners advance 1 base.

        Used for bases loaded scenarios with Infield In.
        """
        movements = []
        runs = 0

        # Lead runner is out (highest base)
        if state.is_runner_on_third():
            # Runner on 3rd is lead runner - out at home or 3rd
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_third.lineup_id,
                    from_base=3,
                    to_base=0,
                    is_out=True,
                )
            )
        elif state.is_runner_on_second():
            # Runner on 2nd is lead runner
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_second.lineup_id,
                    from_base=2,
                    to_base=0,
                    is_out=True,
                )
            )
        elif state.is_runner_on_first():
            # Runner on 1st is lead runner
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_first.lineup_id,
                    from_base=1,
                    to_base=0,
                    is_out=True,
                )
            )

        # Batter safe at first
        movements.append(
            RunnerMovement(
                lineup_id=state.current_batter.lineup_id,
                from_base=0,
                to_base=1,
                is_out=False,
            )
        )

        # Other runners advance if play doesn't end inning
        if state.outs < 2:
            # If runner on 2nd exists and wasn't the lead runner
            if state.is_runner_on_second() and state.is_runner_on_third():
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_second.lineup_id,
                        from_base=2,
                        to_base=3,
                        is_out=False,
                    )
                )

            # If runner on 1st exists and wasn't the lead runner
            if state.is_runner_on_first() and (
                state.is_runner_on_second() or state.is_runner_on_third()
            ):
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_first.lineup_id,
                        from_base=1,
                        to_base=2,
                        is_out=False,
                    )
                )

        return AdvancementResult(
            movements=movements,
            outs_recorded=1,
            runs_scored=runs,
            result_type=GroundballResultType.BATTER_SAFE_LEAD_OUT,
            description="Batter safe, lead runner out, others advance",
        )

    def _gb_result_12(self, state: GameState, hit_location: str) -> AdvancementResult:
        """
        Result 12: DECIDE opportunity.

        Lead runner can attempt to advance a base. Offense chooses whether to attempt.
        If they attempt, defense can take sure out at 1st OR try to throw lead runner out.

        NOTE: This is a simplified implementation. Full DECIDE mechanic requires
        interactive decision-making from offense/defense, which will be handled
        by the game engine / WebSocket layer.

        For now, this returns a result indicating DECIDE opportunity exists,
        and the actual decision-making will be handled at a higher level.
        """
        movements = []

        # Hit to 1B/2B: Simple advancement
        if hit_location in ["1B", "2B"]:
            return self._gb_result_3(state)

        # Hit to 3B: Runners hold
        if hit_location == "3B":
            return self._gb_result_1(state)

        # Hit to SS/P/C: DECIDE opportunity
        # For now, default to conservative play (batter out, runners hold)
        # TODO: This needs to be interactive in the game engine

        movements.append(
            RunnerMovement(
                lineup_id=state.current_batter.lineup_id,
                from_base=0,
                to_base=0,
                is_out=True,
            )
        )

        # Hold all runners by default
        if state.is_runner_on_first():
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_first.lineup_id,
                    from_base=1,
                    to_base=1,
                    is_out=False,
                )
            )

        if state.is_runner_on_second():
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_second.lineup_id,
                    from_base=2,
                    to_base=2,
                    is_out=False,
                )
            )

        if state.is_runner_on_third():
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_third.lineup_id,
                    from_base=3,
                    to_base=3,
                    is_out=False,
                )
            )

        return AdvancementResult(
            movements=movements,
            outs_recorded=1,
            runs_scored=0,
            result_type=GroundballResultType.DECIDE_OPPORTUNITY,
            description="DECIDE opportunity (conservative: batter out, runners hold)",
        )

    def _gb_result_13(
        self,
        state: GameState,
        defensive_decision: DefensiveDecision | None,
        hit_location: str,
    ) -> AdvancementResult:
        """
        Result 13: Conditional double play.

        - Hit to C/3B: Double play at 3rd and 2nd base, batter safe
        - Hit anywhere else: Same as Result 2 (double play at 2nd and 1st)
        """
        hit_to_c_or_3b = hit_location in ["C", "3B"]

        if hit_to_c_or_3b:
            movements = []
            outs = 0

            # Check if DP is possible
            can_turn_dp = state.outs < 2

            if can_turn_dp:
                # Runner on 3rd out
                if state.is_runner_on_third():
                    movements.append(
                        RunnerMovement(
                            lineup_id=state.on_third.lineup_id,
                            from_base=3,
                            to_base=0,
                            is_out=True,
                        )
                    )
                    outs += 1

                # Runner on 2nd out
                if state.is_runner_on_second():
                    movements.append(
                        RunnerMovement(
                            lineup_id=state.on_second.lineup_id,
                            from_base=2,
                            to_base=0,
                            is_out=True,
                        )
                    )
                    outs += 1

                # Batter safe at first
                movements.append(
                    RunnerMovement(
                        lineup_id=state.current_batter.lineup_id,
                        from_base=0,
                        to_base=1,
                        is_out=False,
                    )
                )

                description = "Double play at 3rd and 2nd, batter safe"
            else:
                # Can't turn DP
                movements.append(
                    RunnerMovement(
                        lineup_id=state.current_batter.lineup_id,
                        from_base=0,
                        to_base=0,
                        is_out=True,
                    )
                )
                outs += 1
                description = "Batter out"

            return AdvancementResult(
                movements=movements,
                outs_recorded=outs,
                runs_scored=0,
                result_type=GroundballResultType.CONDITIONAL_DOUBLE_PLAY,
                description=description,
            )
        # Same as Result 2
        return self._gb_result_2(state, defensive_decision, hit_location)

    # ========================================
    # FLYBALL METHODS
    # ========================================

    def _advance_runners_flyball(
        self,
        outcome: PlayOutcome,
        hit_location: str,
        state: GameState,
        defensive_decision: DefensiveDecision,
    ) -> AdvancementResult:
        """
        Calculate runner advancement for flyball outcome.

        Direct mapping (no chart needed):
        - FLYOUT_A: Deep - all runners tag up and advance one base
        - FLYOUT_B: Medium - R3 scores, R2 may attempt (DECIDE), R1 holds
        - FLYOUT_BQ: Medium-shallow - R3 may attempt to score (DECIDE), R2 holds, R1 holds
        - FLYOUT_C: Shallow - no advancement

        Args:
            outcome: The play outcome (FLYOUT_A, B, BQ, or C)
            hit_location: Where the ball was hit (LF, CF, RF)
            state: Current game state
            defensive_decision: Defensive team's positioning decisions

        Returns:
            AdvancementResult with all runner movements and outs
        """
        self.logger.info(
            f"Flyball {outcome.name} to {hit_location}, bases {state.current_on_base_code}, "
            f"{state.outs} outs"
        )

        # Dispatch directly based on outcome
        if outcome == PlayOutcome.FLYOUT_A:
            return self._fb_result_deep(state, hit_location)
        if outcome == PlayOutcome.FLYOUT_B:
            return self._fb_result_medium(state, hit_location)
        if outcome == PlayOutcome.FLYOUT_BQ:
            return self._fb_result_bq(state, hit_location)
        if outcome == PlayOutcome.FLYOUT_C:
            return self._fb_result_shallow(state, hit_location)
        raise ValueError(f"Unknown flyball outcome: {outcome}")

    # ========================================
    # Flyball Result Handlers
    # ========================================

    def _fb_result_deep(self, state: GameState, hit_location: str) -> AdvancementResult:
        """
        FLYOUT_A: Deep flyball - all runners tag up and advance one base.

        - Runner on 3rd scores (sacrifice fly)
        - Runner on 2nd advances to 3rd
        - Runner on 1st advances to 2nd
        - Batter is out
        """
        movements = []
        runs = 0

        # Batter is out
        movements.append(
            RunnerMovement(
                lineup_id=state.current_batter.lineup_id,
                from_base=0,
                to_base=0,
                is_out=True,
            )
        )

        # All runners tag up and advance one base (if less than 3 outs)
        if state.outs < 2:  # Play doesn't end inning
            if state.is_runner_on_third():
                # Runner scores (sacrifice fly)
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_third.lineup_id,
                        from_base=3,
                        to_base=4,
                        is_out=False,
                    )
                )
                runs += 1

            if state.is_runner_on_second():
                # Runner advances to third
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_second.lineup_id,
                        from_base=2,
                        to_base=3,
                        is_out=False,
                    )
                )

            if state.is_runner_on_first():
                # Runner advances to second
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_first.lineup_id,
                        from_base=1,
                        to_base=2,
                        is_out=False,
                    )
                )

        # Build dynamic description based on outs and actual results
        if state.outs >= 2:
            desc = f"Deep flyball to {hit_location} - 3rd out, inning over"
        else:
            parts = [f"Deep flyball to {hit_location}"]
            if runs > 0:
                parts.append("R3 scores")
            if state.is_runner_on_second():
                parts.append("R2→3B")
            if state.is_runner_on_first():
                parts.append("R1→2B")
            desc = " - ".join(parts) if len(parts) > 1 else parts[0] + " - all runners tag"

        return AdvancementResult(
            movements=movements,
            outs_recorded=1,
            runs_scored=runs,
            result_type=None,  # Flyballs don't use result types
            description=desc,
        )

    def _fb_result_medium(
        self, state: GameState, hit_location: str
    ) -> AdvancementResult:
        """
        FLYOUT_B: Medium flyball - interactive tag-up situation.

        - Runner on 3rd scores (always)
        - Runner on 2nd may attempt to tag to 3rd (DECIDE opportunity)
        - Runner on 1st holds (too risky)
        - Batter is out

        NOTE: DECIDE mechanic will be handled by game engine/WebSocket layer.
        For now, this returns conservative default (R2 holds).
        """
        movements = []
        runs = 0

        # Batter is out
        movements.append(
            RunnerMovement(
                lineup_id=state.current_batter.lineup_id,
                from_base=0,
                to_base=0,
                is_out=True,
            )
        )

        # Runner advancement (if less than 3 outs)
        if state.outs < 2:
            if state.is_runner_on_third():
                # Runner on 3rd always scores
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_third.lineup_id,
                        from_base=3,
                        to_base=4,
                        is_out=False,
                    )
                )
                runs += 1

            if state.is_runner_on_second():
                # DECIDE opportunity - R2 may attempt to tag to 3rd
                # TODO: Interactive decision-making via game engine
                # For now: conservative default (holds)
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_second.lineup_id,
                        from_base=2,
                        to_base=2,  # Holds by default
                        is_out=False,
                    )
                )

            if state.is_runner_on_first():
                # Runner on 1st always holds (too risky)
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_first.lineup_id,
                        from_base=1,
                        to_base=1,
                        is_out=False,
                    )
                )

        # Build dynamic description based on outs and actual results
        if state.outs >= 2:
            desc = f"Medium flyball to {hit_location} - 3rd out, inning over"
        else:
            parts = [f"Medium flyball to {hit_location}"]
            if runs > 0:
                parts.append("R3 scores")
            if state.is_runner_on_second():
                parts.append("R2 DECIDE (held)")
            if state.is_runner_on_first():
                parts.append("R1 holds")
            desc = " - ".join(parts) if len(parts) > 1 else parts[0]

        return AdvancementResult(
            movements=movements,
            outs_recorded=1,
            runs_scored=runs,
            result_type=None,  # Flyballs don't use result types
            description=desc,
        )

    def _fb_result_bq(self, state: GameState, hit_location: str) -> AdvancementResult:
        """
        FLYOUT_BQ: Medium-shallow flyball (fly(b)?) - interactive tag-up situation.

        - Runner on 3rd may attempt to score (DECIDE opportunity)
        - Runner on 2nd holds (too risky)
        - Runner on 1st holds (too risky)
        - Batter is out

        NOTE: DECIDE mechanic will be handled by game engine/WebSocket layer.
        For now, this returns conservative default (R3 holds).
        """
        movements = []
        runs = 0

        # Batter is out
        movements.append(
            RunnerMovement(
                lineup_id=state.current_batter.lineup_id,
                from_base=0,
                to_base=0,
                is_out=True,
            )
        )

        # Runner advancement (if less than 3 outs)
        if state.outs < 2:
            if state.is_runner_on_third():
                # DECIDE opportunity - R3 may attempt to score
                # TODO: Interactive decision-making via game engine
                # For now: conservative default (holds)
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_third.lineup_id,
                        from_base=3,
                        to_base=3,  # Holds by default
                        is_out=False,
                    )
                )

            if state.is_runner_on_second():
                # Runner on 2nd holds (too risky)
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_second.lineup_id,
                        from_base=2,
                        to_base=2,
                        is_out=False,
                    )
                )

            if state.is_runner_on_first():
                # Runner on 1st holds (too risky)
                movements.append(
                    RunnerMovement(
                        lineup_id=state.on_first.lineup_id,
                        from_base=1,
                        to_base=1,
                        is_out=False,
                    )
                )

        # Build dynamic description based on outs and actual results
        if state.outs >= 2:
            desc = f"Medium-shallow flyball to {hit_location} - 3rd out, inning over"
        else:
            parts = [f"Medium-shallow flyball to {hit_location}"]
            if state.is_runner_on_third():
                parts.append("R3 DECIDE (held)")
            if state.is_runner_on_second() or state.is_runner_on_first():
                parts.append("all others hold")
            desc = " - ".join(parts) if len(parts) > 1 else parts[0]

        return AdvancementResult(
            movements=movements,
            outs_recorded=1,
            runs_scored=runs,
            result_type=None,  # Flyballs don't use result types
            description=desc,
        )

    def _fb_result_shallow(self, state: GameState, hit_location: str) -> AdvancementResult:
        """
        FLYOUT_C: Shallow flyball - no runners advance.

        - Batter is out
        - All runners hold (too shallow to tag)
        """
        movements = []

        # Batter is out
        movements.append(
            RunnerMovement(
                lineup_id=state.current_batter.lineup_id,
                from_base=0,
                to_base=0,
                is_out=True,
            )
        )

        # All runners hold
        if state.is_runner_on_first():
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_first.lineup_id,
                    from_base=1,
                    to_base=1,
                    is_out=False,
                )
            )

        if state.is_runner_on_second():
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_second.lineup_id,
                    from_base=2,
                    to_base=2,
                    is_out=False,
                )
            )

        if state.is_runner_on_third():
            movements.append(
                RunnerMovement(
                    lineup_id=state.on_third.lineup_id,
                    from_base=3,
                    to_base=3,
                    is_out=False,
                )
            )

        return AdvancementResult(
            movements=movements,
            outs_recorded=1,
            runs_scored=0,
            result_type=None,  # Flyballs don't use result types
            description="Shallow flyball - all runners hold",
        )


# ============================================================================
# X-CHECK RUNNER ADVANCEMENT (Placeholders - to be implemented in Phase 3D)
# ============================================================================


def x_check_g1(
    on_base_code: int,
    defender_in: bool,
    error_result: str,
    state: GameState,
    hit_location: str,
    defensive_decision: DefensiveDecision,
) -> AdvancementResult:
    """
    Runner advancement for X-Check G1 result.

    Uses G1 advancement table to get GroundballResultType based on
    base situation, defensive positioning, and error result.

    Args:
        on_base_code: Current base situation code (0-7 bit field)
        defender_in: Is the defender playing in?
        error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
        state: Current game state (for lineup IDs, outs, etc.)
        hit_location: Where the ball was hit (1B, 2B, SS, 3B, P, C)
        defensive_decision: Defensive positioning decision

    Returns:
        AdvancementResult with runner movements
    """
    from app.core.x_check_advancement_tables import (
        build_advancement_from_code,
        get_groundball_advancement,
    )

    # Lookup groundball result type from table
    gb_type = get_groundball_advancement("G1", on_base_code, defender_in, error_result)

    # If error result: use simple error advancement (doesn't need GameState details)
    if error_result in ["E1", "E2", "E3", "RP"]:
        return build_advancement_from_code(on_base_code, gb_type, result_name="G1")

    # If no error: delegate to existing result handler (needs full GameState)
    runner_adv = RunnerAdvancement()
    return runner_adv._execute_result(
        result_type=gb_type,
        state=state,
        hit_location=hit_location,
        defensive_decision=defensive_decision,
    )


def x_check_g2(
    on_base_code: int,
    defender_in: bool,
    error_result: str,
    state: GameState,
    hit_location: str,
    defensive_decision: DefensiveDecision,
) -> AdvancementResult:
    """
    Runner advancement for X-Check G2 result.

    Uses G2 advancement table to get GroundballResultType based on
    base situation, defensive positioning, and error result.

    Args:
        on_base_code: Current base situation code (0-7 bit field)
        defender_in: Is the defender playing in?
        error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
        state: Current game state (for lineup IDs, outs, etc.)
        hit_location: Where the ball was hit (1B, 2B, SS, 3B, P, C)
        defensive_decision: Defensive positioning decision

    Returns:
        AdvancementResult with runner movements
    """
    from app.core.x_check_advancement_tables import (
        build_advancement_from_code,
        get_groundball_advancement,
    )

    gb_type = get_groundball_advancement("G2", on_base_code, defender_in, error_result)

    # If error result: use simple error advancement (doesn't need GameState details)
    if error_result in ["E1", "E2", "E3", "RP"]:
        return build_advancement_from_code(on_base_code, gb_type, result_name="G2")

    # If no error: delegate to existing result handler (needs full GameState)
    runner_adv = RunnerAdvancement()
    return runner_adv._execute_result(
        result_type=gb_type,
        state=state,
        hit_location=hit_location,
        defensive_decision=defensive_decision,
    )


def x_check_g3(
    on_base_code: int,
    defender_in: bool,
    error_result: str,
    state: GameState,
    hit_location: str,
    defensive_decision: DefensiveDecision,
) -> AdvancementResult:
    """
    Runner advancement for X-Check G3 result.

    Uses G3 advancement table to get GroundballResultType based on
    base situation, defensive positioning, and error result.

    Args:
        on_base_code: Current base situation code (0-7 bit field)
        defender_in: Is the defender playing in?
        error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
        state: Current game state (for lineup IDs, outs, etc.)
        hit_location: Where the ball was hit (1B, 2B, SS, 3B, P, C)
        defensive_decision: Defensive positioning decision

    Returns:
        AdvancementResult with runner movements
    """
    from app.core.x_check_advancement_tables import (
        build_advancement_from_code,
        get_groundball_advancement,
    )

    gb_type = get_groundball_advancement("G3", on_base_code, defender_in, error_result)

    # If error result: use simple error advancement (doesn't need GameState details)
    if error_result in ["E1", "E2", "E3", "RP"]:
        return build_advancement_from_code(on_base_code, gb_type, result_name="G3")

    # If no error: delegate to existing result handler (needs full GameState)
    runner_adv = RunnerAdvancement()
    return runner_adv._execute_result(
        result_type=gb_type,
        state=state,
        hit_location=hit_location,
        defensive_decision=defensive_decision,
    )


def x_check_f1(
    on_base_code: int, error_result: str, state: GameState, hit_location: str
) -> AdvancementResult:
    """
    Runner advancement for X-Check F1 (deep flyball) result.

    F1 maps to FLYOUT_A behavior:
    - If error: all runners advance E# bases (out negated)
    - If no error: delegate to existing FLYOUT_A logic

    Args:
        on_base_code: Current base situation code (0-7 bit field)
        error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
        state: Current game state (for lineup IDs, outs, etc.)
        hit_location: Where the ball was hit (LF, CF, RF)

    Returns:
        AdvancementResult with runner movements
    """
    from app.core.x_check_advancement_tables import build_flyball_advancement_with_error

    # If error result: use simple error advancement (doesn't need GameState details)
    if error_result != "NO":
        return build_flyball_advancement_with_error(
            on_base_code, error_result, flyball_type="F1"
        )

    # If no error: delegate to existing FLYOUT_A logic
    runner_adv = RunnerAdvancement()
    return runner_adv._fb_result_deep(state, hit_location)


def x_check_f2(
    on_base_code: int, error_result: str, state: GameState, hit_location: str
) -> AdvancementResult:
    """
    Runner advancement for X-Check F2 (medium flyball) result.

    F2 maps to FLYOUT_B behavior:
    - If error: all runners advance E# bases (out negated)
    - If no error: delegate to existing FLYOUT_B logic

    Args:
        on_base_code: Current base situation code (0-7 bit field)
        error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
        state: Current game state (for lineup IDs, outs, etc.)
        hit_location: Where the ball was hit (LF, CF, RF)

    Returns:
        AdvancementResult with runner movements
    """
    from app.core.x_check_advancement_tables import build_flyball_advancement_with_error

    # If error result: use simple error advancement (doesn't need GameState details)
    if error_result != "NO":
        return build_flyball_advancement_with_error(
            on_base_code, error_result, flyball_type="F2"
        )

    # If no error: delegate to existing FLYOUT_B logic
    runner_adv = RunnerAdvancement()
    return runner_adv._fb_result_medium(state, hit_location)


def x_check_f3(
    on_base_code: int, error_result: str, state: GameState, hit_location: str
) -> AdvancementResult:
    """
    Runner advancement for X-Check F3 (shallow flyball) result.

    F3 maps to FLYOUT_C behavior:
    - If error: all runners advance E# bases (out negated)
    - If no error: delegate to existing FLYOUT_C logic (batter out, no advancement)

    Args:
        on_base_code: Current base situation code (0-7 bit field)
        error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
        state: Current game state (for lineup IDs, outs, etc.)
        hit_location: Where the ball was hit (LF, CF, RF)

    Returns:
        AdvancementResult with runner movements
    """
    from app.core.x_check_advancement_tables import build_flyball_advancement_with_error

    # If error result: use simple error advancement (doesn't need GameState details)
    if error_result != "NO":
        return build_flyball_advancement_with_error(
            on_base_code, error_result, flyball_type="F3"
        )

    # If no error: delegate to existing FLYOUT_C logic
    runner_adv = RunnerAdvancement()
    return runner_adv._fb_result_shallow(state, hit_location)
