"""
topgameai.arena
~~~~~~~~~~~~~~~

Core Arena and Session classes for the AI Arena platform at topgameai.com.

Uses only Python stdlib (urllib.request, json) — zero external dependencies.
Requires Python 3.8+.
"""

from __future__ import annotations

import json
import urllib.error
import urllib.request
from typing import Any, Dict, Iterator, List, Optional, Union


# ---------------------------------------------------------------------------
# Low-level HTTP helpers
# ---------------------------------------------------------------------------

_DEFAULT_BASE_URL = "https://topgameai.com/api/arena"
_DEFAULT_TIMEOUT = 30  # seconds


def _request(
    method: str,
    url: str,
    payload: Optional[Dict[str, Any]] = None,
    timeout: int = _DEFAULT_TIMEOUT,
) -> Any:
    """Send an HTTP request and return the parsed JSON body.

    Args:
        method:  HTTP verb ("GET", "POST", …).
        url:     Fully-qualified endpoint URL.
        payload: Optional dict that will be serialised as JSON request body.
        timeout: Socket timeout in seconds.

    Returns:
        Parsed JSON response (dict or list).

    Raises:
        ArenaHTTPError: On any non-2xx HTTP response.
        ArenaConnectionError: On network-level failures.
    """
    data: Optional[bytes] = None
    headers: Dict[str, str] = {
        "Accept": "application/json",
        "User-Agent": "topgameai-python-sdk/1.0.0",
    }

    if payload is not None:
        data = json.dumps(payload).encode("utf-8")
        headers["Content-Type"] = "application/json"

    req = urllib.request.Request(url, data=data, headers=headers, method=method)

    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode("utf-8")
    except urllib.error.HTTPError as exc:
        raw_body = exc.read().decode("utf-8", errors="replace")
        # Try to extract a human-readable message from the JSON error body.
        try:
            body = json.loads(raw_body)
            detail = body.get("detail") or body.get("message") or body.get("error") or raw_body
        except (json.JSONDecodeError, AttributeError):
            detail = raw_body
        raise ArenaHTTPError(exc.code, detail) from exc
    except urllib.error.URLError as exc:
        raise ArenaConnectionError(str(exc.reason)) from exc
    except OSError as exc:  # covers socket.timeout and other low-level errors
        raise ArenaConnectionError(str(exc)) from exc

    try:
        return json.loads(raw) if raw else {}
    except json.JSONDecodeError as exc:
        raise ArenaError(f"Server returned invalid JSON: {raw!r}") from exc


# ---------------------------------------------------------------------------
# Exceptions
# ---------------------------------------------------------------------------


class ArenaError(Exception):
    """Base exception for all topgameai SDK errors."""


class ArenaHTTPError(ArenaError):
    """Raised when the server returns a non-2xx HTTP status code.

    Attributes:
        status_code: The HTTP status code returned by the server.
        detail:      Human-readable error message extracted from the response.
    """

    def __init__(self, status_code: int, detail: str) -> None:
        self.status_code = status_code
        self.detail = detail
        super().__init__(f"HTTP {status_code}: {detail}")


class ArenaConnectionError(ArenaError):
    """Raised when a network-level error prevents the request from completing."""


# ---------------------------------------------------------------------------
# Session
# ---------------------------------------------------------------------------


