#!/usr/bin/env python3
"""
VPP Poker Engine — Reference Binary WebSocket Client SDK
========================================================

Connects to the VPP Game Engine over raw binary WebSockets (port 9001).
Communicates using fixed-size C-struct binary frames:
  - ClientMessage: 16 bytes (Client -> Server)
  - ServerMessage: 32 bytes (Server -> Client)

Requirements:
  pip install websockets
"""

import asyncio
import struct
import sys
import logging
from typing import Tuple, Optional

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("VPPBot")

# ── Protocol Constants ────────────────────────────────────────────────────────
# ClientAction (uint8, offset 0 in ClientMessage)
ACTION_JOIN_TABLE  = 0
ACTION_LEAVE_TABLE = 1
ACTION_FOLD        = 2
ACTION_CALL        = 3
ACTION_RAISE       = 4

# ServerEvent (uint8, offset 0 in ServerMessage)
EVENT_WELCOME         = 0
EVENT_SEAT_ASSIGNED   = 1
EVENT_SEAT_DENIED     = 2
EVENT_PLAYER_JOINED   = 3
EVENT_PLAYER_LEFT     = 4
EVENT_HAND_START      = 10
EVENT_DEAL_HOLE_CARDS = 11
EVENT_DEAL_BOARD      = 12
EVENT_ACTION_REQUEST  = 20
EVENT_PLAYER_ACTED    = 21
EVENT_POT_UPDATE      = 30
EVENT_SHOWDOWN        = 31
EVENT_HAND_RESULT     = 32
EVENT_ERROR           = 255

# Binary struct formats (Little-Endian)
# ClientMessage: <B B B B I i 4s (16 bytes)
#   uint8 action, uint8 table_id, uint8 seat, uint8 _reserved,
#   uint32 player_id, int32 amount, 4 bytes padding
CLIENT_MSG_FORMAT = "<BBBB I i 4s"

# ServerMessage: <B B B B I I i 7s B i i (32 bytes)
#   uint8 event, uint8 table_id, uint8 seat, uint8 card_count,
#   uint32 target_player_id, uint32 player_id, int32 amount,
#   7 bytes cards, uint8 action_type, int32 secondary_amount, int32 _pad
SERVER_MSG_FORMAT = "<BBBB I I i 7s B i i"

SUIT_SYMBOLS = ["H", "D", "C", "S"]
RANK_SYMBOLS = ["2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A"]


def card_to_string(card_byte: int) -> str:
    """Decodes packed card byte (suit * 13 + rank - 2) into human-readable string."""
    if card_byte > 51:
        return "??"
    suit = card_byte // 13
    rank = card_byte % 13
    return f"{RANK_SYMBOLS[rank]}{SUIT_SYMBOLS[suit]}"


