#!/usr/bin/env python3
"""Configurable client for Galaxy Arena bot tables.

The program stays on your computer. It speaks flawless-bot-1 to
https://diamondbrightai.com and plays on the Galaxy Arena tables.
It does not need the site source.

  FLAWLESS_BOT_TOKEN='paste-token' FLAWLESS_OPEN=1 FLAWLESS_SEATS=2 python3 call-station.py
  FLAWLESS_BOT_TOKEN='other-token' FLAWLESS_JOIN=1 FLAWLESS_STYLE=tight python3 call-station.py

FLAWLESS_SITE overrides the host. FLAWLESS_SEATS is 2, 4, 6, or 8.
Galaxy tables have no hand limit. FLAWLESS_STYLE is station, tight, or loose.
station checks or calls. tight bets a pair of sevens or better, broadway,
or a suited ace, and otherwise checks or folds. loose bets the minimum
when it can. The client polls once before it sits, so the table can see
that it is connected. Logs go to stderr.
"""

import json
import os
import sys
import time
import urllib.error
import urllib.request

PROTOCOL = "flawless-bot-1"
SITE = os.environ.get("FLAWLESS_SITE", "https://diamondbrightai.com").rstrip("/")
TOKEN = os.environ.get("FLAWLESS_BOT_TOKEN", "")
STYLE = os.environ.get("FLAWLESS_STYLE", "station").strip().lower()
RANKS = "23456789TJQKA"


def log(message):
    print(message, file=sys.stderr)


def request(method, path, body=None, auth=False):
    data = None if body is None else json.dumps(body).encode()
    headers = {"content-type": "application/json"}
    if auth:
        headers["authorization"] = f"Bearer {TOKEN}"
    req = urllib.request.Request(SITE + path, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=30) as response:
            return json.load(response)
    except urllib.error.HTTPError as error:
        detail = error.read().decode("utf-8", "replace")
        try:
            parsed = json.loads(detail)
        except json.JSONDecodeError:
            parsed = {"error": detail or f"HTTP {error.code}"}
        parsed["statusCode"] = error.code
        return parsed


def action_for(observation, kind, to=None):
    hand = observation.get("hand") or {}
    action = {
        "protocol": PROTOCOL,
        "kind": kind,
        "hand": hand.get("number"),
        "actions": len(observation.get("actions") or []),
    }
    if to is not None:
        action["to"] = to
    return action


def strong_hole(observation):
    hole = (observation.get("you") or {}).get("hole") or []
    if len(hole) < 2 or len(hole[0]) < 2 or len(hole[1]) < 2:
        return False
    high = RANKS.find(hole[0][0])
    low = RANKS.find(hole[1][0])
    if high < 0 or low < 0:
        return False
    pair = high == low and high >= RANKS.find("7")
    broadway = high >= RANKS.find("T") and low >= RANKS.find("T")
    suited_ace = "A" in (hole[0][0], hole[1][0]) and hole[0][1] == hole[1][1]
    return pair or broadway or suited_ace


def choose(observation):
    legal = observation.get("legal") or {}
    style = STYLE if STYLE in {"station", "tight", "loose"} else "station"
    if style == "tight" and strong_hole(observation):
        if legal.get("raise"):
            return action_for(observation, "raise", legal.get("minTo"))
        if legal.get("bet"):
            return action_for(observation, "bet", legal.get("minTo"))
        if legal.get("call"):
            return action_for(observation, "call")
    if style == "loose":
        if legal.get("bet"):
            return action_for(observation, "bet", legal.get("minTo"))
        if legal.get("call"):
            return action_for(observation, "call")
        if legal.get("raise") and strong_hole(observation):
            return action_for(observation, "raise", legal.get("minTo"))
    if legal.get("check"):
        return action_for(observation, "check")
    if style != "tight" and legal.get("call"):
        return action_for(observation, "call")
    if legal.get("fold"):
        return action_for(observation, "fold")
    if legal.get("check"):
        return action_for(observation, "check")
    return action_for(observation, "fold")


def find_table(seats):
    board = request("GET", "/api/bot-arena/matches")
    tables = list(board.get("open") or []) + list(board.get("live") or [])
    for table in tables:
        if table.get("seats") == seats and (table.get("openSeats") or 0) > 0 and table.get("status") != "complete":
            return table.get("id")
    return None


def connect():
    seats = int(os.environ.get("FLAWLESS_SEATS", "2"))
    if os.environ.get("FLAWLESS_JOIN") == "1":
        match_id = find_table(seats)
        if match_id:
            joined = request("POST", "/api/bot-arena/join", {"matchId": match_id}, auth=True)
            log(joined.get("error") or f"Joined {match_id}")
            return
        log("No table with an open seat matches that size yet.")
    if os.environ.get("FLAWLESS_OPEN") == "1":
        opened = request("POST", "/api/bot-arena/join", {"seats": seats}, auth=True)
        match = opened.get("match") or {}
        log(opened.get("error") or f"Opened {match.get('id', 'a table')}")


def play():
    pending = None
    announced = None
    while True:
        message = request("POST", "/api/bot-arena/turn", {"action": pending} if pending else {}, auth=True)
        if message.get("statusCode"):
            log(message.get("error") or "Bot Arena rejected the request")
            pending = None
            time.sleep(3)
            continue
        status = message.get("status")
        if status == "action":
            pending = choose(message.get("observation") or {})
            continue
        pending = None
        if status == "complete" and message.get("matchId") != announced:
            announced = message.get("matchId")
            log(json.dumps(message.get("result")))
        elif status == "idle" and message.get("lastResult") and message["lastResult"].get("id") != announced:
            announced = message["lastResult"].get("id")
            log(json.dumps(message.get("lastResult")))
        time.sleep((message.get("retryAfterMs") or 1000) / 1000)


def heartbeat():
    message = request("POST", "/api/bot-arena/turn", {}, auth=True)
    if message.get("statusCode"):
        log(message.get("error") or "Galaxy Arena rejected the bot token")
        return False
    log(f"Connected as {STYLE if STYLE in {'station', 'tight', 'loose'} else 'station'}")
    return True


def main():
    if not TOKEN:
        log("Set FLAWLESS_BOT_TOKEN to the token shown when you register a bot.")
        return 1
    if not heartbeat():
        return 1
    connect()
    play()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
