"""
This module contains models for User objects.
"""

from __future__ import annotations

from collections.abc import Mapping
from datetime import datetime
from enum import Enum
from enum import unique
from functools import cached_property
from typing import Literal
from typing import Optional

from pydantic import Field
from pydantic import computed_field

from .base import BaseModel
from .base import cast_float
from .base import cast_int
from .common import Country
from .common import HTMLBody
from .common import TimestampedCount
from .gamemode import Gamemode

__all__ = (
    "ManiaStatsVariantsType",
    "User",
    "UserAccountHistory",
    "UserAccountHistoryType",
    "UserBadge",
    "UserGradeCounts",
    "UserGroup",
    "UserKudosu",
    "UserLevel",
    "UserProfileCover",
    "UserProfileTournamentBanner",
    "UserQueryType",
    "UserRankHighest",
    "UserRankHistoryElement",
    "UserRelation",
    "UserStats",
    "UserStatsRulesets",
    "UserStatsVariant",
)


UserAccountHistoryType = Literal[
    "note",
    "restriction",
    "silence",
    "tournament_ban",
]

ManiaStatsVariantsType = Literal[
    "4k",
    "7k",
]

OLD_QUERY_TYPES = {
    "ID": "id",
    "USERNAME": "string",
}


@unique
class UserQueryType(Enum):
    ID = "id"
    USERNAME = "username"

    @computed_field  # type: ignore
    @property
    def old_api_name(self) -> str:
        return OLD_QUERY_TYPES[self.name]

    @computed_field  # type: ignore
    @property
    def new_api_name(self) -> str:
        return self.value

    @classmethod
    def _missing_(cls, query: object) -> UserQueryType:
        if isinstance(query, str):
            query = query.lower()
        for q in list(UserQueryType):
            if query in (q.old_api_name, q.new_api_name):
                return q
        raise ValueError(f"UserQueryType {query} does not exist.")


class UserLevel(BaseModel):
    current: int
    progress: int

    @classmethod
    def _from_api_v1(cls, data: Mapping[str, object]) -> UserLevel:
        level = cast_float(data["level"])
        current = int(level)
        progress = (level - current) * 100
        return cls.model_validate({"current": current, "progress": int(progress)})


class UserKudosu(BaseModel):
    total: int
    available: int


class UserRankHistoryElement(BaseModel):
    mode: str
    data: list[int]

    @computed_field  # type: ignore
    @cached_property
    def average_gain(self) -> float:
        r"""Average rank gain.

        :return: Average rank gain for a user
        :rtype: float
        """
        if not self.data:
            return 0.0
        return (self.data[0] - self.data[-1]) / len(self.data)


class UserRankHighest(BaseModel):
    rank: int
    updated_at: datetime


class UserProfileCover(BaseModel):
    url: str
    custom_url: Optional[str] = None
    id: Optional[str] = None


class UserProfileTournamentBanner(BaseModel):
    tournament_id: int
    id: Optional[int] = None
    image: Optional[str] = None
    image_2_x: Optional[str] = Field(default=None, alias="image@2x")


class UserBadge(BaseModel):
    awarded_at: datetime
    description: str
    image_url: str
    image_2x_url: str = Field(alias="image@2x_url")
    url: str


class UserAccountHistory(BaseModel):
    id: int
    timestamp: datetime
    length: int
    permanent: bool
    type: UserAccountHistoryType
    description: Optional[str] = None


class UserGradeCounts(BaseModel):
    ssh: int
    """Number of Silver SS ranks achieved."""
    ss: int
    """Number of SS ranks achieved."""
    sh: int
    """Number of Silver S ranks achieved."""
    s: int
    """Number of S ranks achieved."""
    a: int
    """Number of A ranks achieved."""

    @classmethod
    def _from_api_v1(cls, data: Mapping[str, object]) -> UserGradeCounts:
        return cls.model_validate(
            {
                "ss": cast_int(data["count_rank_ss"]),
                "ssh": cast_int(data["count_rank_ssh"]),
                "s": cast_int(data["count_rank_s"]),
                "sh": cast_int(data["count_rank_sh"]),
                "a": cast_int(data["count_rank_a"]),
            },
        )


class UserGroup(BaseModel):
    id: int
    identifier: str
    name: str
    short_name: str
    has_listing: bool
    has_playmodes: bool
    is_probationary: bool
    colour: Optional[str] = None
    playmodes: Optional[list[Gamemode]] = None
    description: Optional[str] = None


class UserStatsVariant(BaseModel):
    mode: Gamemode
    variant: str
    pp: float
    country_rank: Optional[int] = None
    global_rank: Optional[int] = None


