from __future__ import annotations

import uuid
from datetime import datetime
from typing import Any, TypeVar, overload

import httpx
from pydantic import BaseModel, TypeAdapter

from cogames.cli.base import console
from cogames.cli.login import CoGamesAuthenticator

T = TypeVar("T")


class PolicyVersionInfo(BaseModel):
    id: uuid.UUID
    policy_id: uuid.UUID
    created_at: datetime
    policy_created_at: datetime
    user_id: str
    name: str
    version: int


class PoolInfo(BaseModel):
    name: str
    description: str
    config_id: str | None = None


class SeasonInfo(BaseModel):
    name: str
    version: int
    canonical: bool
    summary: str
    entry_pool: str | None = None
    leaderboard_pool: str | None = None
    is_default: bool = False
    pools: list[PoolInfo]


class MatchPlayerInfo(BaseModel):
    policy: PolicyVersionSummary
    num_agents: int
    score: float | None


class MatchResponse(BaseModel):
    id: uuid.UUID
    season_name: str
    pool_name: str
    status: str
    assignments: list[int]
    players: list[MatchPlayerInfo]
    error: str | None
    episode_id: uuid.UUID | None
    episode: dict[str, Any] | None = None
    created_at: datetime


class SeasonVersionInfo(BaseModel):
    version: int
    canonical: bool
    disabled_at: str | None
    created_at: str


class LeaderboardEntry(BaseModel):
    rank: int
    policy: PolicyVersionSummary
    score: float
    matches: int


class PolicyVersionSummary(BaseModel):
    id: uuid.UUID
    name: str | None
    version: int | None


class SubmitToSeasonResponse(BaseModel):
    pools: list[str]


class PoolMembership(BaseModel):
    pool_name: str
    active: bool
    completed: int
    failed: int
    pending: int


class SeasonPolicyEntry(BaseModel):
    policy: PolicyVersionSummary
    pools: list[PoolMembership]
    entered_at: str


