"""
Polymorphic player models for league-agnostic game engine.

Supports both SBA and PD leagues with different data complexity:
- SBA: Simple player data (id, name, image, positions)
- PD: Complex player data with scouting ratings for batting/pitching

Author: Claude
Date: 2025-10-28
"""

from abc import ABC, abstractmethod
from typing import Any, Optional

from pydantic import BaseModel, Field

# ==================== Base Player (Abstract) ====================


class BasePlayer(BaseModel, ABC):
    """
    Abstract base class for all player types.

    Provides common interface for league-agnostic game engine.
    """

    # Common fields across all leagues
    id: int = Field(..., description="Player ID (SBA) or Card ID (PD)")
    name: str = Field(..., description="Player display name")
    image: str | None = Field(
        None, description="PRIMARY CARD: Main playing card image URL"
    )
    image2: str | None = Field(
        None, description="ALT CARD: Secondary card for two-way players"
    )
    headshot: str | None = Field(
        None, description="DEFAULT: League-provided headshot fallback"
    )
    vanity_card: str | None = Field(
        None, description="CUSTOM: User-uploaded profile image"
    )

    # Positions (up to 8 possible positions)
    pos_1: str | None = Field(None, description="Primary position")
    pos_2: str | None = Field(None, description="Secondary position")
    pos_3: str | None = Field(None, description="Tertiary position")
    pos_4: str | None = Field(None, description="Fourth position")
    pos_5: str | None = Field(None, description="Fifth position")
    pos_6: str | None = Field(None, description="Sixth position")
    pos_7: str | None = Field(None, description="Seventh position")
    pos_8: str | None = Field(None, description="Eighth position")

    # Active position rating (loaded for current defensive position)
    active_position_rating: Optional["PositionRating"] = Field(
        default=None, description="Defensive rating for current position"
    )

    @abstractmethod
    def get_positions(self) -> list[str]:
        """Get list of positions player can play (e.g., ['2B', 'SS'])."""
        pass

    @abstractmethod
    def get_display_name(self) -> str:
        """Get formatted display name for UI."""
        pass

    @abstractmethod
    def get_image_url(self) -> str:
        """Get card image URL with fallback logic (image -> image2 -> headshot -> empty)."""
        pass

    def get_player_image_url(self) -> str:
        """Get player profile image (prioritizes custom uploads over league defaults)."""
        return self.vanity_card or self.headshot or ""

    class Config:
        """Pydantic configuration."""

        # Allow extra fields for future extensibility
        extra = "allow"


# ==================== SBA Player Model ====================


class SbaPlayer(BasePlayer):
    """
    SBA League player model.

    Simple model with minimal data needed for gameplay.
    Matches API response from: {{baseUrl}}/players/:player_id
    """

    # SBA-specific fields
    wara: float = Field(default=0.0, description="Wins Above Replacement Average")
    team_id: int | None = Field(None, description="Current team ID")
    team_name: str | None = Field(None, description="Current team name")
    season: int | None = Field(None, description="Season number")

    # Additional info
    strat_code: str | None = Field(None, description="Strat-O-Matic code")
    bbref_id: str | None = Field(None, description="Baseball Reference ID")
    injury_rating: str | None = Field(None, description="Injury rating")

    def get_pitching_card_url(self) -> str:
        """Get pitching card image"""
        if self.pos_1 in ["SP", "RP"]:
            return self.image
        if self.image2 and (
            "P" in str(self.pos_2) or "P" in str(self.pos_3) or "P" in str(self.pos_4)
        ):
            return self.image2
        raise ValueError(f"Pitching card not found for {self.get_display_name()}")

    def get_batting_card_url(self) -> str:
        """Get batting card image"""
        if "P" not in self.pos_1:
            return self.image
        if self.image2 and any(
            "P" in str(pos)
            for pos in [
                self.pos_2,
                self.pos_3,
                self.pos_4,
                self.pos_5,
                self.pos_6,
                self.pos_7,
                self.pos_8,
            ]
            if pos
        ):
            return self.image2
        raise ValueError(f"Batting card not found for {self.get_display_name()}")

    def get_positions(self) -> list[str]:
        """Get list of all positions player can play."""
        positions = [
            self.pos_1,
            self.pos_2,
            self.pos_3,
            self.pos_4,
            self.pos_5,
            self.pos_6,
            self.pos_7,
            self.pos_8,
        ]
        return [pos for pos in positions if pos is not None]

    def get_display_name(self) -> str:
        """Get formatted display name."""
        return self.name

    def get_image_url(self) -> str:
        """Get card image URL with fallback logic."""
        return self.image or self.image2 or self.headshot or ""

    @classmethod
    def from_api_response(cls, data: dict[str, Any]) -> "SbaPlayer":
        """
        Create SbaPlayer from API response.

        Args:
            data: API response dict from /players/:player_id

        Returns:
            SbaPlayer instance
        """
        # Extract team info if present
        team_info = data.get("team", {})
        team_id = team_info.get("id") if team_info else None
        team_name = team_info.get("lname") if team_info else None

        return cls(
            id=data["id"],
            name=data["name"],
            image=data.get("image"),
            image2=data.get("image2"),
            wara=data.get("wara", 0.0),
            team_id=team_id,
            team_name=team_name,
            season=data.get("season"),
            pos_1=data.get("pos_1"),
            pos_2=data.get("pos_2"),
            pos_3=data.get("pos_3"),
            pos_4=data.get("pos_4"),
            pos_5=data.get("pos_5"),
            pos_6=data.get("pos_6"),
            pos_7=data.get("pos_7"),
            pos_8=data.get("pos_8"),
            headshot=data.get("headshot"),
            vanity_card=data.get("vanity_card"),
            strat_code=data.get("strat_code"),
            bbref_id=data.get("bbref_id"),
            injury_rating=data.get("injury_rating"),
        )


