"""Steam Web API client for fetching games and achievement data."""

from __future__ import annotations

from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
import logging
import threading
import time
from typing import TYPE_CHECKING, Any

import requests

if TYPE_CHECKING:
    from collections.abc import Callable

logger = logging.getLogger(__name__)

STEAM_API_BASE = "https://api.steampowered.com"
MAX_WORKERS = 20


@dataclass
class AchievementInfo:
    """Single achievement state."""

    api_name: str
    display_name: str
    achieved: bool
    unlock_time: int


@dataclass
class GameInfo:
    """Info about an owned Steam game."""

    app_id: int
    name: str
    total_achievements: int
    unlocked_achievements: int
    playtime_minutes: int
    achievements: list[AchievementInfo] = field(default_factory=list)
    completionist_hours: float = -1
    comp_100_count: int = 0
    count_comp: int = 0

    @property
    def completion_pct(self) -> float:
        """Achievement completion percentage."""
        if self.total_achievements == 0:
            return 100.0
        return (self.unlocked_achievements / self.total_achievements) * 100.0

    @property
    def is_complete(self) -> bool:
        """True if all achievements are unlocked."""
        return (
            self.total_achievements > 0
            and self.unlocked_achievements >= self.total_achievements
        )

    def to_snapshot(self) -> dict[str, Any]:
        """Serialize to JSON-safe dict."""
        return {
            "app_id": self.app_id,
            "name": self.name,
            "total_achievements": self.total_achievements,
            "unlocked_achievements": self.unlocked_achievements,
            "playtime_minutes": self.playtime_minutes,
            "completionist_hours": self.completionist_hours,
            "comp_100_count": self.comp_100_count,
            "count_comp": self.count_comp,
            "achievements": [
                {
                    "api_name": a.api_name,
                    "display_name": a.display_name,
                    "achieved": a.achieved,
                    "unlock_time": a.unlock_time,
                }
                for a in self.achievements
            ],
        }

    @classmethod
    def from_snapshot(cls, data: dict[str, Any]) -> GameInfo:
        """Deserialize from a cached snapshot dict."""
        achievements = [
            AchievementInfo(
                api_name=a["api_name"],
                display_name=a.get("display_name", a["api_name"]),
                achieved=a["achieved"],
                unlock_time=a.get("unlock_time", 0),
            )
            for a in data.get("achievements", [])
        ]
        return cls(
            app_id=data["app_id"],
            name=data["name"],
            total_achievements=data["total_achievements"],
            unlocked_achievements=data["unlocked_achievements"],
            playtime_minutes=data.get("playtime_minutes", 0),
            completionist_hours=data.get("completionist_hours", -1),
            comp_100_count=data.get("comp_100_count", 0),
            count_comp=data.get("count_comp", 0),
            achievements=achievements,
        )


class SteamAPIError(Exception):
    """Raised when the Steam API returns an error."""