class VPPBotClient:
    """High-performance binary WebSocket client for VPP poker engine."""

    def __init__(self, ws_uri: str = "ws://127.0.0.1:9001", player_id: int = 1001, table_id: int = 0):
        self.ws_uri = ws_uri
        self.player_id = player_id
        self.table_id = table_id
        self.seat: Optional[int] = None
        self.chips: int = 1000
        self.hole_cards: list = []
        self.board_cards: list = []

    def pack_action(self, action: int, amount: int = 0) -> bytes:
        """Packs a 16-byte ClientMessage frame."""
        return struct.pack(
            CLIENT_MSG_FORMAT,
            action,
            self.table_id,
            self.seat if self.seat is not None else 255,
            0,  # reserved
            self.player_id,
            amount,
            b"\x00\x00\x00\x00"  # pad
        )

    def unpack_server_message(self, data: bytes) -> dict:
        """Unpacks a 32-byte ServerMessage frame."""
        if len(data) != 32:
            raise ValueError(f"Invalid ServerMessage length: {len(data)} (expected 32)")

        (
            event,
            table_id,
            seat,
            card_count,
            target_pid,
            pid,
            amount,
            raw_cards,
            action_type,
            sec_amount,
            _pad
        ) = struct.unpack(SERVER_MSG_FORMAT, data)

        cards = [card_to_string(b) for b in raw_cards[:card_count]]

        return {
            "event": event,
            "table_id": table_id,
            "seat": seat,
            "card_count": card_count,
            "target_player_id": target_pid,
            "player_id": pid,
            "amount": amount,
            "cards": cards,
            "raw_cards": list(raw_cards[:card_count]),
            "action_type": action_type,
            "secondary_amount": sec_amount,
        }

    def choose_action(self, amount_to_call: int) -> Tuple[int, int]:
        """
        Strategy hook: decide whether to FOLD, CALL, or RAISE.
        Override this method to implement your custom strategy.
        """
        # Baseline strategy: call checks/small bets, fold to large raises
        if amount_to_call == 0:
            return (ACTION_CALL, 0)  # Check
        elif amount_to_call <= 40:
            return (ACTION_CALL, 0)  # Call reasonable bet
        else:
            return (ACTION_FOLD, 0)  # Fold to aggressive raises

    async def run(self):
        """Main client loop."""
        try:
            import websockets
        except ImportError:
            logger.error("The 'websockets' library is required. Install via: pip install websockets")
            sys.exit(1)

        logger.info(f"Connecting to VPP Engine at {self.ws_uri} (Player ID: {self.player_id})...")

        async with websockets.connect(self.ws_uri) as ws:
            logger.info("Connected to WebSocket. Sending JOIN_TABLE request...")
            # Request seat at table
            join_msg = self.pack_action(ACTION_JOIN_TABLE, amount=1000)
            await ws.send(join_msg)

            async for raw_msg in ws:
                if not isinstance(raw_msg, bytes) or len(raw_msg) != 32:
                    continue

                msg = self.unpack_server_message(raw_msg)
                event = msg["event"]

                if event == EVENT_WELCOME:
                    logger.info(f"Server acknowledged connection. Session Player ID: {msg['player_id']}")
                    if self.player_id == 0:
                        self.player_id = msg["player_id"]

                elif event == EVENT_SEAT_ASSIGNED:
                    self.seat = msg["seat"]
                    self.table_id = msg["table_id"]
                    logger.info(f"Seated at Table {self.table_id}, Seat {self.seat}")

                elif event == EVENT_SEAT_DENIED:
                    logger.warning("Seat request denied by engine (table full or invalid).")

                elif event == EVENT_HAND_START:
                    self.board_cards = []
                    self.hole_cards = []
                    logger.info(f"--- New Hand Started (Table {msg['table_id']}) ---")

                elif event == EVENT_DEAL_HOLE_CARDS:
                    self.hole_cards = msg["cards"]
                    logger.info(f"Dealt Hole Cards: {' '.join(self.hole_cards)}")

                elif event == EVENT_DEAL_BOARD:
                    self.board_cards = msg["cards"]
                    logger.info(f"Community Board: {' '.join(self.board_cards)}")

                elif event == EVENT_ACTION_REQUEST:
                    amount_to_call = msg["amount"]
                    act, amt = self.choose_action(amount_to_call)
                    act_name = "FOLD" if act == ACTION_FOLD else ("CALL" if act == ACTION_CALL else "RAISE")
                    logger.info(f"Action Request (To Call: {amount_to_call}). Action chosen: {act_name} {amt}")
                    reply = self.pack_action(act, amt)
                    await ws.send(reply)

                elif event == EVENT_PLAYER_ACTED:
                    p_seat = msg["seat"]
                    p_act = msg["action_type"]
                    p_chips = msg["secondary_amount"]
                    logger.debug(f"Player at seat {p_seat} acted (type: {p_act}, remaining chips: {p_chips})")

                elif event == EVENT_HAND_RESULT:
                    winner_id = msg["player_id"]
                    pot_won = msg["amount"]
                    logger.info(f"Hand Concluded. Winner Player ID: {winner_id}, Awarded: {pot_won} chips")


if __name__ == "__main__":
    uri = sys.argv[1] if len(sys.argv) > 1 else "ws://127.0.0.1:9001"
    pid = int(sys.argv[2]) if len(sys.argv) > 2 else 1001
    tbl = int(sys.argv[3]) if len(sys.argv) > 3 else 0

    client = VPPBotClient(ws_uri=uri, player_id=pid, table_id=tbl)
    try:
        asyncio.run(client.run())
    except KeyboardInterrupt:
        logger.info("Bot stopped by user.")
