"""
Shared command implementations for terminal client.

This module contains the core logic for game commands that can be
used by both the REPL (repl.py) and CLI (main.py) interfaces.

Author: Claude
Date: 2025-10-27
Updated: 2025-10-30 - Added manual outcome commands
"""
import asyncio
import logging
from uuid import UUID, uuid4
from typing import Optional, List, Tuple

from app.core.game_engine import game_engine
from app.core.state_manager import state_manager
from app.core.dice import dice_system
from app.models.game_models import DefensiveDecision, OffensiveDecision
from app.config import PlayOutcome
from app.database.operations import DatabaseOperations
from terminal_client import display
from terminal_client.config import Config

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


class GameCommands:
    """Shared command implementations for game operations."""

    def __init__(self):
        self.db_ops = DatabaseOperations()

    async def create_new_game(
        self,
        league: str = 'sba',
        home_team: int = 1,
        away_team: int = 2,
        set_current: bool = True
    ) -> Tuple[UUID, bool]:
        """
        Create a new game with lineups and start it.

        Args:
            league: 'sba' or 'pd'
            home_team: Home team ID
            away_team: Away team ID
            set_current: Whether to set as current game

        Returns:
            Tuple of (game_id, success)
        """
        gid = uuid4()

        try:
            # Step 1: Create game in memory and database
            display.print_info("Step 1: Creating game...")

            state = await state_manager.create_game(
                game_id=gid,
                league_id=league,
                home_team_id=home_team,
                away_team_id=away_team
            )

            await self.db_ops.create_game(
                game_id=gid,
                league_id=league,
                home_team_id=home_team,
                away_team_id=away_team,
                game_mode="friendly",
                visibility="public"
            )

            display.print_success(f"Game created: {gid}")

            if set_current:
                Config.set_current_game(gid)
                display.print_info(f"Current game set to: {gid}")

            # Step 2: Setup lineups
            display.print_info("Step 2: Creating test lineups...")
            await self._create_test_lineups(gid, league, home_team, away_team)

            # Step 3: Start the game
            display.print_info("Step 3: Starting game...")
            state = await game_engine.start_game(gid)

            display.print_success(f"Game started - Inning {state.inning} {state.half}")
            display.display_game_state(state)

            return gid, True

        except Exception as e:
            display.print_error(f"Failed to create new game: {e}")
            logger.exception("New game error")
            return gid, False

    async def _create_test_lineups(
        self,
        game_id: UUID,
        league: str,
        home_team: int,
        away_team: int
    ) -> None:
        """Create test lineups for both teams."""
        positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']

        for team_id in [home_team, away_team]:
            team_name = "Home" if team_id == home_team else "Away"

            for i, position in enumerate(positions, start=1):
                if league == 'sba':
                    player_id = (team_id * 100) + i
                    await self.db_ops.add_sba_lineup_player(
                        game_id=game_id,
                        team_id=team_id,
                        player_id=player_id,
                        position=position,
                        batting_order=i,
                        is_starter=True
                    )
                else:
                    card_id = (team_id * 100) + i
                    await self.db_ops.add_pd_lineup_card(
                        game_id=game_id,
                        team_id=team_id,
                        card_id=card_id,
                        position=position,
                        batting_order=i,
                        is_starter=True
                    )

            display.console.print(f"  ✓ {team_name} team lineup created (9 players)")

    async def submit_defensive_decision(
        self,
        game_id: UUID,
        alignment: str = 'normal',
        infield: str = 'normal',
        outfield: str = 'normal',
        hold_runners: Optional[List[int]] = None
    ) -> bool:
        """
        Submit defensive decision.

        Returns:
            True if successful, False otherwise
        """
        try:
            decision = DefensiveDecision(
                alignment=alignment,
                infield_depth=infield,
                outfield_depth=outfield,
                hold_runners=hold_runners or []
            )

            state = await game_engine.submit_defensive_decision(game_id, decision)
            display.print_success("Defensive decision submitted")
            display.display_decision("defensive", decision)
            display.display_game_state(state)
            return True

        except Exception as e:
            display.print_error(f"Failed to submit defensive decision: {e}")
            logger.exception("Defensive decision error")
            return False

    async def submit_offensive_decision(
        self,
        game_id: UUID,
        action: str = 'swing_away',
        steal_attempts: Optional[List[int]] = None
    ) -> bool:
        """
        Submit offensive decision.

        Args:
            action: Offensive action (swing_away, steal, check_jump, hit_and_run, sac_bunt, squeeze_bunt)
            steal_attempts: List of bases to steal (required when action="steal")

        Returns:
            True if successful, False otherwise

        Session 2 Update (2025-01-14): Replaced approach with action field. Removed deprecated fields.
        """
        try:
            decision = OffensiveDecision(
                action=action,
                steal_attempts=steal_attempts or []
            )

            state = await game_engine.submit_offensive_decision(game_id, decision)
            display.print_success("Offensive decision submitted")
            display.display_decision("offensive", decision)
            display.display_game_state(state)
            return True

        except Exception as e:
            display.print_error(f"Failed to submit offensive decision: {e}")
            logger.exception("Offensive decision error")
            return False

    async def resolve_play(
        self,
        game_id: UUID,
        forced_outcome: Optional[PlayOutcome] = None,
        xcheck_position: Optional[str] = None,
        xcheck_result: Optional[str] = None,
        xcheck_error: Optional[str] = None
    ) -> bool:
        """
        Resolve the current play.

        Args:
            game_id: Game to resolve
            forced_outcome: If provided, use this outcome instead of rolling dice
            xcheck_position: For X_CHECK outcomes, the position to check (SS, LF, etc.)
            xcheck_result: For X_CHECK, force the converted result (G1, G2, SI2, DO2, etc.)
            xcheck_error: For X_CHECK, force the error result (NO, E1, E2, E3, RP)

        Returns:
            True if successful, False otherwise
        """
        try:
            # Get game state to check mode
            state = state_manager.get_state(game_id)
            if not state:
                display.print_error(f"Game {game_id} not found")
                return False

            # If no forced outcome and in manual mode, use manual resolution with random outcome
            if forced_outcome is None:
                # For terminal testing, auto-generate a random outcome
                import random
                test_outcomes = [
                    PlayOutcome.STRIKEOUT,
                    PlayOutcome.GROUNDBALL_A,
                    PlayOutcome.GROUNDBALL_B,
                    PlayOutcome.GROUNDBALL_C,
                    PlayOutcome.FLYOUT_A,
                    PlayOutcome.FLYOUT_B,
                    PlayOutcome.FLYOUT_C,
                    PlayOutcome.SINGLE_1,
                    PlayOutcome.DOUBLE_2,
                    PlayOutcome.HOMERUN,
                    PlayOutcome.WALK,
                ]
                forced_outcome = random.choice(test_outcomes)
                display.print_info(f"🎲 Auto-generated outcome for testing: {forced_outcome.value}")

            # Display what we're doing (if forcing specific outcome)
            if xcheck_position:
                if xcheck_result:
                    error_display = f"+{xcheck_error}" if xcheck_error and xcheck_error != 'NO' else ""
                    display.print_info(f"🎯 Forcing X-Check to: {xcheck_position} → {xcheck_result}{error_display}")
                else:
                    display.print_info(f"🎯 Forcing X-Check to: {xcheck_position}")

            result = await game_engine.resolve_play(
                game_id,
                forced_outcome,
                xcheck_position,
                xcheck_result,
                xcheck_error
            )
            state = await game_engine.get_game_state(game_id)

            if state:
                display.display_play_result(result, state)
                display.display_game_state(state)
                return True
            else:
                display.print_error(f"Game {game_id} not found after resolution")
                return False

        except Exception as e:
            display.print_error(f"Failed to resolve play: {e}")
            logger.exception("Resolve play error")
            return False

    def list_outcomes(self) -> None:
        """
        Display all available PlayOutcome values for manual selection.
        """
        from rich.table import Table
        from rich.console import Console

        console = Console()

        # Create categorized table
        table = Table(title="Available Play Outcomes", show_header=True, header_style="bold cyan")
        table.add_column("Category", style="yellow", width=20)
        table.add_column("Outcome", style="green", width=25)
        table.add_column("Description", style="white", width=50)

        # Outs
        table.add_row("Outs", "strikeout", "Batter strikes out")
        table.add_row("", "groundball_a", "Groundball - double play if possible")
        table.add_row("", "groundball_b", "Groundball - standard")
        table.add_row("", "groundball_c", "Groundball - weak contact")
        table.add_row("", "flyout_a", "Flyout variant A")
        table.add_row("", "flyout_b", "Flyout variant B (medium depth)")
        table.add_row("", "flyout_c", "Flyout variant C (deep)")
        table.add_row("", "lineout", "Line drive out")
        table.add_row("", "popout", "Pop fly out")

        # Hits
        table.add_row("Hits", "single_1", "Single - standard advancement")
        table.add_row("", "single_2", "Single - enhanced advancement")
        table.add_row("", "single_uncapped", "Single (uncapped) - decision tree")
        table.add_row("", "double_2", "Double to 2nd base")
        table.add_row("", "double_3", "Double to 3rd base")
        table.add_row("", "double_uncapped", "Double (uncapped) - decision tree")
        table.add_row("", "triple", "Triple")
        table.add_row("", "homerun", "Home run")

        # Walks/HBP
        table.add_row("Walks/HBP", "walk", "Base on balls")
        table.add_row("", "hbp", "Hit by pitch")
        table.add_row("", "intentional_walk", "Intentional walk")

        # Errors
        table.add_row("Errors", "error", "Defensive error")

        # Interrupts
        table.add_row("Interrupts", "wild_pitch", "Wild pitch (pa=0)")
        table.add_row("", "passed_ball", "Passed ball (pa=0)")
        table.add_row("", "stolen_base", "Stolen base (pa=0)")
        table.add_row("", "caught_stealing", "Caught stealing (pa=0)")
        table.add_row("", "balk", "Balk (pa=0)")
        table.add_row("", "pick_off", "Pick off (pa=0)")

        # Ballpark Power
        table.add_row("Ballpark", "bp_homerun", "Ballpark home run")
        table.add_row("", "bp_single", "Ballpark single")
        table.add_row("", "bp_flyout", "Ballpark flyout")
        table.add_row("", "bp_lineout", "Ballpark lineout")

        console.print(table)
        console.print("\n[cyan]Usage:[/cyan] [green]resolve_with[/green] [yellow]<outcome>[/yellow]")
        console.print("[dim]Example: resolve_with single_1[/dim]")

    async def quick_play_rounds(
        self,
        game_id: UUID,
        count: int = 1
    ) -> int:
        """
        Execute multiple plays with default decisions.

        Returns:
            Number of plays successfully executed
        """
        plays_completed = 0

        for i in range(count):
            try:
                state = await game_engine.get_game_state(game_id)
                if not state or state.status != "active":
                    display.print_warning(f"Game ended at play {i + 1}")
                    break

                display.print_info(f"Play {i + 1}/{count}")

                # Submit default decisions
                await game_engine.submit_defensive_decision(game_id, DefensiveDecision())
                await game_engine.submit_offensive_decision(game_id, OffensiveDecision())

                # Resolve
                result = await game_engine.resolve_play(game_id)
                state = await game_engine.get_game_state(game_id)

                if state:
                    display.print_success(f"{result.description}")
                    display.console.print(
                        f"[cyan]Score: Away {state.away_score} - {state.home_score} Home, "
                        f"Inning {state.inning} {state.half}, {state.outs} outs[/cyan]"
                    )
                    plays_completed += 1

                await asyncio.sleep(0.3)  # Brief pause for readability

            except Exception as e:
                display.print_error(f"Error on play {i + 1}: {e}")
                logger.exception("Quick play error")
                break

        # Show final state
        state = await game_engine.get_game_state(game_id)
        if state:
            display.print_info("Final state:")
            display.display_game_state(state)

        return plays_completed

    async def show_game_status(self, game_id: UUID) -> bool:
        """
        Display current game state.

        Returns:
            True if successful, False otherwise
        """
        try:
            state = await game_engine.get_game_state(game_id)
            if state:
                display.display_game_state(state)
                return True
            else:
                display.print_error(f"Game {game_id} not found")
                return False
        except Exception as e:
            display.print_error(f"Failed to get game status: {e}")
            return False

    async def show_box_score(self, game_id: UUID) -> bool:
        """
        Display box score using materialized views.

        Returns:
            True if successful, False otherwise
        """
        try:
            from app.services import box_score_service

            # Get box score from materialized views
            box_score = await box_score_service.get_box_score(game_id)

            if box_score:
                display.display_box_score(box_score)
                return True
            else:
                display.print_error(f"No box score found for game {game_id}")
                display.print_info("Note: Run migration first (alembic upgrade head) and refresh views")
                return False
        except Exception as e:
            display.print_error(f"Failed to get box score: {e}")
            logger.error(f"Box score error for game {game_id}: {e}", exc_info=True)
            return False

    def validate_manual_outcome(self, outcome: str, location: Optional[str] = None) -> bool:
        """
        Validate a manual outcome submission.

        Args:
            outcome: PlayOutcome enum value (e.g., 'groundball_c')
            location: Optional hit location (e.g., 'SS')

        Returns:
            True if valid, False otherwise
        """
        from app.models.game_models import ManualOutcomeSubmission
        from pydantic import ValidationError

        try:
            # Try to create ManualOutcomeSubmission
            submission = ManualOutcomeSubmission(
                outcome=outcome,
                hit_location=location
            )

            # Show success
            display.print_success(f"✅ Valid manual outcome submission")
            display.console.print(f"  [cyan]Outcome:[/cyan] [green]{submission.outcome}[/green]")
            if submission.hit_location:
                display.console.print(f"  [cyan]Location:[/cyan] [green]{submission.hit_location}[/green]")
            else:
                display.console.print(f"  [cyan]Location:[/cyan] [dim]None (not required for this outcome)[/dim]")

            # Check if location is required
            outcome_enum = PlayOutcome(outcome)
            if outcome_enum.requires_hit_location():
                if not location:
                    display.print_warning("⚠️  Note: This outcome typically requires a hit location")
                    display.console.print("     [dim]Groundballs and flyouts need location for runner advancement[/dim]")

            return True

        except ValidationError as e:
            display.print_error("❌ Invalid manual outcome submission")
            for error in e.errors():
                field = error['loc'][0] if error['loc'] else 'unknown'
                message = error['msg']
                display.console.print(f"  [red]•[/red] [yellow]{field}:[/yellow] {message}")
            return False
        except Exception as e:
            display.print_error(f"Validation error: {e}")
            return False

    def test_hit_location(self, outcome: str, handedness: str = 'R', count: int = 10) -> None:
        """
        Test hit location calculation for a given outcome and handedness.

        Args:
            outcome: PlayOutcome enum value (e.g., 'groundball_c')
            handedness: Batter handedness ('L' or 'R')
            count: Number of samples to generate

        Displays:
            Distribution of hit locations
        """
        from app.config.result_charts import calculate_hit_location, PlayOutcome
        from collections import Counter
        from rich.table import Table

        try:
            outcome_enum = PlayOutcome(outcome)
        except ValueError:
            display.print_error(f"Invalid outcome: {outcome}")
            display.console.print("  [dim]Use 'list_outcomes' to see valid outcomes[/dim]")
            return

        # Generate samples
        locations = []
        for _ in range(count):
            location = calculate_hit_location(outcome_enum, handedness)
            if location:
                locations.append(location)

        if not locations:
            display.print_info(f"Outcome '{outcome}' does not require hit location")
            display.console.print("  [dim]Location only tracked for groundballs and flyouts[/dim]")
            return

        # Count distribution
        counter = Counter(locations)
        total = len(locations)

        # Display results
        display.print_success(f"Hit Location Distribution for {outcome} ({handedness}HB)")

        table = Table(show_header=True, header_style="bold cyan")
        table.add_column("Location", style="yellow", width=15)
        table.add_column("Count", style="green", width=10, justify="right")
        table.add_column("Percentage", style="cyan", width=15, justify="right")
        table.add_column("Visual", style="white", width=30)

        for location in sorted(counter.keys()):
            count_val = counter[location]
            pct = (count_val / total) * 100
            bar = "█" * int(pct / 3)  # Scale bar to fit
            table.add_row(
                location,
                str(count_val),
                f"{pct:.1f}%",
                bar
            )

        display.console.print(table)

        # Show pull rates info
        display.console.print(f"\n[dim]Pull rates: 45% pull, 35% center, 20% opposite[/dim]")
        if handedness == 'R':
            display.console.print(f"[dim]RHB pulls left (3B, SS, LF)[/dim]")
        else:
            display.console.print(f"[dim]LHB pulls right (1B, 2B, RF)[/dim]")

    async def rollback_plays(self, game_id: UUID, num_plays: int) -> bool:
        """
        Roll back the last N plays.

        Deletes plays and reconstructs game state from remaining plays.

        Args:
            game_id: Game to roll back
            num_plays: Number of plays to roll back

        Returns:
            True if successful, False otherwise
        """
        try:
            display.print_info(f"Rolling back {num_plays} play(s)...")

            # Call game engine rollback
            state = await game_engine.rollback_plays(game_id, num_plays)

            display.print_success(f"✓ Rolled back {num_plays} play(s)")
            display.console.print(f"  [cyan]Now at play:[/cyan] {state.play_count}")
            display.console.print(f"  [cyan]Inning:[/cyan] {state.inning} {state.half}")
            display.console.print(f"  [cyan]Score:[/cyan] Away {state.away_score} - {state.home_score} Home")

            # Show current game state
            display.display_game_state(state)

            return True

        except ValueError as e:
            display.print_error(f"Cannot roll back: {e}")
            return False
        except Exception as e:
            display.print_error(f"Failed to roll back plays: {e}")
            logger.exception("Rollback error")
            return False

    async def roll_manual_dice(self, game_id: UUID) -> bool:
        """
        Roll dice for manual outcome mode.

        Server rolls dice and stores in state for fairness/auditing.
        Players then read their physical cards and submit outcomes.

        Args:
            game_id: Game to roll dice for

        Returns:
            True if successful, False otherwise
        """
        try:
            # Get game state
            state = state_manager.get_state(game_id)
            if not state:
                display.print_error(f"Game {game_id} not found")
                return False

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

            # Store in state
            state.pending_manual_roll = ab_roll
            state_manager.update_state(game_id, state)

            # Display dice results
            display.print_success("✓ Dice rolled!")
            display.console.print(f"\n[bold cyan]Dice Results:[/bold cyan]")
            display.console.print(f"  [cyan]Roll ID:[/cyan] {ab_roll.roll_id}")
            display.console.print(f"  [cyan]Column (1d6):[/cyan] {ab_roll.d6_one}")
            display.console.print(f"  [cyan]Row (2d6):[/cyan] {ab_roll.d6_two_total} ({ab_roll.d6_two_a}+{ab_roll.d6_two_b})")
            display.console.print(f"  [cyan]Chaos (1d20):[/cyan] {ab_roll.chaos_d20}")
            display.console.print(f"  [cyan]Resolution (1d20):[/cyan] {ab_roll.resolution_d20}")

            if ab_roll.check_wild_pitch:
                display.console.print(f"\n[yellow]⚠️  Wild pitch check! (chaos d20 = 1)[/yellow]")
            elif ab_roll.check_passed_ball:
                display.console.print(f"\n[yellow]⚠️  Passed ball check! (chaos d20 = 2)[/yellow]")

            display.console.print(f"\n[dim]Read your physical card and submit outcome with:[/dim]")
            display.console.print(f"[dim]  manual_outcome <outcome> [hit_location][/dim]")

            return True

        except Exception as e:
            display.print_error(f"Failed to roll dice: {e}")
            logger.exception("Roll dice error")
            return False

    async def submit_manual_outcome(
        self,
        game_id: UUID,
        outcome: str,
        hit_location: Optional[str] = None
    ) -> bool:
        """
        Submit manually-selected outcome from physical card.

        Args:
            game_id: Game to submit outcome for
            outcome: PlayOutcome value (e.g., 'groundball_c', 'walk')
            hit_location: Optional hit location (e.g., 'SS', '1B')

        Returns:
            True if successful, False otherwise
        """
        try:
            # Get game state
            state = state_manager.get_state(game_id)
            if not state:
                display.print_error(f"Game {game_id} not found")
                return False

            # Validate outcome
            try:
                play_outcome = PlayOutcome(outcome.lower())
            except ValueError:
                valid_outcomes = [o.value for o in PlayOutcome]
                display.print_error(f"Invalid outcome: {outcome}")
                display.console.print(f"[dim]Valid outcomes: {', '.join(valid_outcomes[:10])}...[/dim]")
                return False

            # Check for pending roll
            if not state.pending_manual_roll:
                display.print_error("No pending dice roll - run 'roll_dice' first")
                return False

            ab_roll = state.pending_manual_roll

            # Validate hit location if required
            if play_outcome.requires_hit_location() and not hit_location:
                display.print_error(f"Outcome '{outcome}' requires hit_location")
                display.console.print(f"[dim]Valid locations: 1B, 2B, SS, 3B, LF, CF, RF, P, C[/dim]")
                return False

            display.print_info(f"Submitting manual outcome: {play_outcome.value}" +
                             (f" to {hit_location}" if hit_location else ""))

            # Call game engine
            result = await game_engine.resolve_manual_play(
                game_id=game_id,
                ab_roll=ab_roll,
                outcome=play_outcome,
                hit_location=hit_location
            )

            # Display result
            display.display_play_result(result)

            # Refresh and display game state
            state = state_manager.get_state(game_id)
            if state:
                display.display_game_state(state)

            return True

        except ValueError as e:
            display.print_error(f"Validation error: {e}")
            return False
        except Exception as e:
            display.print_error(f"Failed to submit manual outcome: {e}")
            logger.exception("Manual outcome error")
            return False

    async def force_wild_pitch(self, game_id: UUID) -> bool:
        """
        Force a wild pitch interrupt play.

        Wild pitch advances all runners one base.

        Args:
            game_id: Game to force wild pitch in

        Returns:
            True if successful, False otherwise
        """
        display.print_info("🎯 Forcing interrupt: WILD PITCH")
        return await self.resolve_play(game_id, PlayOutcome.WILD_PITCH)

    async def force_passed_ball(self, game_id: UUID) -> bool:
        """
        Force a passed ball interrupt play.

        Passed ball advances all runners one base.

        Args:
            game_id: Game to force passed ball in

        Returns:
            True if successful, False otherwise
        """
        display.print_info("🎯 Forcing interrupt: PASSED BALL")
        return await self.resolve_play(game_id, PlayOutcome.PASSED_BALL)

    def roll_jump(self, league: str = 'sba', game_id: Optional[UUID] = None) -> bool:
        """
        Roll jump dice for stolen base testing.

        Jump roll: 1d20 check + conditional 2d6 or 1d20
        - check_roll == 1: Pickoff attempt (uses resolution_roll)
        - check_roll == 2: Balk check (uses resolution_roll)
        - check_roll >= 3: Normal jump (uses 2d6)

        Args:
            league: League ID ('sba' or 'pd')
            game_id: Optional game ID for context

        Returns:
            True if successful, False otherwise
        """
        try:
            from app.core.roll_types import RollType

            # Roll jump dice
            roll = dice_system.roll_jump(league_id=league, game_id=game_id)

            # Display results
            display.print_success("✓ Jump roll completed!")
            display.console.print(f"\n[bold cyan]Jump Roll Results:[/bold cyan]")
            display.console.print(f"  [cyan]Roll ID:[/cyan] {roll.roll_id}")
            display.console.print(f"  [cyan]League:[/cyan] {league.upper()}")
            display.console.print(f"  [cyan]Check Roll (1d20):[/cyan] {roll.check_roll}")

            if roll.is_pickoff_check:
                display.console.print(f"\n[bold red]🎯 PICKOFF ATTEMPT![/bold red]")
                display.console.print(f"  [cyan]Resolution (1d20):[/cyan] {roll.resolution_roll}")
                display.console.print(f"\n[dim]Pitcher attempts to pick off runner[/dim]")
            elif roll.is_balk_check:
                display.console.print(f"\n[bold yellow]⚠️  BALK CHECK![/bold yellow]")
                display.console.print(f"  [cyan]Resolution (1d20):[/cyan] {roll.resolution_roll}")
                display.console.print(f"\n[dim]Pitcher may have committed balk[/dim]")
            else:
                display.console.print(f"  [cyan]Jump Dice (2d6):[/cyan] {roll.jump_total} ({roll.jump_dice_a}+{roll.jump_dice_b})")
                display.console.print(f"\n[green]Normal steal attempt - use jump total for success check[/green]")

            return True

        except Exception as e:
            display.print_error(f"Failed to roll jump: {e}")
            logger.exception("Jump roll error")
            return False

    def test_jump(self, count: int = 10, league: str = 'sba') -> bool:
        """
        Test jump roll distribution.

        Rolls N jump rolls and displays distribution statistics.

        Args:
            count: Number of rolls to test
            league: League ID ('sba' or 'pd')

        Returns:
            True if successful, False otherwise
        """
        try:
            from collections import Counter
            from rich.table import Table

            display.print_info(f"Rolling {count} jump rolls for {league.upper()} league...")

            # Roll multiple times
            pickoff_count = 0
            balk_count = 0
            normal_count = 0
            jump_totals = []

            for _ in range(count):
                roll = dice_system.roll_jump(league_id=league)
                if roll.is_pickoff_check:
                    pickoff_count += 1
                elif roll.is_balk_check:
                    balk_count += 1
                else:
                    normal_count += 1
                    jump_totals.append(roll.jump_total)

            # Display summary
            display.print_success(f"✓ Completed {count} jump rolls")

            # Event distribution table
            event_table = Table(title="Jump Roll Event Distribution", show_header=True, header_style="bold cyan")
            event_table.add_column("Event Type", style="yellow", width=20)
            event_table.add_column("Count", style="green", width=10, justify="right")
            event_table.add_column("Percentage", style="cyan", width=12, justify="right")
            event_table.add_column("Expected", style="dim", width=12, justify="right")

            pickoff_pct = (pickoff_count / count) * 100
            balk_pct = (balk_count / count) * 100
            normal_pct = (normal_count / count) * 100

            event_table.add_row("Pickoff Check", str(pickoff_count), f"{pickoff_pct:.1f}%", "5.0%")
            event_table.add_row("Balk Check", str(balk_count), f"{balk_pct:.1f}%", "5.0%")
            event_table.add_row("Normal Jump", str(normal_count), f"{normal_pct:.1f}%", "90.0%")

            display.console.print(event_table)

            # Jump total distribution (for normal rolls)
            if jump_totals:
                display.console.print(f"\n[bold cyan]Jump Total Distribution (2d6):[/bold cyan]")
                counter = Counter(jump_totals)

                jump_table = Table(show_header=True, header_style="bold cyan")
                jump_table.add_column("Total", style="yellow", width=10, justify="right")
                jump_table.add_column("Count", style="green", width=10, justify="right")
                jump_table.add_column("Percentage", style="cyan", width=12, justify="right")
                jump_table.add_column("Visual", style="white", width=30)

                for total in range(2, 13):  # 2-12 possible with 2d6
                    count_val = counter.get(total, 0)
                    pct = (count_val / len(jump_totals)) * 100 if jump_totals else 0
                    bar = "█" * int(pct / 2)  # Scale bar
                    jump_table.add_row(str(total), str(count_val), f"{pct:.1f}%", bar)

                display.console.print(jump_table)

                # Statistics
                if jump_totals:
                    avg = sum(jump_totals) / len(jump_totals)
                    display.console.print(f"\n[dim]Average jump total: {avg:.2f} (expected: 7.0)[/dim]")

            return True

        except Exception as e:
            display.print_error(f"Failed to test jump rolls: {e}")
            logger.exception("Test jump error")
            return False

    def roll_fielding(self, position: str, league: str = 'sba', game_id: Optional[UUID] = None) -> bool:
        """
        Roll fielding check dice for testing.

        Fielding roll: 1d20 + 3d6 + 1d100
        - d20: Range check
        - 3d6: Error total (3-18)
        - d100: Rare play check

        Args:
            position: Defensive position (P, C, 1B, 2B, 3B, SS, LF, CF, RF)
            league: League ID ('sba' or 'pd')
            game_id: Optional game ID for context

        Returns:
            True if successful, False otherwise
        """
        try:
            # Validate position
            valid_positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']
            position = position.upper()
            if position not in valid_positions:
                display.print_error(f"Invalid position: {position}")
                display.console.print(f"[dim]Valid positions: {', '.join(valid_positions)}[/dim]")
                return False

            # Roll fielding dice
            roll = dice_system.roll_fielding(position=position, league_id=league, game_id=game_id)

            # Display results
            display.print_success(f"✓ Fielding roll completed for {position}!")
            display.console.print(f"\n[bold cyan]Fielding Roll Results:[/bold cyan]")
            display.console.print(f"  [cyan]Roll ID:[/cyan] {roll.roll_id}")
            display.console.print(f"  [cyan]Position:[/cyan] {roll.position}")
            display.console.print(f"  [cyan]League:[/cyan] {league.upper()}")
            display.console.print(f"\n[bold]Dice Components:[/bold]")
            display.console.print(f"  [cyan]Range (1d20):[/cyan] {roll.d20}")
            display.console.print(f"  [cyan]Error Dice (3d6):[/cyan] {roll.error_total} ({roll.d6_one}+{roll.d6_two}+{roll.d6_three})")
            display.console.print(f"  [cyan]Rare Play (1d100):[/cyan] {roll.d100}")

            if roll.is_rare_play:
                if league == 'sba':
                    display.console.print(f"\n[bold yellow]⚠️  RARE PLAY! (d100 = 1)[/bold yellow]")
                else:
                    display.console.print(f"\n[bold yellow]⚠️  RARE PLAY! (error_total = 5)[/bold yellow]")
                display.console.print(f"[dim]Unusual fielding event may occur[/dim]")

            return True

        except ValueError as e:
            display.print_error(f"Validation error: {e}")
            return False
        except Exception as e:
            display.print_error(f"Failed to roll fielding: {e}")
            logger.exception("Fielding roll error")
            return False

    def test_fielding(self, position: str, count: int = 10, league: str = 'sba') -> bool:
        """
        Test fielding roll distribution for a position.

        Rolls N fielding rolls and displays distribution statistics.

        Args:
            position: Defensive position (P, C, 1B, 2B, 3B, SS, LF, CF, RF)
            count: Number of rolls to test
            league: League ID ('sba' or 'pd')

        Returns:
            True if successful, False otherwise
        """
        try:
            from collections import Counter
            from rich.table import Table

            # Validate position
            valid_positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']
            position = position.upper()
            if position not in valid_positions:
                display.print_error(f"Invalid position: {position}")
                display.console.print(f"[dim]Valid positions: {', '.join(valid_positions)}[/dim]")
                return False

            display.print_info(f"Rolling {count} fielding checks for {position} in {league.upper()} league...")

            # Roll multiple times
            d20_values = []
            error_totals = []
            d100_values = []
            rare_play_count = 0

            for _ in range(count):
                roll = dice_system.roll_fielding(position=position, league_id=league)
                d20_values.append(roll.d20)
                error_totals.append(roll.error_total)
                d100_values.append(roll.d100)
                if roll.is_rare_play:
                    rare_play_count += 1

            # Display summary
            display.print_success(f"✓ Completed {count} fielding rolls for {position}")

            # Summary statistics
            display.console.print(f"\n[bold cyan]Summary Statistics:[/bold cyan]")
            display.console.print(f"  [cyan]Rare Plays:[/cyan] {rare_play_count} ({(rare_play_count/count)*100:.1f}%)")
            display.console.print(f"  [cyan]Avg Range (d20):[/cyan] {sum(d20_values)/len(d20_values):.2f} (expected: 10.5)")
            display.console.print(f"  [cyan]Avg Error Total (3d6):[/cyan] {sum(error_totals)/len(error_totals):.2f} (expected: 10.5)")

            # Error total distribution
            display.console.print(f"\n[bold cyan]Error Total Distribution (3d6):[/bold cyan]")
            counter = Counter(error_totals)

            error_table = Table(show_header=True, header_style="bold cyan")
            error_table.add_column("Total", style="yellow", width=10, justify="right")
            error_table.add_column("Count", style="green", width=10, justify="right")
            error_table.add_column("Percentage", style="cyan", width=12, justify="right")
            error_table.add_column("Visual", style="white", width=30)

            for total in range(3, 19):  # 3-18 possible with 3d6
                count_val = counter.get(total, 0)
                pct = (count_val / count) * 100
                bar = "█" * int(pct / 2)  # Scale bar
                error_table.add_row(str(total), str(count_val), f"{pct:.1f}%", bar)

            display.console.print(error_table)

            # Rare play info
            if league == 'sba':
                expected_rare = 1.0  # 1% (d100 = 1)
            else:
                expected_rare = 2.78  # ~2.78% (3d6 = 5)

            display.console.print(f"\n[dim]Expected rare play rate: {expected_rare:.2f}%[/dim]")
            display.console.print(f"[dim]Observed rare play rate: {(rare_play_count/count)*100:.2f}%[/dim]")

            return True

        except ValueError as e:
            display.print_error(f"Validation error: {e}")
            return False
        except Exception as e:
            display.print_error(f"Failed to test fielding rolls: {e}")
            logger.exception("Test fielding error")
            return False


# Singleton instance
game_commands = GameCommands()
