from abc import ABC
from typing import Literal, Optional

from pydantic import AliasChoices, BaseModel, Field, computed_field, field_validator

from pycricinfo.models.source.api.athelete import Athlete
from pycricinfo.models.source.api.common import CCBaseModel, RefMixin
from pycricinfo.models.source.api.statistics import (
    PlayerStatisticsCategoryContainer,
    StatsCategoryContainer,
    TeamStatisticsCategory,
)


class BaseInningsDetails(CCBaseModel, ABC):
    """
    Abstract base class for commong fields between a Team's Innings and individual Player innings
    """

    period: int = Field(description="Which period of the match this Innings is")
    statistics: StatsCategoryContainer = Field(
        description="The stats category container for this innings. Expected to be overridden by Player or Team stats"
    )

    def find(self, name: str) -> Optional[int | str | float]:
        """
        Find a matching stat within the attached category

        Parameters
        ----------
        name : str
            The name of the stat to find. Just a name if searching within the primary base container, otherwise (if in
            a Player innings) starting with "batting." or "bowling." to search in those extensions

        Returns
        -------
        Optional[int | str | float]
            The value of the matching stat, if found, otherwise None
        """
        return self.statistics.find(name)


class PlayerInningsDetails(BaseInningsDetails):
    media_id: int = Field(description="A media ID for this innings")
    statistics: PlayerStatisticsCategoryContainer

    @computed_field
    @property
    def batted(self) -> bool:
        return self.find("batted")

    @computed_field
    @property
    def absent(self) -> bool:
        return self.find("absentDescription") == "absent"

    @computed_field
    @property
    def retired_hurt(self) -> bool:
        return (not self.absent) and self.find("retiredDescription") == "hurt"

    @computed_field
    @property
    def is_batting_innings(self) -> bool:
        did_bat = bool(self.batted) and bool(int(self.batted))
        return did_bat or self.absent

    @computed_field
    @property
    def bowled(self) -> bool:
        return self.find("bowled")


class PartnershipBatter(CCBaseModel):
    athlete: Athlete
    balls: str | int
    runs: str | int


class InningsState(BaseModel):
    overs: str | float
    runs: str | int
    wickets: str | int


class PartnershipFallOfWicketCommon(RefMixin, CCBaseModel, ABC):
    """
    Abstract base class for commong fields between a Partnership and a Fall of Wicket
    """

    wicket_number: int = Field(description="Which number wicket in the innings this was")
    fow_type: Literal["out", "end of innings", "not out", "retired not out"] = Field(
        description="Which of the fixed values for a wicket falling this represents"
    )
    runs: int = Field(description="The number of runs scored at this current point")

    @field_validator("fow_type", mode="before")
    @classmethod
    def strip_fow_type(cls, v):
        """
        In source data, fow_type has trailing whitespace on "retired not out", so trim it before validation.
        """
        if isinstance(v, str):
            return v.strip()
        return v


class Partnership(PartnershipFallOfWicketCommon):
    wicket_name: str
    overs: float
    run_rate: float
    start: InningsState
    end: InningsState
    batsmen: list[PartnershipBatter]


class FallOfWicket(PartnershipFallOfWicketCommon):
    wicket_over: float
    runs_scored: int
    balls_faced: int
    athlete: Athlete


class TeamInningsDetails(BaseInningsDetails):
    wickets: int
    runs: int
    overs: float
    is_batting: bool
    fours: Optional[int] = None
    sixes: Optional[int] = None
    description: str
    target: int
    follow_on: int
    statistics: Optional[TeamStatisticsCategory]
    partnerships: Optional[list[Partnership]] = None
    fall_of_wicket: Optional[list[FallOfWicket]] = Field(
        default=None, validation_alias=AliasChoices("fall_of_wicket", "fow")
    )

    @computed_field
    @property
    def declared(self) -> bool:
        return self.description.lower() == "declared"