# ==================== PD Player Model ====================


class PdCardset(BaseModel):
    """PD cardset information."""

    id: int
    name: str
    description: str
    ranked_legal: bool = True


class PdRarity(BaseModel):
    """PD card rarity information."""

    id: int
    value: int
    name: str  # MVP, Starter, Replacement, etc.
    color: str  # Hex color


class PdBattingRating(BaseModel):
    """
    PD batting card ratings for one handedness matchup.

    Contains all probability data for dice roll outcomes.
    """

    vs_hand: str = Field(..., description="Pitcher handedness: L or R")

    # Hit location rates
    pull_rate: float
    center_rate: float
    slap_rate: float

    # Outcome probabilities (sum to ~100.0)
    homerun: float = 0.0
    bp_homerun: float = 0.0
    triple: float = 0.0
    double_three: float = 0.0
    double_two: float = 0.0
    double_pull: float = 0.0
    single_two: float = 0.0
    single_one: float = 0.0
    single_center: float = 0.0
    bp_single: float = 0.0
    hbp: float = 0.0
    walk: float = 0.0
    strikeout: float = 0.0
    lineout: float = 0.0
    popout: float = 0.0
    flyout_a: float = 0.0
    flyout_bq: float = 0.0
    flyout_lf_b: float = 0.0
    flyout_rf_b: float = 0.0
    groundout_a: float = 0.0
    groundout_b: float = 0.0
    groundout_c: float = 0.0

    # Summary stats
    avg: float
    obp: float
    slg: float


class PdPitchingRating(BaseModel):
    """
    PD pitching card ratings for one handedness matchup.

    Contains all probability data for dice roll outcomes.
    """

    vs_hand: str = Field(..., description="Batter handedness: L or R")

    # Outcome probabilities (sum to ~100.0)
    homerun: float = 0.0
    bp_homerun: float = 0.0
    triple: float = 0.0
    double_three: float = 0.0
    double_two: float = 0.0
    double_cf: float = 0.0
    single_two: float = 0.0
    single_one: float = 0.0
    single_center: float = 0.0
    bp_single: float = 0.0
    hbp: float = 0.0
    walk: float = 0.0
    strikeout: float = 0.0
    flyout_lf_b: float = 0.0
    flyout_cf_b: float = 0.0
    flyout_rf_b: float = 0.0
    groundout_a: float = 0.0
    groundout_b: float = 0.0

    # X-check probabilities (defensive plays)
    xcheck_p: float = 0.0
    xcheck_c: float = 0.0
    xcheck_1b: float = 0.0
    xcheck_2b: float = 0.0
    xcheck_3b: float = 0.0
    xcheck_ss: float = 0.0
    xcheck_lf: float = 0.0
    xcheck_cf: float = 0.0
    xcheck_rf: float = 0.0

    # Summary stats
    avg: float
    obp: float
    slg: float