class TournamentServerClient:
    def __init__(
        self,
        server_url: str,
        token: str | None = None,
        login_server: str | None = None,
    ):
        self._server_url = server_url
        self._token = token
        self._login_server = login_server
        self._http_client = httpx.Client(base_url=server_url, timeout=30.0)

    def __enter__(self):
        return self

    def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None:
        self.close()

    def close(self):
        self._http_client.close()

    @classmethod
    def from_login(cls, server_url: str, login_server: str) -> TournamentServerClient | None:
        authenticator = CoGamesAuthenticator()
        if not authenticator.has_saved_token(login_server):
            console.print("[red]Error:[/red] Not authenticated.")
            console.print("Please run: [cyan]cogames login[/cyan]")
            return None

        token = authenticator.load_token(login_server)
        if not token:
            console.print(f"[red]Error:[/red] Token not found for {login_server}")
            return None

        return cls(server_url=server_url, token=token, login_server=login_server)

    def _request(
        self,
        method: str,
        path: str,
        response_type: type[T] | None = None,
        timeout: float | None = None,
        **kwargs: Any,
    ) -> T | dict[str, Any]:
        headers = kwargs.pop("headers", {})
        if self._token:
            headers["X-Auth-Token"] = self._token

        if timeout is not None:
            kwargs["timeout"] = timeout

        response = self._http_client.request(method, path, headers=headers, **kwargs)
        response.raise_for_status()

        if response_type is not None:
            return TypeAdapter(response_type).validate_python(response.json())
        return response.json()

    @overload
    def _get(self, path: str, response_type: type[T], **kwargs: Any) -> T: ...
    @overload
    def _get(self, path: str, response_type: None = None, **kwargs: Any) -> dict[str, Any]: ...
    def _get(self, path: str, response_type: type[T] | None = None, **kwargs: Any) -> T | dict[str, Any]:
        return self._request("GET", path, response_type, **kwargs)

    @overload
    def _post(self, path: str, response_type: type[T], **kwargs: Any) -> T: ...
    @overload
    def _post(self, path: str, response_type: None = None, **kwargs: Any) -> dict[str, Any]: ...
    def _post(self, path: str, response_type: type[T] | None = None, **kwargs: Any) -> T | dict[str, Any]:
        return self._request("POST", path, response_type, **kwargs)

    @overload
    def _put(self, path: str, response_type: type[T], **kwargs: Any) -> T: ...
    @overload
    def _put(self, path: str, response_type: None = None, **kwargs: Any) -> dict[str, Any]: ...
    def _put(self, path: str, response_type: type[T] | None = None, **kwargs: Any) -> T | dict[str, Any]:
        return self._request("PUT", path, response_type, **kwargs)

    def get_seasons(self) -> list[SeasonInfo]:
        return self._get("/tournament/seasons", list[SeasonInfo])

    def get_season(self, season_name: str, include_hidden: bool = False) -> SeasonInfo:
        params = {"include_hidden": "true"} if include_hidden else None
        return self._get(f"/tournament/seasons/{season_name}", SeasonInfo, params=params)

    def get_default_season(self) -> SeasonInfo:
        seasons = self.get_seasons()
        for s in seasons:
            if s.is_default:
                return s
        if seasons:
            return seasons[0]
        raise RuntimeError("No seasons available from server")

    def get_season_matches(
        self, season_name: str, include_hidden_seasons: bool = False, policy_version_ids: list[uuid.UUID] | None = None
    ) -> list[MatchResponse]:
        params: dict[str, str | list[str]] = {}

        if include_hidden_seasons:
            params["include_hidden"] = "true"

        if policy_version_ids:
            params["policy_version_ids"] = [str(pvid) for pvid in policy_version_ids]

        return self._get(
            f"/tournament/seasons/{season_name}/matches",
            list[MatchResponse],
            params=params if params else None,
        )

    def get_config(self, config_id: str) -> dict[str, Any]:
        return self._get(f"/tournament/configs/{config_id}")

    def get_season_versions(self, season_name: str) -> list[SeasonVersionInfo]:
        return self._get(f"/tournament/seasons/{season_name}/versions", list[SeasonVersionInfo])

    def get_leaderboard(self, season_name: str, include_hidden_seasons: bool = False) -> list[LeaderboardEntry]:
        return self._get(
            f"/tournament/seasons/{season_name}/leaderboard",
            list[LeaderboardEntry],
            params={"include_hidden": "true"} if include_hidden_seasons else None,
        )

    def get_my_policy_versions(
        self,
        name: str | None = None,
        version: int | None = None,
    ) -> list[PolicyVersionInfo]:
        params: dict[str, Any] = {"mine": "true", "limit": 100}
        if name is not None:
            params["name_exact"] = name
        if version is not None:
            params["version"] = version
        result = self._get("/stats/policy-versions", params=params)
        entries = result.get("entries", [])
        return [PolicyVersionInfo.model_validate(e) for e in entries]

    def lookup_policy_version(
        self,
        name: str,
        version: int | None = None,
    ) -> PolicyVersionInfo | None:
        versions = self.get_my_policy_versions(name=name, version=version)
        return versions[0] if versions else None

    def get_policy_version(self, policy_version_id: uuid.UUID) -> PolicyVersionInfo:
        return self._get(f"/stats/policy-versions/{policy_version_id}", PolicyVersionInfo)

    def submit_to_season(self, season_name: str, policy_version_id: uuid.UUID) -> SubmitToSeasonResponse:
        return self._post(
            f"/tournament/seasons/{season_name}/submissions",
            SubmitToSeasonResponse,
            json={"policy_version_id": str(policy_version_id)},
        )

    def get_season_policies(
        self, season_name: str, mine: bool = False, include_hidden_seasons: bool = False
    ) -> list[SeasonPolicyEntry]:
        params: dict[str, str] = {}
        if mine:
            params["mine"] = "true"
        if include_hidden_seasons:
            params["include_hidden"] = "true"

        return self._get(
            f"/tournament/seasons/{season_name}/policies",
            list[SeasonPolicyEntry],
            params=params if params else None,
        )

    def get_presigned_upload_url(self) -> dict[str, Any]:
        return self._post("/stats/policies/submit/presigned-url", timeout=60.0)

    def complete_policy_upload(self, upload_id: str, name: str, season: str | None = None) -> dict[str, Any]:
        payload: dict[str, Any] = {"upload_id": upload_id, "name": name}
        if season is not None:
            payload["season"] = season
        return self._post(
            "/stats/policies/submit/complete",
            timeout=120.0,
            json=payload,
        )

    def get_policy_memberships(self, policy_version_id: uuid.UUID) -> list[dict[str, Any]]:
        return self._get(f"/tournament/policies/{policy_version_id}/memberships", list[dict[str, Any]])

    def get_my_memberships(self) -> dict[str, list[str]]:
        """Get all season memberships for the user's policy versions.

        Returns a mapping of policy_version_id -> list of season names.
        """
        return self._get("/tournament/my-memberships", dict[str, list[str]])
