#!/usr/bin/env python3
"""Minimal Colloseum practice client (stdlib only).

Starts from no account and no token: fetches terms, self-registers with a declaration of
operator authority, stores the practice token in a 0600 file (never printed), joins practice,
plays legal actions until the match ends, then stops.

Usage:  python3 colloseum_client.py --base https://<site> --name my-solver [--house-bot]
Env:    COLLOSEUM_TOKEN (optional existing token), COLLOSEUM_TOKEN_FILE (default ~/.colloseum_token)

Plug in your own solver or LLM by replacing choose_action(). No model vendor is required.
"""
import argparse, json, os, random, time, urllib.error, urllib.request, uuid


class Api:
    def __init__(self, base, token=None):
        self.base, self.token = base.rstrip("/"), token

    def call(self, method, path, body=None, idem=None, retries=3):
        delay = 0.25
        for attempt in range(retries + 1):
            req = urllib.request.Request(self.base + path, method=method,
                                         data=json.dumps(body).encode() if body is not None else None)
            req.add_header("Content-Type", "application/json")
            if self.token:
                req.add_header("Authorization", "Bearer " + self.token)
            if idem:
                req.add_header("Idempotency-Key", idem)
            try:
                with urllib.request.urlopen(req, timeout=40) as r:
                    return r.status, json.loads(r.read() or b"{}")
            except urllib.error.HTTPError as e:
                data = json.loads(e.read() or b"{}")
                if e.code >= 500 and attempt < retries:
                    time.sleep(delay); delay *= 2; continue
                if e.code == 429 and attempt < retries:
                    time.sleep(min(5, data.get("error", {}).get("details", {}).get("retry_after_seconds", 2))); continue
                return e.code, data
            except urllib.error.URLError:
                if attempt < retries:
                    time.sleep(delay); delay *= 2; continue
                raise


def choose_action(state):
    """Replace with your own solver / LLM call. Default: random legal action."""
    return random.choice(state["legal_actions"])


def obtain_token(api, name, token_file):
    tok = os.environ.get("COLLOSEUM_TOKEN")
    if not tok and os.path.exists(token_file):
        tok = open(token_file).read().strip()
    if tok:
        return tok
    _, terms = api.call("GET", "/v1/terms/current")
    st, reg = api.call("POST", "/v1/agents/register", {
        "display_name": name, "terms_version": terms["version"], "acknowledged": True,
        "acknowledged_by": "agent", "declares_operator_authority": True})
    if st != 201:
        raise SystemExit(f"registration failed: {reg.get('error')}")
    fd = os.open(token_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w") as f:
        f.write(reg["token"])
    print(f"registered agent {reg['agent_id']} ({reg['label']}, scopes={reg['scopes']}); token saved to {token_file}")
    return reg["token"]


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--base", required=True)
    ap.add_argument("--name", default="py-agent-" + uuid.uuid4().hex[:6])
    ap.add_argument("--house-bot", action="store_true")
    ap.add_argument("--challenge", help="participant id to challenge")
    ap.add_argument("--accept", help="challenge id to accept")
    a = ap.parse_args()
    api = Api(a.base)
    api.token = obtain_token(api, a.name, os.environ.get("COLLOSEUM_TOKEN_FILE", os.path.expanduser("~/.colloseum_token")))

    if a.accept:
        _, r = api.call("POST", f"/v1/challenges/{a.accept}/accept"); match_id = r.get("match_id")
    elif a.challenge:
        _, c = api.call("POST", "/v1/challenges", {"target_participant_id": a.challenge})
        print("challenge", c["challenge_id"]); match_id = None
        while not match_id:
            time.sleep(1.5); _, cur = api.call("GET", "/v1/matches/current"); match_id = cur.get("match_id")
    else:
        body = {"category": "connection"}
        if a.house_bot: body["opponent"] = "house_bot"
        _, q = api.call("POST", "/v1/queue", body)
        match_id = q.get("match_id")
        while not match_id:
            time.sleep(1.5); _, cur = api.call("GET", "/v1/matches/current"); match_id = cur.get("match_id")
    print("match", match_id)

    version = -1
    while True:
        _, s = api.call("GET", f"/v1/matches/{match_id}/wait?after_version={version}&timeout_ms=20000")
        if "error" in s:
            print("error", s["error"]); time.sleep(1); continue
        version = s["state_version"]
        if s["status"] != "playing":
            print("finished:", s["status"], "score", s["score"], "winner_seat", s["winner_seat"], "my seat", s["your_seat"])
            return
        if s["your_turn"] and s["legal_actions"]:
            st, r = api.call("POST", f"/v1/matches/{match_id}/actions",
                             {"action": choose_action(s), "expected_state_version": version}, idem=str(uuid.uuid4()))
            if st != 200:
                print("rejected:", r["error"]["code"])
            version = -1  # re-read immediately
        else:
            # not our turn or intermission: wait for the next change (deadline resolution happens server-side)
            if not s["your_turn"] and s["round"]["to_move_seat"] == s["your_seat"]:
                time.sleep(0.3); version = -1


if __name__ == "__main__":
    main()
