"""Lichess API client and game adapter for playing against online bots."""

from __future__ import annotations

import contextlib
import json
import queue
import threading
from dataclasses import dataclass

import chess
import httpx

LICHESS_BASE = "https://lichess.org"


# ── Data types ────────────────────────────────────────────────────────


@dataclass
class BotInfo:
    username: str
    rating: int
    title: str
    online: bool
    description: str


@dataclass
class CloudEvalResult:
    eval_pawns: float  # Always White's perspective
    is_mate: bool
    mate_in: int | None
    depth: int


class LichessAPIError(Exception):
    """Raised on non-2xx Lichess API responses."""

    def __init__(self, status_code: int, message: str) -> None:
        self.status_code = status_code
        super().__init__(f"Lichess API error {status_code}: {message}")


class LichessGameOver(Exception):
    """Raised when the Lichess game stream indicates the game is over."""

    def __init__(self, status: str, winner: str | None = None) -> None:
        self.status = status
        self.winner = winner
        super().__init__(f"Game over: {status}")


# ── LichessClient — low-level API wrapper ─────────────────────────────


class LichessClient:
    """Thin wrapper around Lichess HTTP API endpoints."""

    def __init__(self, token: str | None = None, timeout: float = 10.0) -> None:
        headers: dict[str, str] = {}
        if token:
            headers["Authorization"] = f"Bearer {token}"
        self._client = httpx.Client(
            base_url=LICHESS_BASE,
            headers=headers,
            timeout=timeout,
        )

    def cloud_eval(self, fen: str, multi_pv: int = 1) -> CloudEvalResult:
        """GET /api/cloud-eval — no auth needed."""
        resp = self._client.get(
            "/api/cloud-eval", params={"fen": fen, "multiPv": multi_pv}
        )
        if resp.status_code == 404:
            raise LichessAPIError(404, "Position not in cloud eval database")
        resp.raise_for_status()
        data = resp.json()
        pv = data["pvs"][0]
        mate = pv.get("mate")
        if mate is not None:
            return CloudEvalResult(
                eval_pawns=10.0 if mate > 0 else -10.0,
                is_mate=True,
                mate_in=mate,
                depth=data.get("depth", 0),
            )
        cp = pv.get("cp", 0)
        return CloudEvalResult(
            eval_pawns=cp / 100.0,
            is_mate=False,
            mate_in=None,
            depth=data.get("depth", 0),
        )

    def get_online_bots(self, max_count: int = 50) -> list[BotInfo]:
        """GET /api/bot/online — NDJSON stream, reads up to max_count."""
        bots: list[BotInfo] = []
        with self._client.stream(
            "GET",
            "/api/bot/online",
            headers={"Accept": "application/x-ndjson"},
            timeout=30.0,
        ) as resp:
            resp.raise_for_status()
            for line in resp.iter_lines():
                if not line.strip():
                    continue
                data = json.loads(line)
                perfs = data.get("perfs", {})
                rating = 1500
                for variant in ("classical", "rapid", "blitz"):
                    if variant in perfs:
                        rating = perfs[variant].get("rating", 1500)
                        break
                bots.append(
                    BotInfo(
                        username=data["username"],
                        rating=rating,
                        title=data.get("title", "BOT"),
                        online=True,
                        description=(
                            data.get("profile", {}).get("bio", "") or ""
                        )[:80],
                    )
                )
                if len(bots) >= max_count:
                    break
        return bots

    def challenge(
        self,
        username: str,
        clock_limit: int = 600,
        clock_increment: int = 0,
        color: str = "random",
    ) -> str:
        """POST /api/challenge/{username} — returns game ID."""
        resp = self._client.post(
            f"/api/challenge/{username}",
            data={
                "rated": "false",
                "clock.limit": str(clock_limit),
                "clock.increment": str(clock_increment),
                "color": color,
                "variant": "standard",
            },
        )
        if resp.status_code == 400:
            error = resp.json().get("error", "Challenge rejected")
            raise LichessAPIError(400, error)
        if resp.status_code == 401:
            raise LichessAPIError(401, "Invalid or missing API token")
        resp.raise_for_status()
        data = resp.json()
        game_id = (
            data.get("game", {}).get("id")
            or data.get("challenge", {}).get("id")
        )
        if not game_id:
            raise LichessAPIError(0, f"No game ID in response: {data}")
        return game_id

    def stream_game(
        self, game_id: str
    ) -> contextlib.AbstractContextManager[httpx.Response]:
        """GET /api/board/game/stream/{gameId} — returns streaming response.

        Caller must use as context manager:
            with client.stream_game(id) as resp:
                for line in resp.iter_lines(): ...
        """
        return self._client.stream(
            "GET",
            f"/api/board/game/stream/{game_id}",
            headers={"Accept": "application/x-ndjson"},
            timeout=None,
        )

    def make_move(self, game_id: str, uci_move: str) -> None:
        """POST /api/board/game/{gameId}/move/{move}"""
        resp = self._client.post(f"/api/board/game/{game_id}/move/{uci_move}")
        if resp.status_code != 200:
            raise LichessAPIError(resp.status_code, resp.text)

    def resign(self, game_id: str) -> None:
        """POST /api/board/game/{gameId}/resign"""
        self._client.post(f"/api/board/game/{game_id}/resign")

    def offer_draw(self, game_id: str) -> None:
        """POST /api/board/game/{gameId}/draw/yes"""
        self._client.post(f"/api/board/game/{game_id}/draw/yes")

    def close(self) -> None:
        self._client.close()


