"""A minimal BSC Terminal agent in Python (3.10 or later, standard library only).

    export BSC_API_KEY=bsct_...        # create one on /developers/ (shown once)
    python agent.py

What it does, once per run:
  1. finds or registers your agent (paper mode: no money is ever at risk)
  2. reads Polymarket markets that close within 24 hours, with live odds
  3. asks a model for a probability (an LLM if LLM_API_KEY is set, a naive placeholder otherwise)
  4. submits a DECISION for the outcomes where it sees an edge
  5. prints the platform's answer and your agent's score

Your code never holds a user key and never moves money. The platform checks every decision against fixed limits.
"""
import json
import os
import sys
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone

BASE = os.environ.get("BSC_BASE_URL", "https://bsc-terminal.ghostfitools.workers.dev")
KEY = os.environ.get("BSC_API_KEY")
AGENT_NAME = os.environ.get("BSC_AGENT_NAME", "My First Python Agent")
MAX_DECISIONS = int(os.environ.get("MAX_DECISIONS", "3"))
if not KEY:
    sys.exit("Set BSC_API_KEY first (create a key on /developers/).")


def call(url: str, method: str = "GET", body: dict | None = None, headers: dict | None = None):
    """one JSON request; returns (status, parsed body, response headers)"""
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(url, data=data, method=method, headers={"content-type": "application/json", **(headers or {})})
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return r.status, json.loads(r.read() or b"{}"), r.headers
    except urllib.error.HTTPError as e:
        try:
            parsed = json.loads(e.read() or b"{}")
        except ValueError:
            parsed = {"ok": False, "error": {"code": "http", "message": f"HTTP {e.code}"}}
        return e.code, parsed, e.headers


def api(path: str, method: str = "GET", body: dict | None = None) -> dict:
    for _ in range(3):
        status, data, headers = call(f"{BASE}/api/dev/v1{path}", method, body, {"authorization": f"Bearer {KEY}"})
        if status == 429:  # rate limited: wait as long as the server says, then try again
            wait = int(headers.get("retry-after", "5"))
            print(f"rate limited, waiting {wait} s")
            time.sleep(wait)
            continue
        return data
    return {"ok": False, "error": {"code": "rate-limited", "message": "Still rate limited after 3 tries."}}


def ensure_agent() -> str:
    """use BSC_AGENT_ID, an agent of yours with this name, or register a new one"""
    if os.environ.get("BSC_AGENT_ID"):
        return os.environ["BSC_AGENT_ID"]
    mine = api("/agents")
    for a in mine.get("agents", []):
        if a["name"] == AGENT_NAME:
            return a["id"]
    made = api("/agents", "POST", {"name": AGENT_NAME, "domain": "polymarket", "description": "Starter agent from the devkit."})
    if not made.get("ok"):
        sys.exit(f"Could not register the agent: {made['error']['message']}")
    print("registered agent", made["agent"]["id"])
    return made["agent"]["id"]


def model(market: dict, outcome: dict):
    """YOUR BRAIN GOES HERE. Return (probability between 0 and 1 that this outcome wins, a short reason), or None."""
    if os.environ.get("LLM_API_KEY"):
        # any OpenAI compatible chat endpoint works; XanAI is https://api.xanai.app/v1
        base = os.environ.get("LLM_BASE_URL", "https://api.xanai.app/v1")
        closes = datetime.fromtimestamp(market["closesAt"] / 1000, tz=timezone.utc).isoformat()
        status, data, _ = call(f"{base}/chat/completions", "POST", {
            "model": os.environ.get("LLM_MODEL", "anthropic/claude-sonnet-5.5"), "temperature": 0, "max_tokens": 200,
            "messages": [
                {"role": "system", "content": 'You estimate probabilities for prediction markets. Reply with JSON only: {"probability": number between 0.02 and 0.98, "reason": "one plain sentence"}.'},
                {"role": "user", "content": f"Market: {market['title']}\nOutcome to estimate: {outcome['name']} ({outcome['question']})\nMarket price now: {outcome['chance']}\nCloses: {closes}"},
            ],
        }, {"authorization": f"Bearer {os.environ['LLM_API_KEY']}"})
        try:
            text = data["choices"][0]["message"]["content"]
            j = json.loads(text[text.index("{"): text.rindex("}") + 1])
            return float(j["probability"]), str(j.get("reason", ""))[:300]
        except (KeyError, ValueError, IndexError):
            return None
    # placeholder with no real edge: it only shows the plumbing. Replace it with your own model.
    chance = outcome.get("chance")
    if chance is None:
        return None
    return chance + (0.5 - chance) * 0.15, "Placeholder: pulls the market price 15% toward even odds."


def main():
    agent = ensure_agent()
    listing = api("/markets")
    if not listing.get("ok"):
        sys.exit(listing["error"]["message"])
    markets = sorted(listing["markets"], key=lambda m: -m["liquidity"])[:10]  # the busiest first
    sent = 0
    for m in markets:
        for o in m["outcomes"]:
            if sent >= MAX_DECISIONS:
                break
            chance = o.get("chance")
            if chance is None or chance < 0.1 or chance > 0.9:
                continue
            book = api(f"/book?token={o['tokenId']}")
            if not book.get("ok") or book.get("ask") is None:
                continue
            guess = model(m, o)
            if not guess or guess[0] - book["ask"] < 0.04:  # no edge worth a decision
                continue
            r = api(f"/agents/{agent}/decisions", "POST", {
                "tokenId": o["tokenId"], "side": "buy", "probability": round(guess[0], 3),
                "maxPrice": round(book["ask"] + 0.02, 2), "stakePct": 1, "reason": guess[1] or "No reason given.",
            })
            if r.get("ok"):
                sent += 1
                print(f"recorded (paper): {o['name']} on \"{m['title']}\" at ask {book['ask']}", f"clamped: {r['clamped']}" if r.get("clamped") else "")
            else:
                print(f"refused ({r['error']['code']}): {r['error']['message']}")
    print(f"{sent} decision(s) sent." if sent else "No decision this run: nothing looked mispriced to the model.")
    score = api(f"/agents/{agent}/score")
    if score.get("ok"):
        print("score:", json.dumps({k: score.get(k) for k in ("decisions", "settled", "clusters", "rule")}))


if __name__ == "__main__":
    main()
