"""Cards game engine - Named players with auto-registration.

Features:
- Players identified by names (case-insensitive)
- Auto-registration on first deal
- Per-player draw tracking embedded in token
- Pure functional API: deck token in/out
"""

from __future__ import annotations

import base64
import json
import re
import secrets
from dataclasses import dataclass
from typing import Any

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

# Card constants
SUITS = ["S", "H", "D", "C"]
RANKS = ["A", "K", "Q", "J", "10", "9", "8", "7", "6", "5", "4", "3", "2"]
CARD_COUNT = 52
DISCARD_OWNER = 255

# Card validation pattern
CARD_PATTERN = re.compile(r"^(A|K|Q|J|10|[2-9])(S|H|D|C)$", re.IGNORECASE)


def idx_to_card(idx: int) -> str:
    """Convert card index to face string."""
    return f"{RANKS[idx % 13]}{SUITS[idx // 13]}"


_RANK_TO_INDEX = {r: i for i, r in enumerate(RANKS)}
_SUIT_TO_INDEX = {s: i for i, s in enumerate(SUITS)}


def card_to_idx(card: str) -> int:
    """Convert card face string to index."""
    s = card.strip().upper()
    if not s or s[-1] not in "SHDC":
        raise ValueError(f"bad suit in card {card}")
    rank, suit = s[:-1], s[-1]
    if rank not in _RANK_TO_INDEX:
        raise ValueError(f"bad rank in card {card}")
    return _SUIT_TO_INDEX[suit] * 13 + _RANK_TO_INDEX[rank]


def is_valid_player_name(name: str) -> bool:
    """Check if name is valid (alphanumeric + - _ and not a card pattern)."""
    if not name:
        return False
    # Can't be a card pattern
    if CARD_PATTERN.match(name):
        return False
    # Must be alphanumeric + - _
    if not re.match(r"^[a-zA-Z0-9_-]+$", name):
        return False
    return True


@dataclass
class DeckState:
    """Deck state."""

    seed: str
    players: list[str]  # Ordered list of player names (lowercase)
    draws: dict[str, int]  # Per-player draw count for batching invariance
    t: int  # Total cards drawn from deck
    owner_map: list[int]  # 0=undrawn, 1..N=player position, 255=discard


def encode_deck(state: DeckState) -> str:
    """Encode deck state to token.

    Format: d.<base64-json>
    """
    data = {
        "seed": state.seed,
        "players": state.players,
        "draws": state.draws,
        "t": state.t,
        "owner": state.owner_map,
    }
    json_bytes = json.dumps(data, separators=(",", ":")).encode("utf-8")
    b64 = base64.urlsafe_b64encode(json_bytes).decode("ascii").rstrip("=")
    return f"d.{b64}"


def decode_deck(token: str) -> DeckState:
    """Decode token to deck state."""
    if not token.startswith("d."):
        raise ValueError("Invalid token: must start with 'd.'")

    b64_data = token[2:]
    pad = "=" * (-len(b64_data) % 4)
    json_bytes = base64.urlsafe_b64decode(b64_data + pad)
    data = json.loads(json_bytes)

    return DeckState(
        seed=data["seed"],
        players=data["players"],
        draws=data.get("draws", {}),
        t=data["t"],
        owner_map=data["owner"],
    )


def deck_from_seed(seed: str) -> list[int]:
    """Generate deterministic shuffled deck from seed."""
    prng = SplitMix64(seed.encode("utf-8"))
    deck = list(range(CARD_COUNT))
    for i in range(CARD_COUNT - 1, 0, -1):
        j = prng.randbelow(i + 1)
        deck[i], deck[j] = deck[j], deck[i]
    return deck


def _reverse_index(deck: list[int]) -> list[int]:
    """Build reverse index: card value -> position in deck."""
    rev = [0] * CARD_COUNT
    for i, v in enumerate(deck):
        rev[v] = i
    return rev


def hand_of(state: DeckState, player_name: str) -> list[str]:
    """Get hand for a player by name."""
    name_lower = player_name.lower()
    if name_lower not in state.players:
        raise ValueError(f"Player '{player_name}' not registered")

    position = state.players.index(name_lower) + 1  # 1-indexed
    deck = deck_from_seed(state.seed)
    card_indices = [deck[i] for i in range(state.t) if state.owner_map[i] == position]
    return [idx_to_card(c) for c in card_indices]


# Results
@dataclass
class DeckResult:
    """Result from deck operations."""

    deck: str  # New deck token
    cards: list[str] | None = None  # Cards dealt (if applicable)
    hand: list[str] | None = None  # Current hand (if applicable)


# Operations


def create_deck(seed: str | None = None) -> DeckResult:
    """Create a new empty deck.

    Args:
        seed: Optional seed string. If None, generates random seed.

    Returns:
        DeckResult with new deck token
    """
    if seed is None:
        seed = secrets.token_urlsafe(16)

    state = DeckState(seed=seed, players=[], draws={}, t=0, owner_map=[0] * CARD_COUNT)

    return DeckResult(deck=encode_deck(state))


