"""Chess game engine - Stateless chessboard with no rule validation.

Features:
- Stateless FEN-based board representation
- No move validation - agents can do anything
- Support for place, remove, move, clear operations
- FEN import/export for interop with chess tools
- Compact token format: c.<base64-fen>
"""

from __future__ import annotations

import base64
from dataclasses import dataclass
from typing import Any

import chess

from purefn.core.codec import canonical_json, commit_hash
from purefn.core.registry import register_game
from purefn.core.result import PureResult, error, success


@dataclass
class BoardResult:
    """Result from board operations."""

    board: str  # New board token
    fen: str  # Current FEN string
    ascii: str  # ASCII representation
    piece: str | None = None  # Piece involved (if applicable)


def encode_board(board: chess.Board) -> str:
    """Encode board to token.

    Format: c.<base64-fen>
    """
    fen = board.fen()
    b64 = base64.urlsafe_b64encode(fen.encode("utf-8")).decode("ascii").rstrip("=")
    return f"c.{b64}"


def decode_board(token: str) -> chess.Board:
    """Decode token to board."""
    if not token.startswith("c."):
        raise ValueError("Invalid token: must start with 'c.'")

    b64_data = token[2:]
    pad = "=" * (-len(b64_data) % 4)
    fen = base64.urlsafe_b64decode(b64_data + pad).decode("utf-8")

    return chess.Board(fen=fen)


def new_board(variant: str = "standard") -> BoardResult:
    """Create a new board.

    Args:
        variant: "standard" (starting position) or "empty" (empty board)

    Returns:
        BoardResult with new board token
    """
    if variant == "empty":
        board = chess.Board.empty()
    else:
        board = chess.Board()

    token = encode_board(board)
    return BoardResult(board=token, fen=board.fen(), ascii=str(board))


def place_piece(board_token: str, square: str, piece: str) -> BoardResult:
    """Place a piece on a square (overwrites existing piece).

    Args:
        board_token: Current board token
        square: Square in algebraic notation (e.g., "e4")
        piece: Piece in format "wP", "bK", etc. (color + piece type)
               Valid pieces: P/N/B/R/Q/K (pawn/knight/bishop/rook/queen/king)
               Valid colors: w/b (white/black)

    Returns:
        BoardResult with new board token
    """
    board = decode_board(board_token)

    # Parse square
    try:
        sq = chess.parse_square(square)
    except ValueError as e:
        raise ValueError(f"Invalid square '{square}': {e}")

    # Parse piece
    if len(piece) != 2:
        raise ValueError(f"Invalid piece format '{piece}': must be 2 chars (e.g., 'wP')")

    color_char, piece_char = piece[0].lower(), piece[1].upper()

    if color_char not in "wb":
        raise ValueError(f"Invalid color '{color_char}': must be 'w' or 'b'")
    if piece_char not in "PNBRQK":
        raise ValueError(f"Invalid piece type '{piece_char}': must be P/N/B/R/Q/K")

    color = chess.WHITE if color_char == "w" else chess.BLACK
    piece_obj = chess.Piece.from_symbol(piece_char if color == chess.WHITE else piece_char.lower())

    # Place piece (overwrites if already there)
    board.set_piece_at(sq, piece_obj)

    token = encode_board(board)
    return BoardResult(board=token, fen=board.fen(), ascii=str(board), piece=piece)


def remove_piece(board_token: str, square: str) -> BoardResult:
    """Remove a piece from a square.

    Args:
        board_token: Current board token
        square: Square in algebraic notation

    Returns:
        BoardResult with new board token
    """
    board = decode_board(board_token)

    try:
        sq = chess.parse_square(square)
    except ValueError as e:
        raise ValueError(f"Invalid square '{square}': {e}")

    piece_obj = board.piece_at(sq)
    piece_str = str(piece_obj) if piece_obj else None

    board.remove_piece_at(sq)

    token = encode_board(board)
    return BoardResult(board=token, fen=board.fen(), ascii=str(board), piece=piece_str)


def move_piece(board_token: str, from_square: str, to_square: str) -> BoardResult:
    """Move a piece from one square to another (no validation).

    Args:
        board_token: Current board token
        from_square: Source square
        to_square: Destination square

    Returns:
        BoardResult with new board token
    """
    board = decode_board(board_token)

    try:
        from_sq = chess.parse_square(from_square)
        to_sq = chess.parse_square(to_square)
    except ValueError as e:
        raise ValueError(f"Invalid square: {e}")

    piece_obj = board.piece_at(from_sq)
    if piece_obj is None:
        raise ValueError(f"No piece at {from_square}")

    # Move piece (no validation)
    board.remove_piece_at(from_sq)
    board.set_piece_at(to_sq, piece_obj)

    # Toggle turn to the other side
    board.turn = not board.turn

    token = encode_board(board)
    return BoardResult(board=token, fen=board.fen(), ascii=str(board), piece=str(piece_obj))


def clear_board(board_token: str) -> BoardResult:
    """Clear all pieces from the board.

    Args:
        board_token: Current board token

    Returns:
        BoardResult with empty board
    """
    board = chess.Board.empty()
    token = encode_board(board)
    return BoardResult(board=token, fen=board.fen(), ascii=str(board))


def import_fen(fen: str) -> BoardResult:
    """Import a board from FEN string.

    Args:
        fen: FEN string

    Returns:
        BoardResult with new board token
    """
    try:
        board = chess.Board(fen=fen)
    except ValueError as e:
        raise ValueError(f"Invalid FEN: {e}")

    token = encode_board(board)
    return BoardResult(board=token, fen=board.fen(), ascii=str(board))


