"""Chess engine wrapper for the C-based random/scoring engine."""

import contextlib
import json
import logging
import os
from pathlib import Path
import subprocess

import chess

_logger = logging.getLogger(__name__)


class RandomEngine:
    """Thin wrapper around the C engine in C/lichess_random_engine/random_engine.

    Contract:
    - Given a chess.Board, call the C binary with all legal moves encoded as
      UCI (with optional annotations in the future). The binary prints the
      chosen move's UCI on stdout (or JSON when --explain, which we don't need).
    - We do not compute or rank anything in Python; we just pass through moves
      and play exactly what the engine returns.
    - If the binary is missing or returns an invalid/illegal move, raise.
    """

    def __init__(
        self,
        *,
        engine_path: str | None = None,
        max_time_sec: float = 2.0,
        depth: int | None = None,
    ) -> None:
        """Initialize the engine wrapper with path and time settings."""
        self.max_time_sec = max_time_sec
        # depth is accepted for compatibility with existing callers but is unused;
        # the C engine handles its own scoring/selection.
        self.depth = depth
        # Default relative path inside this repo
        default_path = (
            Path(__file__).resolve().parent.parent.parent
            / "C"
            / "lichess_random_engine"
            / "random_engine"
        )
        self.engine_path = Path(engine_path) if engine_path else default_path
        if not self.engine_path.is_file() or not os.access(self.engine_path, os.X_OK):
            msg = (
                f"C engine not found or not executable at '{self.engine_path}'. "
                "Build it first (make -C C/lichess_random_engine)."
            )
            raise FileNotFoundError(msg)

    def _call_engine(self, args: list[str], *, timeout: float) -> str:
        # S603: subprocess call is safe - engine_path is validated in __init__
        # with is_file() and X_OK permission check, args are explicit strings
        try:
            proc = subprocess.run(
                [self.engine_path, *args],
                capture_output=True,
                text=True,
                timeout=timeout,
                check=True,
            )
        except subprocess.CalledProcessError as e:
            stderr = (e.stderr or "").strip()
            msg = f"C engine failed: {stderr or e}"
            raise RuntimeError(msg) from e
        except subprocess.TimeoutExpired as e:
            msg = "C engine timed out"
            raise TimeoutError(msg) from e
        return (proc.stdout or "").strip()

    def choose_move(self, board: chess.Board) -> chess.Move:
        """Choose a move for the given board position."""
        mv, _ = self.choose_move_with_explanation(
            board, time_budget_sec=self.max_time_sec
        )
        return mv

    def choose_move_with_explanation(
        self, board: chess.Board, *, time_budget_sec: float
    ) -> tuple[chess.Move | None, str]:
        """Choose a move and return explanation for the decision."""
        # Collect legal moves and send to engine as plain UCI tokens.
        legal = list(board.legal_moves)
        if not legal:
            return None, "no_legal_moves"

        args = ["--fen", board.fen()] + [m.uci() for m in legal]
        # Optionally pass a seed for reproducibility when desired;
        # keep default behavior otherwise.
        # We deliberately avoid adding annotations here per request.

        output = self._call_engine(args, timeout=max(0.1, time_budget_sec))

        # The engine, without --explain, should print the chosen UCI.
        chosen_uci = output.splitlines()[-1].strip() if output else ""
        try:
            move = chess.Move.from_uci(chosen_uci)
        except ValueError:
            msg = f"Engine returned invalid move: '{chosen_uci}' (output: {output!r})"
            raise RuntimeError(msg) from None

        if move not in board.legal_moves:
            msg = f"Engine returned illegal move for position: {chosen_uci}"
            raise RuntimeError(msg)

        return move, "from_c_engine"

    def _parse_engine_analysis(
        self, out: str, legal_moves: list[chess.Move]
    ) -> tuple[float, str, chess.Move | None, str]:
        """Parse JSON output from engine analysis.

        Returns (candidate_score, candidate_expl, best_move, best_expl).
        """
        cand_score = 0.0
        best_move: chess.Move | None = None
        cand_expl = out
        best_expl = out

        try:
            data = json.loads(out)
            analyze = data.get("analyze") or {}
            cs = analyze.get("candidate_score")
            if isinstance(cs, int | float):
                cand_score = float(cs)
            chosen = data.get("chosen_move")
            if isinstance(chosen, str):
                with contextlib.suppress(Exception):
                    bm = chess.Move.from_uci(chosen)
                    if bm in legal_moves:
                        best_move = bm
            cand_expl = json.dumps(analyze, ensure_ascii=False)
            best_expl = json.dumps(
                {"chosen_index": data.get("chosen_index"), "chosen_move": chosen},
                ensure_ascii=False,
            )
        except (json.JSONDecodeError, KeyError, TypeError):
            _logger.debug("Failed to parse engine JSON output")

        return cand_score, cand_expl, best_move, best_expl

    def evaluate_proposed_move_with_suggestion(
        self,
        board: chess.Board,
        proposed_move_uci: str,
        *,
        time_budget_sec: float,
    ) -> tuple[float, str, chess.Move | None, str]:
        """Ask the C engine to explain and analyze a specific candidate.

        Returns (candidate_score, candidate_expl, best_move, best_expl)
        where explanations are concise JSON snippets from the engine. All logic is
        delegated to the C binary; no scoring is done in Python.
        """
        legal = list(board.legal_moves)
        if not legal:
            return 0.0, "no_legal_moves", None, "no_best_move"

        args = ["--fen", board.fen(), "--explain", "--analyze", proposed_move_uci] + [
            m.uci() for m in legal
        ]
        out = self._call_engine(args, timeout=max(0.1, time_budget_sec))
        return self._parse_engine_analysis(out, legal)