class SteamAPIClient:
    """Client for interacting with the Steam Web API."""

    def __init__(self, api_key: str, steam_id: str) -> None:
        """Initialize the Steam API client.

        Args:
            api_key: Steam Web API key.
            steam_id: Steam64 ID of the user.
        """
        self.api_key = api_key
        self.steam_id = steam_id
        self.session = requests.Session()
        adapter = requests.adapters.HTTPAdapter(
            pool_maxsize=MAX_WORKERS,
            pool_connections=MAX_WORKERS,
        )
        self.session.mount("https://", adapter)
        self.session.headers["Accept"] = "application/json"
        self._rate_lock = threading.Lock()
        self._request_times: list[float] = []
        self._max_rps = 18

    def _rate_limit(self) -> None:
        """Enforce rate limit across threads."""
        while True:
            with self._rate_lock:
                now = time.time()
                self._request_times = [t for t in self._request_times if now - t < 1.0]
                if len(self._request_times) < self._max_rps:
                    self._request_times.append(now)
                    return
            time.sleep(0.06)

    def _get(self, url: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
        """Rate-limited GET request."""
        self._rate_limit()
        if params is None:
            params = {}
        params["key"] = self.api_key
        try:
            resp = self.session.get(url, params=params, timeout=30)
            resp.raise_for_status()
            result: dict[str, Any] = resp.json()
        except requests.RequestException as e:
            msg = f"Steam API request failed: {e}"
            raise SteamAPIError(msg) from e
        else:
            return result

    def get_owned_games(self) -> list[dict[str, Any]]:
        """Fetch all games owned by the user."""
        url = f"{STEAM_API_BASE}/IPlayerService/GetOwnedGames/v1/"
        data = self._get(
            url,
            {
                "steamid": self.steam_id,
                "include_appinfo": "true",
                "include_played_free_games": "true",
                "format": "json",
            },
        )
        games: list[dict[str, Any]] = data.get("response", {}).get("games", [])
        logger.info("Found %d owned games.", len(games))
        return games

    def get_achievement_details(self, app_id: int) -> list[AchievementInfo]:
        """Fetch per-achievement detail for a game."""
        url = f"{STEAM_API_BASE}/ISteamUserStats/GetPlayerAchievements/v1/"
        try:
            data = self._get(
                url,
                {
                    "steamid": self.steam_id,
                    "appid": str(app_id),
                    "l": "english",
                    "format": "json",
                },
            )
        except SteamAPIError:
            return []

        stats = data.get("playerstats", {})
        if not stats.get("success", False):
            return []

        raw: list[dict[str, Any]] = stats.get("achievements", [])
        return [
            AchievementInfo(
                api_name=a.get("apiname", ""),
                display_name=a.get("name", a.get("apiname", "")),
                achieved=bool(a.get("achieved", 0)),
                unlock_time=a.get("unlocktime", 0),
            )
            for a in raw
        ]

    def _fetch_one_game(
        self, game_dict: dict[str, Any], skip: set[int]
    ) -> GameInfo | None:
        """Fetch achievement data for one game. Thread-safe."""
        app_id = game_dict["appid"]
        if app_id in skip:
            return None

        achievements = self.get_achievement_details(app_id)
        if not achievements:
            return None

        name = game_dict.get("name", f"Unknown ({app_id})")
        total = len(achievements)
        unlocked = sum(1 for a in achievements if a.achieved)

        return GameInfo(
            app_id=app_id,
            name=name,
            total_achievements=total,
            unlocked_achievements=unlocked,
            playtime_minutes=game_dict.get("playtime_forever", 0),
            achievements=achievements,
        )

    def build_game_list(
        self,
        skip_app_ids: list[int] | None = None,
        progress_callback: Callable[[int, int], None] | None = None,
    ) -> list[GameInfo]:
        """Build full game list with achievement data (parallel)."""
        skip = set(skip_app_ids or [])
        owned = self.get_owned_games()
        games: list[GameInfo] = []
        done_count = 0
        total = len(owned)
        lock = threading.Lock()

        with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
            futures = {pool.submit(self._fetch_one_game, g, skip): g for g in owned}
            for future in as_completed(futures):
                try:
                    result = future.result()
                except (
                    KeyError,
                    TypeError,
                    ValueError,
                    SteamAPIError,
                    requests.RequestException,
                ):
                    result = None
                with lock:
                    done_count += 1
                    if progress_callback:
                        progress_callback(done_count, total)
                if result is not None:
                    games.append(result)

        games.sort(key=lambda g: g.name.lower())
        return games

    def refresh_single_game(
        self, app_id: int, name: str, playtime: int = 0
    ) -> GameInfo | None:
        """Re-fetch achievement data for one game."""
        achievements = self.get_achievement_details(app_id)
        if not achievements:
            return None
        total = len(achievements)
        unlocked = sum(1 for a in achievements if a.achieved)
        return GameInfo(
            app_id=app_id,
            name=name,
            total_achievements=total,
            unlocked_achievements=unlocked,
            playtime_minutes=playtime,
            achievements=achievements,
        )