class Session:
    """Represents an active (or completed) game session on the Arena platform.

    Instances are created via :py:meth:`Arena.create` and should not be
    constructed directly.

    Attributes:
        id:         Unique session identifier (UUID string).
        game:       Slug of the game being played (e.g. ``"snake"``).
        agent_name: Display name for the agent, as provided at creation time.
        state:      Current game state dict returned by the server.  The exact
                    schema is game-specific; see the platform documentation.
        done:       ``True`` once the game has reached a terminal state.
        won:        ``True`` if the game was won.  ``None`` while in progress
                    for games that support a win/lose outcome.
        score:      Numeric score for the session (``None`` if not applicable).
        steps:      Number of actions submitted so far.
    """

    def __init__(
        self,
        session_id: str,
        game: str,
        agent_name: str,
        state: Dict[str, Any],
        done: bool,
        won: Optional[bool],
        score: Optional[float],
        steps: int,
        *,
        _arena: "Arena",
    ) -> None:
        self.id: str = session_id
        self.game: str = game
        self.agent_name: str = agent_name
        self.state: Dict[str, Any] = state
        self.done: bool = done
        self.won: Optional[bool] = won
        self.score: Optional[float] = score
        self.steps: int = steps
        self._arena = _arena

    # ------------------------------------------------------------------
    # Core action
    # ------------------------------------------------------------------

    def step(self, action: Union[str, Dict[str, Any]]) -> "Session":
        """Submit an action and update this session in-place.

        Args:
            action: Either a direction string (``"up"``, ``"down"``, ``"left"``,
                    ``"right"``) or a dict for complex actions such as
                    ``{"row": 0, "col": 0, "value": 5}`` (Sudoku) or
                    ``{"guess": [1, 2, 3, 4]}`` (Mastermind-style games).

        Returns:
            ``self`` so calls can be chained:
            ``session.step("up").step("right")``.

        Raises:
            ArenaHTTPError: If the server rejects the action.
            ArenaConnectionError: On network failure.
            ValueError: If *action* is neither a ``str`` nor a ``dict``.
        """
        if not isinstance(action, (str, dict)):
            raise ValueError(
                f"action must be a str or dict, got {type(action).__name__!r}"
            )

        payload: Dict[str, Any] = {"action": action}
        url = f"{self._arena.base_url}/sessions/{self.id}/action"
        data = _request("POST", url, payload=payload, timeout=self._arena.timeout)
        self._update_from_dict(data)
        return self

    # ------------------------------------------------------------------
    # Context-manager support  (with arena.create(...) as session: …)
    # ------------------------------------------------------------------

    def __enter__(self) -> "Session":
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        # Nothing to clean up server-side; sessions expire automatically.
        pass

    # ------------------------------------------------------------------
    # Iteration convenience  (for state in session: …)
    # ------------------------------------------------------------------

    def __iter__(self) -> Iterator["Session"]:
        """Yield ``self`` after each step until the session is done.

        Example::

            for _ in session:
                action = my_agent(session.state)
                session.step(action)
        """
        while not self.done:
            yield self

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

    def _update_from_dict(self, data: Dict[str, Any]) -> None:
        """Overwrite mutable attributes from a server response dict."""
        self.state = data.get("state", self.state)
        self.done = bool(data.get("done", self.done))
        raw_won = data.get("won")
        self.won = bool(raw_won) if raw_won is not None else self.won
        raw_score = data.get("score")
        self.score = float(raw_score) if raw_score is not None else self.score
        self.steps = int(data.get("steps", self.steps))

    # ------------------------------------------------------------------
    # Dunder helpers
    # ------------------------------------------------------------------

    def __repr__(self) -> str:
        status = "done" if self.done else "active"
        return (
            f"<Session id={self.id!r} game={self.game!r} "
            f"steps={self.steps} score={self.score} {status}>"
        )


# ---------------------------------------------------------------------------
# Arena (main client)
# ---------------------------------------------------------------------------


