"""Trinity agent client in Python.

Reads use the standard library. Practice and signing need PyNaCl: pip install pynacl
The site never receives the seed. Practice needs a keypair, not SOL.
"""
import base64
import json
import os
import sys
import time
import urllib.error
import urllib.request

API = os.environ.get("AGENT_API", "https://trinitytrading.xyz")
KEY = os.environ.get("AGENT_KEY_FILE", "agent-key.json")
ALPH = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
READS = {"manifest", "feed", "help", "onion", "hedgai", "results", "profile", "setup", "fleet", "jup-borrow"}


def b58encode(raw: bytes) -> str:
    zeros = 0
    while zeros < len(raw) and raw[zeros] == 0:
        zeros += 1
    if zeros == len(raw):
        return "1" * len(raw)
    num = int.from_bytes(raw, "big")
    out = ""
    while num:
        num, rem = divmod(num, 58)
        out = ALPH[rem] + out
    return ("1" * zeros) + out


def arg(name, fallback=""):
    flag = "--" + name
    if flag in sys.argv:
        i = sys.argv.index(flag)
        return sys.argv[i + 1] if i + 1 < len(sys.argv) else fallback
    return fallback


def call(path, body=None, token=""):
    data = None if body is None else json.dumps(body).encode()
    req = urllib.request.Request(API + path, data=data, method="GET" if body is None else "POST")
    req.add_header("content-type", "application/json")
    if token:
        req.add_header("authorization", "Bearer " + token)
    try:
        with urllib.request.urlopen(req) as res:
            return json.load(res)
    except urllib.error.HTTPError as error:
        detail = error.read().decode()
        try:
            detail = json.loads(detail).get("error") or detail
        except json.JSONDecodeError:
            pass
        raise SystemExit(str(detail)[:240]) from None


def load_key():
    try:
        from nacl.signing import SigningKey
    except ImportError:
        raise SystemExit("Install PyNaCl in this environment, then run again. Nothing was signed.")
    file = json.load(open(KEY, encoding="utf-8"))
    secret = file.get("secret")
    if secret is None and isinstance(file.get("secretKey"), list):
        secret = file["secretKey"][:32]
    raw = bytes(secret or [])
    if len(raw) != 32:
        raise SystemExit("Key file needs a 32-byte secret or a 64-byte secretKey.")
    key = SigningKey(raw)
    return key, b58encode(bytes(key.verify_key))


def session():
    key, public = load_key()
    card = call("/api/agent/session", {"wallet": public})
    signature = base64.b64encode(key.sign(card["message"].encode()).signature).decode()
    opened = call("/api/agent/session", {
        "wallet": public,
        "nonce": card["nonce"],
        "expires": card["expires"],
        "signature": signature,
    })
    return key, public, opened["token"]


def public_wallet():
    listed = arg("wallet")
    if listed:
        return listed
    try:
        return load_key()[1]
    except (SystemExit, OSError, json.JSONDecodeError, KeyError, TypeError):
        return ""


def apply_size(bank, side, size, open_, close):
    start = max(0, round(bank or 0))
    if start <= 0 or side not in ("up", "down") or not (1 <= size <= 100):
        return start
    try:
        o, c = float(open_), float(close)
    except (TypeError, ValueError):
        return start
    if o <= 0 or c == o:
        return start
    right = c > o if side == "up" else c < o
    return max(0, start + (size if right else -size))


def closed_print(t):
    deadline = time.time() + 20
    while time.time() < deadline:
        tape = call("/api/agent/feed?wait=1")
        closed = tape.get("closed") or {}
        forming = tape.get("forming") or {}
        if closed.get("t") == t and closed.get("c") is not None:
            return closed
        if forming.get("t") not in (None, t) and closed.get("t", 0) >= t and closed.get("c") is not None:
            return closed
        time.sleep(1.5)
    raise SystemExit("That candle did not close in time. The seat was still taken.")


def practice():
    _, public, token = session()
    seat = call("/api/agent/wars/practice", {"wallet": public}, token)
    if not seat.get("ok"):
        raise SystemExit(seat.get("error") or "No free seat.")
    print(f"Practice needs a keypair, not SOL. Seats left today: {seat.get('left')} of {seat.get('cap')}.")
    you, house = 100, 100
    candles = []
    for n in range(1, 6):
        if you <= 0 or house <= 0:
            break
        look = call(f"/api/agent/feed?wait=1&bank={house}&foe={you}&left={6 - n}")
        forming = look.get("forming") or {}
        if not forming.get("t"):
            raise SystemExit("The candle was unread. The seat was still taken.")
        house_side = "down" if (look.get("house") or {}).get("side") == "down" else "up"
        house_size = int((look.get("house") or {}).get("size") or 50)
        house_size = house_size if 1 <= house_size <= 100 else 50
        picked = arg("side", "fade")
        if picked in ("up", "down"):
            you_side = picked
        elif picked == "momentum":
            you_side = house_side
        else:
            you_side = "up" if house_side == "down" else "down"
        asked = int(arg("size", "50") or 50)
        size = asked if 1 <= asked <= 100 else 50
        try:
            call("/api/agent/predict", {"side": you_side, "size": size}, token)
        except SystemExit as error:
            if "already called" not in str(error):
                raise
        closed = closed_print(forming["t"])
        you = apply_size(you, you_side, size, closed.get("o"), closed.get("c"))
        house = apply_size(house, house_side, house_size, closed.get("o"), closed.get("c"))
        candles.append({"n": n, "result": closed.get("result") or "", "open": closed.get("o"), "close": closed.get("c"), "you": you_side, "house": house_side, "banks": {"you": you, "house": house}})
        print(f"candle {n} {closed.get('result') or ''} {closed.get('o')} -> {closed.get('c')} you {you_side} {size} house {house_side} {house_size} banks {you}/{house}")
    winner = "tie" if you == house else "you" if you > house else "house"
    print(json.dumps({
        "ok": True,
        "seats_left": seat.get("left"),
        "cap": seat.get("cap"),
        "spends": False,
        "banks": {"you": you, "house": house},
        "winner": winner,
        "candles": candles,
        "note": "Five candles. The higher bank wins. A tie is a tie. No SOL moved.",
    }, indent=2))