class UserStats(BaseModel):
    """Fields are marked as optional since they might be missing from rankings other than performance."""

    ranked_score: Optional[int] = None
    play_count: Optional[int] = None
    grade_counts: Optional[UserGradeCounts] = None
    total_hits: Optional[int] = None
    is_ranked: Optional[bool] = None
    total_score: Optional[int] = None
    level: Optional[UserLevel] = None
    hit_accuracy: Optional[float] = None
    play_time: Optional[int] = None
    pp: Optional[float] = None
    pp_exp: Optional[float] = None
    replays_watched_by_others: Optional[int] = None
    maximum_combo: Optional[int] = None
    global_rank: Optional[int] = None
    global_rank_exp: Optional[int] = None
    country_rank: Optional[int] = None
    user: Optional[User] = None
    count_300: Optional[int] = None
    count_100: Optional[int] = None
    count_50: Optional[int] = None
    count_miss: Optional[int] = None
    variants: Optional[list[UserStatsVariant]] = None

    @computed_field  # type: ignore
    @cached_property
    def pp_per_playtime(self) -> float:
        r"""PP per playtime.

        :return: PP per playtime
        :rtype: float
        """
        if not self.play_time or not self.pp:
            return 0.0
        return self.pp / self.play_time * 3600

    @classmethod
    def _from_api_v1(cls, data: Mapping[str, object]) -> UserStats:
        """Some fields can be None, we want to force them to cast to a value."""
        return cls.model_validate(
            {
                "level": UserLevel._from_api_v1(data),
                "pp": cast_float(data["pp_raw"]),
                "global_rank": cast_int(data["pp_rank"]),
                "country_rank": cast_int(data["pp_country_rank"]),
                "ranked_score": cast_int(data["ranked_score"]),
                "hit_accuracy": cast_float(data["accuracy"]),
                "play_count": cast_int(data["playcount"]),
                "play_time": cast_int(data["total_seconds_played"]),
                "total_score": cast_int(data["total_score"]),
                "total_hits": cast_int(data["count300"])
                + cast_int(data["count100"])
                + cast_int(data["count50"]),
                "is_ranked": cast_float(data["pp_raw"]) != 0,
                "grade_counts": UserGradeCounts._from_api_v1(data),
                "count_300": cast_int(data["count300"]),
                "count_100": cast_int(data["count100"]),
                "count_50": cast_int(data["count50"]),
            },
        )


class UserStatsRulesets(BaseModel):
    osu: Optional[UserStats] = None
    taiko: Optional[UserStats] = None
    fruits: Optional[UserStats] = None
    mania: Optional[UserStats] = None


class UserAchievmement(BaseModel):
    achieved_at: datetime
    achievement_id: int


class UserRelation(BaseModel):
    target_id: int
    relation_type: str
    mutual: bool
    target: Optional[User] = None


class User(BaseModel):
    avatar_url: str
    country_code: str
    id: int
    username: str
    default_group: Optional[str] = None
    is_active: Optional[bool] = None
    is_bot: Optional[bool] = None
    is_online: Optional[bool] = None
    is_supporter: Optional[bool] = None
    pm_friends_only: Optional[bool] = None
    profile_colour: Optional[str] = None
    is_deleted: Optional[bool] = None
    last_visit: Optional[datetime] = None
    discord: Optional[str] = None
    has_supported: Optional[bool] = None
    interests: Optional[str] = None
    join_date: Optional[datetime] = None
    kudosu: Optional[UserKudosu] = None
    location: Optional[str] = None
    max_blocks: Optional[int] = None
    max_friends: Optional[int] = None
    occupation: Optional[str] = None
    playmode: Optional[Gamemode] = None
    playstyle: Optional[list[str]] = None
    post_count: Optional[int] = None
    profile_order: Optional[list[str]] = None
    title: Optional[str] = None
    twitter: Optional[str] = None
    website: Optional[str] = None
    country: Optional[Country] = None
    cover: Optional[UserProfileCover] = None
    is_restricted: Optional[bool] = None
    account_history: Optional[list[UserAccountHistory]] = None
    active_tournament_banners: Optional[list[UserProfileTournamentBanner]] = None
    badges: Optional[list[UserBadge]] = None
    beatmap_playcounts_count: Optional[int] = None
    favourite_beatmapset_count: Optional[int] = None
    follow_user_mapping: Optional[list[int]] = None
    follower_count: Optional[int] = None
    friends: Optional[list[UserRelation]] = None
    graveyard_beatmapset_count: Optional[int] = None
    groups: Optional[list[UserGroup]] = None
    loved_beatmapset_count: Optional[int] = None
    mapping_follower_count: Optional[int] = None
    monthly_playcounts: Optional[list[TimestampedCount]] = None
    page: Optional[HTMLBody] = None
    pending_beatmapset_count: Optional[int] = None
    previous_usernames: Optional[list[str]] = None
    rank_highest: Optional[UserRankHighest] = None
    rank_history: Optional[UserRankHistoryElement] = None
    ranked_beatmapset_count: Optional[int] = None
    replays_watched_counts: Optional[list[TimestampedCount]] = None
    scores_best_count: Optional[int] = None
    scores_first_count: Optional[int] = None
    scores_recent_count: Optional[int] = None
    statistics: Optional[UserStats] = None
    statistics_rulesets: Optional[UserStatsRulesets] = None
    support_level: Optional[int] = None
    unread_pm_count: Optional[int] = None
    user_achievements: Optional[list[UserAchievmement]] = None

    @computed_field  # type: ignore
    @property
    def url(self) -> str:
        return f"https://osu.ppy.sh/users/{self.id}"

    @classmethod
    def _from_api_v1(cls, data: Mapping[str, object]) -> User:
        return cls.model_validate(
            {
                "avatar_url": f"https://s.ppy.sh/a/{data['user_id']}",
                "country_code": data["country"],
                "id": data["user_id"],
                "username": data["username"],
                "join_date": data["join_date"],
                "statistics": UserStats._from_api_v1(data),
            },
        )