def deal(deck_token: str, player_name: str, num_cards: int) -> DeckResult:
    """Deal cards to a player (auto-registers if new).

    Args:
        deck_token: Current deck token
        player_name: Player name (case-insensitive)
        num_cards: Number of cards to deal

    Returns:
        DeckResult with new deck token, cards dealt, and current hand

    Raises:
        ValueError: If invalid parameters or insufficient cards
    """
    state = decode_deck(deck_token)
    name_lower = player_name.lower()

    # Validate player name
    if not is_valid_player_name(player_name):
        raise ValueError(f"Invalid player name: '{player_name}'")

    if num_cards <= 0:
        raise ValueError("num_cards must be >= 1")

    if state.t + num_cards > CARD_COUNT:
        raise ValueError("Not enough cards in deck")

    # Auto-register new player
    if name_lower not in state.players:
        state.players.append(name_lower)
        state.draws[name_lower] = 0

    position = state.players.index(name_lower) + 1
    deck = deck_from_seed(state.seed)

    # Deal cards
    dealt = []
    for i in range(num_cards):
        state.owner_map[state.t + i] = position
        dealt.append(deck[state.t + i])

    state.t += num_cards
    state.draws[name_lower] += num_cards

    new_token = encode_deck(state)
    hand = hand_of(state, player_name)

    return DeckResult(deck=new_token, cards=[idx_to_card(c) for c in dealt], hand=hand)


def discard(deck_token: str, player_name: str, cards: list[str]) -> DeckResult:
    """Discard cards from a player's hand.

    Args:
        deck_token: Current deck token
        player_name: Player name
        cards: Card faces to discard

    Returns:
        DeckResult with new deck token and current hand

    Raises:
        ValueError: If player not registered or doesn't own cards
    """
    state = decode_deck(deck_token)
    name_lower = player_name.lower()

    if name_lower not in state.players:
        raise ValueError(f"Player '{player_name}' not registered")

    position = state.players.index(name_lower) + 1
    deck = deck_from_seed(state.seed)
    rev = _reverse_index(deck)

    # Validate and mark discards
    for card in cards:
        idx = card_to_idx(card)
        i = rev[idx]
        if i >= state.t:
            raise ValueError(f"Card not drawn: {card}")
        if state.owner_map[i] != position:
            raise ValueError(f"Player '{player_name}' doesn't own: {card}")
        state.owner_map[i] = DISCARD_OWNER

    new_token = encode_deck(state)
    hand = hand_of(state, player_name)

    return DeckResult(deck=new_token, hand=hand)


def trade(deck_token: str, name1: str, card1: str, name2: str, card2: str) -> DeckResult:
    """Trade cards between two players.

    Args:
        deck_token: Current deck token
        name1: First player name
        card1: Card player 1 gives
        name2: Second player name
        card2: Card player 2 gives

    Returns:
        DeckResult with new deck token

    Raises:
        ValueError: If players not registered or don't own cards
    """
    state = decode_deck(deck_token)
    name1_lower = name1.lower()
    name2_lower = name2.lower()

    if name1_lower not in state.players:
        raise ValueError(f"Player '{name1}' not registered")
    if name2_lower not in state.players:
        raise ValueError(f"Player '{name2}' not registered")

    pos1 = state.players.index(name1_lower) + 1
    pos2 = state.players.index(name2_lower) + 1

    deck = deck_from_seed(state.seed)
    rev = _reverse_index(deck)

    # Validate and swap card1
    idx1 = card_to_idx(card1)
    i1 = rev[idx1]
    if i1 >= state.t:
        raise ValueError(f"Card not drawn: {card1}")
    if state.owner_map[i1] != pos1:
        raise ValueError(f"Player '{name1}' doesn't own: {card1}")

    # Validate and swap card2
    idx2 = card_to_idx(card2)
    i2 = rev[idx2]
    if i2 >= state.t:
        raise ValueError(f"Card not drawn: {card2}")
    if state.owner_map[i2] != pos2:
        raise ValueError(f"Player '{name2}' doesn't own: {card2}")

    # Swap ownership
    state.owner_map[i1] = pos2
    state.owner_map[i2] = pos1

    new_token = encode_deck(state)
    return DeckResult(deck=new_token)


def exchange(deck_token: str, player_name: str, discards: list[str], num_draw: int) -> DeckResult:
    """Exchange cards (discard then draw).

    Args:
        deck_token: Current deck token
        player_name: Player name
        discards: Cards to discard
        num_draw: Number of new cards to draw

    Returns:
        DeckResult with new deck token, new cards, and current hand

    Raises:
        ValueError: If invalid parameters
    """
    # Discard first
    if discards:
        result = discard(deck_token, player_name, discards)
        deck_token = result.deck

    # Then deal
    if num_draw > 0:
        return deal(deck_token, player_name, num_draw)

    # No deal
    state = decode_deck(deck_token)
    hand = hand_of(state, player_name)
    return DeckResult(deck=deck_token, cards=[], hand=hand)