class PdBattingCard(BaseModel):
    """PD batting card information (contains multiple ratings)."""

    steal_low: int
    steal_high: int
    steal_auto: bool
    steal_jump: float
    bunting: str  # A, B, C, D rating
    hit_and_run: str  # A, B, C, D rating
    running: int  # Base running rating
    offense_col: int  # Which offensive column (1 or 2)
    hand: str  # L or R

    # Ratings for vs LHP and vs RHP
    ratings: dict[str, PdBattingRating] = Field(default_factory=dict)


class PdPitchingCard(BaseModel):
    """PD pitching card information (contains multiple ratings)."""

    balk: int
    wild_pitch: int
    hold: int  # Hold runners rating
    starter_rating: int | None = None
    relief_rating: int | None = None
    closer_rating: int | None = None
    batting: str  # Pitcher's batting rating
    offense_col: int  # Which offensive column when batting (1 or 2)
    hand: str  # L or R

    # Ratings for vs LHB and vs RHB
    ratings: dict[str, PdPitchingRating] = Field(default_factory=dict)


class PositionRating(BaseModel):
    """
    Defensive rating for a player at a specific position.

    Used for X-Check play resolution. Ratings come from:
    - PD: API endpoint /api/v2/cardpositions/player/:player_id
    - SBA: Read from physical cards by players
    """

    position: str = Field(..., description="Position code (SS, LF, CF, etc.)")
    innings: int = Field(..., description="Innings played at position")
    range: int = Field(..., ge=1, le=5, description="Defense range (1=best, 5=worst)")
    error: int = Field(..., ge=0, le=88, description="Error rating (0=best, 88=worst)")
    arm: int | None = Field(None, description="Throwing arm rating")
    pb: int | None = Field(None, description="Passed balls (catchers only)")
    overthrow: int | None = Field(None, description="Overthrow risk")

    @classmethod
    def from_api_response(cls, data: dict[str, Any]) -> "PositionRating":
        """
        Create PositionRating from PD API response.

        Args:
            data: Single position dict from /api/v2/cardpositions response

        Returns:
            PositionRating instance
        """
        return cls(
            position=data["position"],
            innings=data["innings"],
            range=data["range"],
            error=data["error"],
            arm=data.get("arm"),
            pb=data.get("pb"),
            overthrow=data.get("overthrow"),
        )