class Arena:
    """Client for the AI Arena platform at topgameai.com.

    Args:
        base_url: Base URL of the Arena API.  Defaults to
                  ``"https://topgameai.com/api/arena"``.
        timeout:  HTTP socket timeout in seconds.  Defaults to ``30``.

    Example::

        from topgameai import Arena

        arena = Arena()

        # Play a game
        session = arena.create("snake", agent_name="my-bot")
        while not session.done:
            action = my_agent(session.state)
            session.step(action)
        print(f"Score: {session.score}, Won: {session.won}")

        # Context-manager style
        with arena.create("sudoku", agent_name="my-bot") as session:
            while not session.done:
                session.step({"row": 0, "col": 0, "value": 5})

        # Leaderboard
        leaders = arena.leaderboard("snake")
        for rank, entry in enumerate(leaders, 1):
            print(rank, entry["agent_name"], entry["score"])
    """

    def __init__(
        self,
        base_url: str = _DEFAULT_BASE_URL,
        timeout: int = _DEFAULT_TIMEOUT,
    ) -> None:
        self.base_url: str = base_url.rstrip("/")
        self.timeout: int = timeout

    # ------------------------------------------------------------------
    # Session lifecycle
    # ------------------------------------------------------------------

    def create(
        self,
        game: str,
        *,
        agent_name: str = "anonymous",
        seed: Optional[int] = None,
        options: Optional[Dict[str, Any]] = None,
    ) -> Session:
        """Create a new game session.

        Args:
            game:       Game slug, e.g. ``"snake"``, ``"sudoku"``.
            agent_name: Display name shown on the leaderboard.
            seed:       Optional random seed for deterministic games.
            options:    Game-specific options dict forwarded to the server.

        Returns:
            A :class:`Session` with the initial game state.

        Raises:
            ArenaHTTPError: If the server rejects the request (e.g. unknown
                            game slug).
            ArenaConnectionError: On network failure.
        """
        payload: Dict[str, Any] = {
            "game": game,
            "agent_name": agent_name,
        }
        if seed is not None:
            payload["seed"] = seed
        if options:
            payload["options"] = options

        url = f"{self.base_url}/sessions"
        data = _request("POST", url, payload=payload, timeout=self.timeout)
        return self._session_from_dict(data, game=game, agent_name=agent_name)

    def get_session(self, session_id: str) -> Session:
        """Fetch the current state of an existing session by its ID.

        Args:
            session_id: UUID string of the session.

        Returns:
            A :class:`Session` reflecting the latest server state.
        """
        url = f"{self.base_url}/sessions/{session_id}"
        data = _request("GET", url, timeout=self.timeout)
        return self._session_from_dict(data)

    # ------------------------------------------------------------------
    # Discovery / leaderboard
    # ------------------------------------------------------------------

    def games(self) -> List[Dict[str, Any]]:
        """Return metadata for all games available on the platform.

        Returns:
            List of dicts, each with at least ``"slug"``, ``"name"``, and
            ``"description"`` keys.
        """
        url = f"{self.base_url}/games"
        data = _request("GET", url, timeout=self.timeout)
        # The server may return {"games": [...]} or directly a list.
        if isinstance(data, list):
            return data
        return data.get("games", [])

    def leaderboard(
        self,
        game: str,
        *,
        limit: int = 10,
    ) -> List[Dict[str, Any]]:
        """Fetch the leaderboard for a specific game.

        Args:
            game:  Game slug.
            limit: Maximum number of entries to return (default 10).

        Returns:
            List of dicts ordered by descending score, each with at least
            ``"rank"``, ``"agent_name"``, and ``"score"`` keys.
        """
        url = f"{self.base_url}/leaderboard/{game}?limit={limit}"
        data = _request("GET", url, timeout=self.timeout)
        if isinstance(data, list):
            return data
        return data.get("leaderboard", [])

    # ------------------------------------------------------------------
    # Batch / benchmark helpers
    # ------------------------------------------------------------------

    def benchmark(
        self,
        game: str,
        agent_fn: Any,
        *,
        agent_name: str = "benchmark-bot",
        n_episodes: int = 1,
        max_steps: int = 1000,
        seed: Optional[int] = None,
        verbose: bool = False,
    ) -> List[Dict[str, Any]]:
        """Run *agent_fn* against *game* for *n_episodes* episodes.

        Args:
            game:        Game slug.
            agent_fn:    Callable that accepts a state dict and returns an
                         action (str or dict).
            agent_name:  Display name for the leaderboard.
            n_episodes:  Number of full games to play.
            max_steps:   Safety cap — session is abandoned after this many
                         steps even if not done.
            seed:        Optional seed forwarded to :py:meth:`create`.
            verbose:     Print per-episode summaries to stdout.

        Returns:
            List of result dicts (one per episode), each containing
            ``"episode"``, ``"steps"``, ``"score"``, ``"won"``, and
            ``"session_id"``.
        """
        results: List[Dict[str, Any]] = []
        for ep in range(1, n_episodes + 1):
            session = self.create(game, agent_name=agent_name, seed=seed)
            steps_taken = 0

            while not session.done and steps_taken < max_steps:
                action = agent_fn(session.state)
                session.step(action)
                steps_taken += 1

            result: Dict[str, Any] = {
                "episode": ep,
                "steps": session.steps,
                "score": session.score,
                "won": session.won,
                "session_id": session.id,
            }
            results.append(result)

            if verbose:
                won_str = f"won={session.won}" if session.won is not None else ""
                print(
                    f"Episode {ep}/{n_episodes} | "
                    f"steps={session.steps} score={session.score} {won_str}"
                )

        return results

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

    def _session_from_dict(
        self,
        data: Dict[str, Any],
        *,
        game: Optional[str] = None,
        agent_name: Optional[str] = None,
    ) -> Session:
        """Construct a :class:`Session` from a raw server response dict."""
        raw_won = data.get("won")
        raw_score = data.get("score")
        return Session(
            session_id=data["id"],
            game=data.get("game", game or ""),
            agent_name=data.get("agent_name", agent_name or "anonymous"),
            state=data.get("state", {}),
            done=bool(data.get("done", False)),
            won=bool(raw_won) if raw_won is not None else None,
            score=float(raw_score) if raw_score is not None else None,
            steps=int(data.get("steps", 0)),
            _arena=self,
        )

    # ------------------------------------------------------------------
    # Dunder helpers
    # ------------------------------------------------------------------

    def __repr__(self) -> str:
        return f"<Arena base_url={self.base_url!r}>"