def view_public(deck_token: str) -> dict:
    """Get public view of deck state."""
    state = decode_deck(deck_token)
    return {
        "seed": state.seed,
        "players": state.players,
        "deck_remaining": CARD_COUNT - state.t,
        "cards_drawn": state.t,
        "discard_count": sum(1 for o in state.owner_map[: state.t] if o == DISCARD_OWNER),
    }


def view_all_hands(deck_token: str) -> dict[str, list[str]]:
    """View all players' hands (debug/spectator mode)."""
    state = decode_deck(deck_token)
    return {name: hand_of(state, name) for name in state.players}


class CardsEngine:
    """Envelope-based adapter for the cards 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]] = {
            "deck": self._dispatch_deck,
            "deal": self._dispatch_deal,
            "discard": self._dispatch_discard,
            "trade": self._dispatch_trade,
            "exchange": self._dispatch_exchange,
            "view_public": self._dispatch_view_public,
            "view_hand": self._dispatch_view_hand,
            "view_all": self._dispatch_view_all,
        }
        handler = handlers.get(op)
        if handler is None:
            raise ValueError(f"Unknown cards op: {op}")
        return handler(global_token, args)

    def _dispatch_deck(self, _global: str, args: dict[str, Any]) -> PureResult:
        seed = args.get("seed")
        result = create_deck(seed)
        views: dict[str, Any] = {
            "seed": seed or "<random>",
            "players": [],
            "deck_remaining": CARD_COUNT,
            "cards_drawn": 0,
        }
        commit = commit_hash(canonical_json({"deck": result.deck}))
        return success(global_token=result.deck, commit=commit, views=views)

    def _dispatch_deal(self, global_token: str, args: dict[str, Any]) -> PureResult:
        if not global_token:
            raise ValueError("Missing 'global' deck token for deal")
        player = args["player"]
        num_cards = int(args["num_cards"])
        result = deal(global_token, player, num_cards)
        views = {
            "player": player,
            "hand": result.hand or [],
            "cards": result.cards or [],
        }
        commit = commit_hash(canonical_json({"deck": result.deck}))
        return success(global_token=result.deck, commit=commit, views=views)

    def _dispatch_discard(self, global_token: str, args: dict[str, Any]) -> PureResult:
        if not global_token:
            raise ValueError("Missing 'global' deck token for discard")
        player = args["player"]
        cards = list(args.get("cards", []))
        result = discard(global_token, player, cards)
        views = {
            "player": player,
            "hand": result.hand or [],
        }
        commit = commit_hash(canonical_json({"deck": result.deck}))
        return success(global_token=result.deck, commit=commit, views=views)

    def _dispatch_trade(self, global_token: str, args: dict[str, Any]) -> PureResult:
        if not global_token:
            raise ValueError("Missing 'global' deck token for trade")
        name1 = args["player_one"]
        card1 = args["card_one"]
        name2 = args["player_two"]
        card2 = args["card_two"]
        result = trade(global_token, name1, card1, name2, card2)
        views = {"players": [name1, name2]}
        commit = commit_hash(canonical_json({"deck": result.deck}))
        return success(global_token=result.deck, commit=commit, views=views)

    def _dispatch_exchange(self, global_token: str, args: dict[str, Any]) -> PureResult:
        if not global_token:
            raise ValueError("Missing 'global' deck token for exchange")
        player = args["player"]
        num_draw = int(args["num_draw"])
        discards = list(args.get("discards", []))
        result = exchange(global_token, player, discards, num_draw)
        views = {
            "player": player,
            "hand": result.hand or [],
            "cards": result.cards or [],
        }
        commit = commit_hash(canonical_json({"deck": result.deck}))
        return success(global_token=result.deck, commit=commit, views=views)

    def _dispatch_view_public(self, global_token: str, _args: dict[str, Any]) -> PureResult:
        if not global_token:
            raise ValueError("Missing 'global' deck token for view_public")
        info = view_public(global_token)
        commit = commit_hash(canonical_json({"deck": global_token}))
        return success(global_token=global_token, commit=commit, views=info)

    def _dispatch_view_hand(self, global_token: str, args: dict[str, Any]) -> PureResult:
        if not global_token:
            raise ValueError("Missing 'global' deck token for view_hand")
        player = args["player"]
        hand = hand_of(decode_deck(global_token), player)
        commit = commit_hash(canonical_json({"deck": global_token}))
        return success(
            global_token=global_token,
            commit=commit,
            views={"player": player, "hand": hand},
        )

    def _dispatch_view_all(self, global_token: str, _args: dict[str, Any]) -> PureResult:
        if not global_token:
            raise ValueError("Missing 'global' deck token for view_all")
        hands = view_all_hands(global_token)
        commit = commit_hash(canonical_json({"deck": global_token}))
        return success(global_token=global_token, commit=commit, views=hands)


# Register the cards engine for envelope-based usage.
register_game("cards", CardsEngine())