# ── LichessPlayer — adapter duck-typing EnginePlayer ──────────────────


class LichessPlayer:
    """Adapts a Lichess bot game to the EnginePlayer interface.

    GameScreen calls get_move(board) in a @work(thread=True) worker.
    This implementation blocks on a queue fed by a background NDJSON
    stream reader thread — fitting the existing architecture with
    minimal changes to GameScreen.
    """

    def __init__(
        self,
        client: LichessClient,
        game_id: str,
        player_color: chess.Color,
        bot_username: str,
    ) -> None:
        self.client = client
        self.game_id = game_id
        self.player_color = player_color
        self.bot_username = bot_username
        self.elo: int | None = None
        self.difficulty: str = "lichess"

        self._move_queue: queue.Queue[str | LichessGameOver] = queue.Queue()
        self._stream_thread: threading.Thread | None = None
        self._stop_event = threading.Event()
        self._all_moves: list[str] = []

    def start_stream(self) -> None:
        """Begin listening to the game stream in a background thread."""
        self._stream_thread = threading.Thread(
            target=self._stream_loop, daemon=True
        )
        self._stream_thread.start()

    def _stream_loop(self) -> None:
        """Background thread: reads NDJSON events, enqueues bot moves."""
        try:
            with self.client.stream_game(self.game_id) as resp:
                resp.raise_for_status()
                for line in resp.iter_lines():
                    if self._stop_event.is_set():
                        break
                    if not line.strip():
                        continue
                    data = json.loads(line)
                    self._handle_stream_event(data)
        except Exception:
            if not self._stop_event.is_set():
                self._move_queue.put(
                    LichessGameOver("stream_error")
                )

    def _handle_stream_event(self, data: dict) -> None:
        event_type = data.get("type")
        if event_type == "gameFull":
            state = data.get("state", {})
            self._process_moves(state.get("moves", ""))
            status = state.get("status", "started")
            if status not in ("started", "created"):
                self._move_queue.put(
                    LichessGameOver(status, state.get("winner"))
                )
        elif event_type == "gameState":
            self._process_moves(data.get("moves", ""))
            status = data.get("status", "started")
            if status not in ("started", "created"):
                self._move_queue.put(
                    LichessGameOver(status, data.get("winner"))
                )

    def _process_moves(self, moves_str: str) -> None:
        """Parse cumulative moves string, enqueue new bot moves."""
        if not moves_str.strip():
            return
        new_moves = moves_str.strip().split()
        if len(new_moves) <= len(self._all_moves):
            return
        for uci in new_moves[len(self._all_moves) :]:
            self._all_moves.append(uci)
            move_idx = len(self._all_moves) - 1
            # Even index = White's move, odd = Black's
            is_white_move = move_idx % 2 == 0
            is_bot_move = (
                is_white_move and self.player_color == chess.BLACK
            ) or (not is_white_move and self.player_color == chess.WHITE)
            if is_bot_move:
                self._move_queue.put(uci)

    # -- EnginePlayer-compatible interface --

    def send_player_move(self, move: chess.Move) -> None:
        """POST the player's move to Lichess. Called before get_move()."""
        self.client.make_move(self.game_id, move.uci())

    def get_move(self, board: chess.Board) -> chess.Move:
        """Block until the bot makes a move (or game ends).

        Polls with a short timeout so worker cancellation and quit()
        can interrupt promptly rather than hanging for minutes.
        """
        while not self._stop_event.is_set():
            try:
                item = self._move_queue.get(timeout=2)
            except queue.Empty:
                continue
            if isinstance(item, LichessGameOver):
                raise item
            return chess.Move.from_uci(item)
        raise LichessGameOver("stopped", None)

    def get_evaluation(
        self, board: chess.Board
    ) -> tuple[float, bool, int | None]:
        """Cloud eval. Falls back to (0.0, False, None) on error."""
        try:
            result = self.client.cloud_eval(board.fen())
            return result.eval_pawns, result.is_mate, result.mate_in
        except (LichessAPIError, httpx.HTTPError):
            return 0.0, False, None

    def resign_game(self) -> None:
        try:
            self.client.resign(self.game_id)
        except (LichessAPIError, httpx.HTTPError):
            pass

    def offer_draw_game(self) -> None:
        try:
            self.client.offer_draw(self.game_id)
        except (LichessAPIError, httpx.HTTPError):
            pass

    def set_full_strength(self) -> None:
        pass

    def restore_difficulty(self) -> None:
        pass

    def quit(self) -> None:
        """Stop the stream thread and close resources."""
        self._stop_event.set()
        # Wait briefly for the stream thread to notice the stop event
        # and exit cleanly before closing the shared client.
        if self._stream_thread and self._stream_thread.is_alive():
            self._stream_thread.join(timeout=3)
        try:
            self.client.close()
        except Exception:
            pass