def export_fen(board_token: str) -> str:
    """Export board to FEN string.

    Args:
        board_token: Current board token

    Returns:
        FEN string
    """
    board = decode_board(board_token)
    return board.fen()


def view_board(board_token: str) -> dict[str, Any]:
    """Get views of the board.

    Returns:
        Dictionary with fen, ascii, and piece_map views
    """
    board = decode_board(board_token)

    # Create piece map (square -> piece)
    piece_map = {}
    for sq in chess.SQUARES:
        piece = board.piece_at(sq)
        if piece:
            square_name = chess.square_name(sq)
            color = "w" if piece.color == chess.WHITE else "b"
            piece_map[square_name] = f"{color}{piece.symbol().upper()}"

    return {
        "fen": board.fen(),
        "ascii": str(board),
        "pieces": piece_map,
        "turn": "white" if board.turn == chess.WHITE else "black",
    }


class ChessEngine:
    """Envelope-based adapter for the chess engine.

    Implements the GameEngine protocol for use with the global registry and
    the `purefn run` CLI entrypoint.
    """

    def dispatch(self, op: str, global_token: str, args: dict[str, Any]) -> PureResult:
        """Dispatch an operation based on envelope fields."""
        from collections.abc import Callable

        handlers: dict[str, Callable[[str, dict[str, Any]], PureResult]] = {
            "new": self._dispatch_new,
            "place": self._dispatch_place,
            "remove": self._dispatch_remove,
            "move": self._dispatch_move,
            "clear": self._dispatch_clear,
            "import": self._dispatch_import,
            "export": self._dispatch_export,
            "view": self._dispatch_view,
        }
        handler = handlers.get(op)
        if handler is None:
            return error(f"Unknown chess op: {op}")

        try:
            return handler(global_token, args)
        except Exception as e:
            return error(str(e))

    def _dispatch_new(self, _global: str, args: dict[str, Any]) -> PureResult:
        variant = args.get("variant", "standard")
        result = new_board(variant)
        views = {
            "fen": result.fen,
            "ascii": result.ascii,
        }
        events = [{"type": "board_created", "variant": variant}]
        commit = commit_hash(canonical_json({"board": result.board}))
        return success(global_token=result.board, commit=commit, views=views, events=events)

    def _dispatch_place(self, global_token: str, args: dict[str, Any]) -> PureResult:
        if not global_token:
            return error("Missing 'global' board token for place")
        square = args["square"]
        piece = args["piece"]
        result = place_piece(global_token, square, piece)
        views = {
            "fen": result.fen,
            "ascii": result.ascii,
            "square": square,
            "piece": piece,
        }
        events = [{"type": "piece_placed", "square": square, "piece": piece}]
        commit = commit_hash(canonical_json({"board": result.board}))
        return success(global_token=result.board, commit=commit, views=views, events=events)

    def _dispatch_remove(self, global_token: str, args: dict[str, Any]) -> PureResult:
        if not global_token:
            return error("Missing 'global' board token for remove")
        square = args["square"]
        result = remove_piece(global_token, square)
        views = {
            "fen": result.fen,
            "ascii": result.ascii,
            "square": square,
            "removed": result.piece,
        }
        events = [{"type": "piece_removed", "square": square, "piece": result.piece}]
        commit = commit_hash(canonical_json({"board": result.board}))
        return success(global_token=result.board, commit=commit, views=views, events=events)

    def _dispatch_move(self, global_token: str, args: dict[str, Any]) -> PureResult:
        if not global_token:
            return error("Missing 'global' board token for move")
        from_square = args["from"]
        to_square = args["to"]
        result = move_piece(global_token, from_square, to_square)
        views = {
            "fen": result.fen,
            "ascii": result.ascii,
            "from": from_square,
            "to": to_square,
            "piece": result.piece,
        }
        events = [
            {"type": "piece_moved", "from": from_square, "to": to_square, "piece": result.piece}
        ]
        commit = commit_hash(canonical_json({"board": result.board}))
        return success(global_token=result.board, commit=commit, views=views, events=events)

    def _dispatch_clear(self, global_token: str, _args: dict[str, Any]) -> PureResult:
        result = clear_board(global_token)
        views = {
            "fen": result.fen,
            "ascii": result.ascii,
        }
        events = [{"type": "board_cleared"}]
        commit = commit_hash(canonical_json({"board": result.board}))
        return success(global_token=result.board, commit=commit, views=views, events=events)

    def _dispatch_import(self, _global: str, args: dict[str, Any]) -> PureResult:
        fen = args["fen"]
        result = import_fen(fen)
        views = {
            "fen": result.fen,
            "ascii": result.ascii,
        }
        events = [{"type": "board_imported", "fen": fen}]
        commit = commit_hash(canonical_json({"board": result.board}))
        return success(global_token=result.board, commit=commit, views=views, events=events)

    def _dispatch_export(self, global_token: str, _args: dict[str, Any]) -> PureResult:
        if not global_token:
            return error("Missing 'global' board token for export")
        fen = export_fen(global_token)
        views = {"fen": fen}
        commit = commit_hash(canonical_json({"board": global_token}))
        return success(global_token=global_token, commit=commit, views=views)

    def _dispatch_view(self, global_token: str, _args: dict[str, Any]) -> PureResult:
        if not global_token:
            return error("Missing 'global' board token for view")
        views = view_board(global_token)
        commit = commit_hash(canonical_json({"board": global_token}))
        return success(global_token=global_token, commit=commit, views=views)


# Register the chess engine for envelope-based usage.
register_game("chess", ChessEngine())
