""" Lineup Service - High-level lineup operations with player data. Combines database operations with API calls to provide complete lineup entries with player data. Author: Claude Date: 2025-01-10 """ import logging from dataclasses import dataclass from uuid import UUID from app.database.operations import DatabaseOperations from app.models.game_models import LineupPlayerState, TeamLineupState from app.services.sba_api_client import sba_api_client logger = logging.getLogger(f"{__name__}.LineupService") @dataclass class LineupEntryWithPlayer: """Lineup entry with associated player data.""" lineup_id: int player_id: int position: str batting_order: int | None is_starter: bool is_active: bool # Player data from API player_name: str player_image: str # Card image player_headshot: str # Headshot for UI circles class LineupService: """ Service for lineup operations that include player data. Combines database operations with SBA API calls to provide complete lineup entries with player names and images. """ def __init__(self, db_ops: DatabaseOperations | None = None): self.db_ops = db_ops or DatabaseOperations() async def add_sba_player_to_lineup( self, game_id: UUID, team_id: int, player_id: int, position: str, batting_order: int | None = None, is_starter: bool = True, ) -> LineupEntryWithPlayer: """ Add SBA player to lineup with player data. 1. Creates lineup entry in database 2. Fetches player data from SBA API 3. Returns combined result Args: game_id: Game identifier team_id: Team identifier player_id: SBA player ID position: Defensive position batting_order: Batting order (1-9) if applicable is_starter: Whether player is in starting lineup Returns: LineupEntryWithPlayer with lineup data + player info Raises: HTTPError: If SBA API call fails SQLAlchemyError: If database operation fails """ # Step 1: Create lineup entry in database lineup = await self.db_ops.add_sba_lineup_player( game_id=game_id, team_id=team_id, player_id=player_id, position=position, batting_order=batting_order, is_starter=is_starter, ) # Step 2: Fetch player data from SBA API player_name = f"Player #{player_id}" player_image = "" player_headshot = "" try: player = await sba_api_client.get_player(player_id) player_name = player.name player_image = player.get_image_url() player_headshot = player.headshot or "" logger.info(f"Loaded player data for {player_id}: {player_name}") except Exception as e: logger.warning(f"Failed to fetch player data for {player_id}: {e}") # Continue with defaults - lineup entry is still valid # Step 3: Return combined result return LineupEntryWithPlayer( lineup_id=lineup.id, # type: ignore[arg-type] player_id=player_id, position=position, batting_order=batting_order, is_starter=is_starter, is_active=True, player_name=player_name, player_image=player_image, player_headshot=player_headshot, ) async def load_team_lineup_with_player_data( self, game_id: UUID, team_id: int, league_id: str ) -> TeamLineupState | None: """ Load existing team lineup from database with player data. 1. Fetches full lineup (active + bench) from database 2. Fetches player data from SBA API (for SBA league) 3. Returns TeamLineupState with player info populated Args: game_id: Game identifier team_id: Team identifier league_id: League identifier ('sba' or 'pd') Returns: TeamLineupState with player data (including bench), or None if no lineup found """ # Step 1: Get full lineup from database (active + bench) lineup_entries = await self.db_ops.get_full_lineup(game_id, team_id) if not lineup_entries: return None # Step 2: Fetch player data for SBA league player_data = {} if league_id == "sba": player_ids = [p.player_id for p in lineup_entries if p.player_id] # type: ignore[misc] if player_ids: try: player_data = await sba_api_client.get_players_batch(player_ids) logger.info( f"Loaded {len(player_data)}/{len(player_ids)} players for team {team_id}" ) except Exception as e: logger.warning( f"Failed to fetch player data for team {team_id}: {e}" ) # Step 3: Build TeamLineupState with player data players = [] for p in lineup_entries: player_name = None player_image = None player_headshot = None if league_id == "sba" and p.player_id and player_data.get(p.player_id): # type: ignore[arg-type] player = player_data.get(p.player_id) # type: ignore[arg-type] player_name = player.name player_image = player.get_image_url() player_headshot = player.headshot players.append( LineupPlayerState( lineup_id=p.id, # type: ignore[arg-type] card_id=p.card_id if p.card_id else (p.player_id or 0), # type: ignore[arg-type] position=p.position, # type: ignore[arg-type] batting_order=p.batting_order, # type: ignore[arg-type] is_active=p.is_active, # type: ignore[arg-type] is_starter=p.is_starter, # type: ignore[arg-type] player_name=player_name, player_image=player_image, player_headshot=player_headshot, ) ) return TeamLineupState(team_id=team_id, players=players) async def get_sba_player_data(self, player_id: int) -> tuple[str, str]: """ Fetch player data for a single SBA player. Args: player_id: SBA player ID Returns: Tuple of (player_name, player_image) Returns defaults if API call fails """ player_name = f"Player #{player_id}" player_image = "" try: player = await sba_api_client.get_player(player_id) player_name = player.name player_image = player.get_image_url() logger.info(f"Loaded player data for {player_id}: {player_name}") except Exception as e: logger.warning(f"Failed to fetch player data for {player_id}: {e}") return player_name, player_image # Singleton instance lineup_service = LineupService()