""" 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 active lineup 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, or None if no lineup found """ # Step 1: Get lineup from database lineup_entries = await self.db_ops.get_active_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()