class PdPlayer(BasePlayer):
    """
    PD League player model.

    Complex model with detailed scouting data for simulation.
    Matches API response from: {{baseUrl}}/api/v2/players/:player_id

    Note: PD API returns 'player_id' which is mapped to 'id' field in from_api_response().
    """

    # PD-specific fields
    cost: int = Field(..., description="Card cost/value")

    # Card metadata
    cardset: PdCardset
    set_num: int = Field(..., description="Card set number")
    rarity: PdRarity

    # Team info
    mlbclub: str = Field(..., description="MLB club name")
    franchise: str = Field(..., description="Franchise name")

    # Reference IDs
    strat_code: str | None = Field(None, description="Strat-O-Matic code")
    bbref_id: str | None = Field(None, description="Baseball Reference ID")
    fangr_id: str | None = Field(None, description="FanGraphs ID")

    # Card details
    description: str = Field(..., description="Card description (usually year)")
    quantity: int = Field(default=999, description="Card quantity available")

    # Scouting data (loaded separately if needed)
    batting_card: PdBattingCard | None = Field(
        None, description="Batting card with ratings"
    )
    pitching_card: PdPitchingCard | None = Field(
        None, description="Pitching card with ratings"
    )

    @property
    def player_id(self) -> int:
        """Alias for id (backward compatibility)."""
        return self.id

    def get_positions(self) -> list[str]:
        """Get list of all positions player can play."""
        positions = [
            self.pos_1,
            self.pos_2,
            self.pos_3,
            self.pos_4,
            self.pos_5,
            self.pos_6,
            self.pos_7,
            self.pos_8,
        ]
        return [pos for pos in positions if pos is not None]

    def get_display_name(self) -> str:
        """Get formatted display name with description."""
        return f"{self.name} ({self.description})"

    def get_image_url(self) -> str:
        """Get card image URL with fallback logic."""
        return self.image or self.image2 or self.headshot or ""

    def get_batting_rating(self, vs_hand: str) -> PdBattingRating | None:
        """
        Get batting rating for specific pitcher handedness.

        Args:
            vs_hand: Pitcher handedness ('L' or 'R')

        Returns:
            Batting rating or None if not available
        """
        if not self.batting_card or not self.batting_card.ratings:
            return None
        return self.batting_card.ratings.get(vs_hand)

    def get_pitching_rating(self, vs_hand: str) -> PdPitchingRating | None:
        """
        Get pitching rating for specific batter handedness.

        Args:
            vs_hand: Batter handedness ('L' or 'R')

        Returns:
            Pitching rating or None if not available
        """
        if not self.pitching_card or not self.pitching_card.ratings:
            return None
        return self.pitching_card.ratings.get(vs_hand)

    @classmethod
    def from_api_response(
        cls,
        player_data: dict[str, Any],
        batting_data: dict[str, Any] | None = None,
        pitching_data: dict[str, Any] | None = None,
    ) -> "PdPlayer":
        """
        Create PdPlayer from API responses.

        Args:
            player_data: API response from /api/v2/players/:player_id
            batting_data: Optional API response from /api/v2/battingcardratings/player/:player_id
            pitching_data: Optional API response from /api/v2/pitchingcardratings/player/:player_id

        Returns:
            PdPlayer instance with scouting data if provided
        """
        # Parse batting card if provided
        batting_card = None
        if batting_data and "ratings" in batting_data:
            ratings_dict = {}
            for rating in batting_data["ratings"]:
                vs_hand = rating["vs_hand"]
                ratings_dict[vs_hand] = PdBattingRating(**rating)

            # Get card info from first rating (same for all matchups)
            card_info = batting_data["ratings"][0]["battingcard"]
            batting_card = PdBattingCard(
                steal_low=card_info["steal_low"],
                steal_high=card_info["steal_high"],
                steal_auto=card_info["steal_auto"],
                steal_jump=card_info["steal_jump"],
                bunting=card_info["bunting"],
                hit_and_run=card_info["hit_and_run"],
                running=card_info["running"],
                offense_col=card_info["offense_col"],
                hand=card_info["hand"],
                ratings=ratings_dict,
            )

        # Parse pitching card if provided
        pitching_card = None
        if pitching_data and "ratings" in pitching_data:
            ratings_dict = {}
            for rating in pitching_data["ratings"]:
                vs_hand = rating["vs_hand"]
                ratings_dict[vs_hand] = PdPitchingRating(**rating)

            # Get card info from first rating (same for all matchups)
            card_info = pitching_data["ratings"][0]["pitchingcard"]
            pitching_card = PdPitchingCard(
                balk=card_info["balk"],
                wild_pitch=card_info["wild_pitch"],
                hold=card_info["hold"],
                starter_rating=card_info.get("starter_rating"),
                relief_rating=card_info.get("relief_rating"),
                closer_rating=card_info.get("closer_rating"),
                batting=card_info["batting"],
                offense_col=card_info["offense_col"],
                hand=card_info["hand"],
                ratings=ratings_dict,
            )

        return cls(
            id=player_data.get("player_id", player_data.get("id", 0)),
            name=player_data.get("p_name", player_data.get("name", "")),
            cost=player_data.get("cost", 0),
            image=player_data.get("image", ""),
            image2=player_data.get("image2"),
            cardset=PdCardset(**player_data["cardset"])
            if "cardset" in player_data
            else PdCardset(id=0, name="", description=""),
            set_num=player_data.get("set_num", 0),
            rarity=PdRarity(**player_data["rarity"])
            if "rarity" in player_data
            else PdRarity(id=0, value=0, name="", color=""),
            mlbclub=player_data.get("mlbclub", ""),
            franchise=player_data.get("franchise", ""),
            pos_1=player_data.get("pos_1"),
            pos_2=player_data.get("pos_2"),
            pos_3=player_data.get("pos_3"),
            pos_4=player_data.get("pos_4"),
            pos_5=player_data.get("pos_5"),
            pos_6=player_data.get("pos_6"),
            pos_7=player_data.get("pos_7"),
            pos_8=player_data.get("pos_8"),
            headshot=player_data.get("headshot"),
            vanity_card=player_data.get("vanity_card"),
            strat_code=player_data.get("strat_code"),
            bbref_id=player_data.get("bbref_id"),
            fangr_id=player_data.get("fangr_id"),
            description=player_data["description"],
            quantity=player_data.get("quantity", 999),
            batting_card=batting_card,
            pitching_card=pitching_card,
        )