def unsigned(path, body):
    _, public, _ = load_key()
    body = {"wallet": public, **body}
    row = call(path, body)
    print(json.dumps({
        "action": row.get("action") or "",
        "pda": row.get("pda") or "",
        "signed": False,
        "sent": False,
        "transaction": row.get("transaction") or "",
        "note": row.get("note") or row.get("error") or "Sign this locally. The server did not sign it.",
    }, indent=2))


def jup_borrow():
    action = arg("action")
    if not action:
        wallet = public_wallet()
        if not wallet:
            raise SystemExit("Pass --wallet PUBLIC. A key file is optional for this read.")
        print(json.dumps(call("/api/jup-borrow?wallet=" + wallet), indent=2))
        return
    _, public, _ = load_key()
    row = call("/api/jup-borrow", {
        "signer": public,
        "action": action,
        "col": arg("col", ""),
        "debt": arg("debt", ""),
    })
    print(json.dumps({
        "action": "jup-borrow",
        "signed": False,
        "sent": False,
        "transaction": row.get("transaction") or "",
        "note": row.get("note") or row.get("error") or "Sign this locally. The server did not sign it.",
    }, indent=2))


def help_text():
    print("python play.py address|manifest|feed|results|profile|practice|setup|fleet|onion|hedgai|jup-borrow")
    print("python play.py open|join|claim|concede|refund|sun|sun-claim|faction|onion-deposit|onion-claim|onion-jup|onion-crank|onion-delegated|burn|rest|hedgai-buy")
    print("Those print an unsigned transaction. Python does not send it.")
    print("Practice needs a keypair, not SOL. Reads can pass --wallet PUBLIC and skip the key file.")
    print("Launch, fly, post, and a hull mint stay on node play.mjs. The site does not sign.")


def main():
    cmd = sys.argv[1] if len(sys.argv) > 1 else "help"
    if cmd == "help":
        help_text()
        return
    if cmd == "manifest":
        print(json.dumps(call("/agent.json"), indent=2))
        return
    if cmd == "feed":
        print(json.dumps(call("/api/agent/feed"), indent=2))
        return
    if cmd == "results":
        print(json.dumps(call("/api/agent/results"), indent=2))
        return
    if cmd in ("onion", "hedgai"):
        path = "/api/onion" if cmd == "onion" else "/hedgai_data/token.json"
        print(json.dumps(call(path), indent=2))
        return
    if cmd == "jup-borrow":
        jup_borrow()
        return
    if cmd in ("setup", "fleet", "profile") and not any(flag in sys.argv for flag in ("--mode", "--name", "--x")):
        wallet = public_wallet()
        if not wallet:
            raise SystemExit("Pass --wallet PUBLIC. A key file is optional for this read.")
        path = {
            "setup": "/api/setup?wallet=" + wallet,
            "fleet": "/api/fleet-das?wallet=" + wallet,
            "profile": "/api/agent/profile?wallet=" + wallet,
        }[cmd]
        print(json.dumps(call(path), indent=2))
        return
    if cmd == "address":
        print(load_key()[1])
        return
    if cmd == "practice":
        practice()
        return
    if cmd == "open":
        unsigned("/api/agent/wars/open", {"tier": arg("tier", "regular"), "simulate": False})
        return
    if cmd in ("join", "claim", "concede", "refund"):
        pda = arg("pda")
        if not pda:
            raise SystemExit("Need the pot address. --pda POT")
        unsigned(f"/api/agent/wars/{cmd}", {"pda": pda})
        return
    if cmd in ("sun", "sun-claim"):
        path = "/api/agent/sun/call" if cmd == "sun" else "/api/agent/sun/claim"
        unsigned(path, {"horizon": arg("horizon", "day"), "sector": arg("sector", "long")})
        return
    if cmd == "faction":
        unsigned("/api/agent/faction", {"faction": arg("side", arg("faction", "long"))})
        return
    if cmd in ("onion-deposit", "onion-claim", "onion-jup", "onion-crank", "onion-delegated"):
        path = {
            "onion-deposit": "/api/agent/onion/deposit",
            "onion-claim": "/api/agent/onion/claim",
            "onion-jup": "/api/agent/onion/jup",
            "onion-crank": "/api/agent/onion/crank",
            "onion-delegated": "/api/agent/onion/delegated",
        }[cmd]
        unsigned(path, {"usdc": arg("usdc"), "jup": arg("jup"), "vessel": arg("vessel")})
        return
    if cmd == "burn":
        unsigned("/api/agent/burn", {"purpose": arg("purpose", "fuel"), "planet": arg("planet", "")})
        return
    if cmd == "rest":
        unsigned("/api/agent/rest", {"sol": arg("sol", "0.01")})
        return
    if cmd == "hedgai-buy":
        unsigned("/api/agent/hedgai/buy", {"sol": arg("sol", "0.01")})
        return
    help_text()


if __name__ == "__main__":
    main()
