"""CurseForge API v1 client with error handling and retry logic."""

from __future__ import annotations

import time
from typing import Any, Dict, Optional

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry


# ---------------------------------------------------------------------------
# Custom exceptions
# ---------------------------------------------------------------------------


class CurseForgeError(Exception):
    """Base exception for all CurseForge API errors."""


class CurseForgeAuthError(CurseForgeError):
    """API key is missing, invalid, or lacks required permissions."""


class CurseForgeNotFoundError(CurseForgeError):
    """The requested modpack or file does not exist."""


class CurseForgeRateLimitError(CurseForgeError):
    """The API rate limit has been exceeded (429)."""


# ---------------------------------------------------------------------------
# Client
# ---------------------------------------------------------------------------


class CurseForgeClient:
    """Thin wrapper around the CurseForge REST API v1.

    Handles authentication, retries on transient errors, and translates
    HTTP status codes into typed exceptions so callers never see raw
    requests exceptions.
    """

    BASE_URL = "https://api.curseforge.com"
    _MINECRAFT_GAME_ID = 432  # used for API key validation

    def __init__(self, api_key: str, timeout: int = 15) -> None:
        self.api_key = api_key
        self.timeout = timeout

        self._session = requests.Session()
        self._session.headers.update(
            {
                "x-api-key": api_key,
                "Accept": "application/json",
                "User-Agent": "ModpackVersionChecker/1.0 (Firefrost Gaming)",
            }
        )

        # Retry on connection errors and server-side 5xx — never on 4xx
        retry = Retry(
            total=3,
            backoff_factor=1.0,
            status_forcelist=[500, 502, 503, 504],
            allowed_methods=["GET"],
            raise_on_status=False,
        )
        self._session.mount("https://", HTTPAdapter(max_retries=retry))

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
        """Perform a GET request and return parsed JSON.

        Raises:
            CurseForgeAuthError: on 401 / 403
            CurseForgeNotFoundError: on 404
            CurseForgeRateLimitError: on 429
            CurseForgeError: on connection failure, timeout, or other HTTP error
        """
        url = f"{self.BASE_URL}{path}"
        try:
            response = self._session.get(url, params=params, timeout=self.timeout)
        except requests.ConnectionError as exc:
            raise CurseForgeError(
                f"Could not connect to CurseForge API. Check your internet connection."
            ) from exc
        except requests.Timeout:
            raise CurseForgeError(
                f"CurseForge API timed out after {self.timeout}s. Try again later."
            )

        if response.status_code == 401:
            raise CurseForgeAuthError(
                "Invalid API key. Verify your key at https://console.curseforge.com"
            )
        if response.status_code == 403:
            raise CurseForgeAuthError(
                "API key lacks permission for this resource. "
                "Ensure your key has 'Mods' read access."
            )
        if response.status_code == 404:
            raise CurseForgeNotFoundError(f"Resource not found: {path}")
        if response.status_code == 429:
            raise CurseForgeRateLimitError(
                "CurseForge rate limit exceeded (100 req/min). "
                "Wait a moment and try again."
            )

        try:
            response.raise_for_status()
        except requests.HTTPError as exc:
            raise CurseForgeError(
                f"Unexpected API error ({response.status_code}): {response.text[:200]}"
            ) from exc

        return response.json()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def validate_api_key(self) -> bool:
        """Return True if the current API key is valid."""
        try:
            self._get(f"/v1/games/{self._MINECRAFT_GAME_ID}")
            return True
        except CurseForgeAuthError:
            return False

    def get_mod(self, mod_id: int) -> Dict[str, Any]:
        """Fetch full modpack metadata by CurseForge project ID.

        Raises:
            CurseForgeNotFoundError: if the project ID does not exist.
        """
        data = self._get(f"/v1/mods/{mod_id}")
        return data["data"]

    def get_mod_name(self, mod_id: int) -> str:
        """Return the display name for a modpack."""
        mod = self.get_mod(mod_id)
        return mod.get("name", f"Modpack {mod_id}")

    def get_latest_file(self, mod_id: int) -> Optional[Dict[str, Any]]:
        """Return the most recent file object for a modpack.

        Prefers the ``latestFiles`` field on the mod object (single request).
        Falls back to querying the files endpoint if that is empty.

        Returns None if no files are found.
        """
        mod = self.get_mod(mod_id)

        # Fast path: latestFiles is populated by CurseForge automatically
        latest_files = mod.get("latestFiles", [])
        if latest_files:
            # Sort by fileDate descending to get the newest release
            latest_files_sorted = sorted(
                latest_files,
                key=lambda f: f.get("fileDate", ""),
                reverse=True,
            )
            return latest_files_sorted[0]

        # Slow path: query the files endpoint
        data = self._get(
            f"/v1/mods/{mod_id}/files",
            params={"pageSize": 1, "sortOrder": "desc"},
        )
        files = data.get("data", [])
        return files[0] if files else None

    def extract_version(self, file_obj: Dict[str, Any]) -> str:
        """Extract a human-readable version string from a CurseForge file object.

        Priority: displayName → fileName → file ID fallback.
        """
        display_name = (file_obj.get("displayName") or "").strip()
        if display_name:
            return display_name

        file_name = (file_obj.get("fileName") or "").strip()
        if file_name:
            return file_name

        return f"File ID {file_obj.get('id', 'unknown')}"
