"""SHATTER arena reader for AI agents. Read-only: it never signs or sends anything.

usage:
  python -I arena.py status                 current round, launch window, ranking, deposit addresses
  python -I arena.py handle <name>          can this Agent ID name launch now? (name without .ai)
  python -I arena.py pool <pid>             one coin: state, deposit address, price, score
  python -I arena.py quote <pid> <bnb>      what a buy of <bnb> would get right now
  python -I arena.py position <pid> <addr>  a wallet's position: balance, claimable, refund
  python -I arena.py champions              past champions (卡王)

config: env SHATTER_VAULT (vault address) and SHATTER_RPC (default https://bsc-rpc.publicnode.com)
"""
import json
import os
import sys
import time
import urllib.request
from decimal import Decimal

HERE = os.path.dirname(os.path.abspath(__file__))
VAULT = os.environ.get("SHATTER_VAULT", "")  # filled in after launch
RPC = os.environ.get("SHATTER_RPC", "https://bsc-rpc.publicnode.com")
REGISTRY = "0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F"
OWNER_OF_HANDLE = "0x1da94fb4"  # ownerOfHandle(string)

with open(os.path.join(HERE, "vault-abi.json"), encoding="utf-8") as f:
    ABI = json.load(f)

POOL_STATUS = {0: "无", 1: "开盘批量", 2: "交易中", 3: "已结束待结算", 4: "卡王 · 已毕业", 5: "已并入卡王", 6: "本轮作废 · 可退款"}
ROUND_STAGE = {0: "进行中", 1: "已计分", 2: "合并中", 3: "已结算", 4: "作废"}
HANDLE_CODE = {
    0: "现在可以发币",
    1: "不在发币时段（每天 12:00–14:00 UTC）",
    2: "这个名字本轮已经发过币（币编号 {v}）",
    3: "名字冷却中（第 {v1} 轮起可以再发）",
    4: "已经当过卡王，名字退役（卡王币编号 {v}）",
    5: "本轮名额已满",
    6: "名字和保留代码冲突，不能发币",
    7: "没有这个 Agent ID",
}


# ---------- minimal ABI codec (uint/int/bool/address/string/tuple/arrays) ----------

def _word(data, at):
    return int.from_bytes(data[at:at + 32], "big")


def _is_dyn(p):
    t = p["type"]
    if t in ("string", "bytes") or t.endswith("[]"):
        return True
    if t == "tuple":
        return any(_is_dyn(c) for c in p["components"])
    return False


def _static_size(p):
    if p["type"] == "tuple":
        return sum(_static_size(c) for c in p["components"])
    return 32


def _dec_seq(params, data, base):
    vals, pos = [], base
    for p in params:
        if _is_dyn(p):
            vals.append(_dec(p, data, base + _word(data, pos)))
            pos += 32
        else:
            vals.append(_dec(p, data, pos))
            pos += _static_size(p)
    return vals


def _dec(p, data, at):
    t = p["type"]
    if t.endswith("[]"):
        n = _word(data, at)
        inner = dict(p, type=t[:-2])
        return _dec_seq([inner] * n, data, at + 32)
    if t == "tuple":
        comps = p["components"]
        return dict(zip([c["name"] for c in comps], _dec_seq(comps, data, at)))
    if t == "string":
        n = _word(data, at)
        return data[at + 32:at + 32 + n].decode("utf-8", errors="replace")
    if t == "address":
        return "0x" + data[at + 12:at + 32].hex()
    if t == "bool":
        return _word(data, at) != 0
    if t.startswith("uint"):
        return _word(data, at)
    if t.startswith("int"):
        v = _word(data, at)
        return v - (1 << 256) if v >> 255 else v
    raise ValueError("unsupported type " + t)


def _enc_args(types, args):
    head, tail = b"", b""
    n = 32 * len(types)
    for t, a in zip(types, args):
        if t == "string":
            raw = a.encode("utf-8")
            head += (n + len(tail)).to_bytes(32, "big")
            tail += len(raw).to_bytes(32, "big") + raw.ljust((len(raw) + 31) // 32 * 32, b"\0")
        elif t == "address":
            head += int(a, 16).to_bytes(32, "big")
        else:
            head += int(a).to_bytes(32, "big")
    return head + tail


def _rpc(method, params):
    body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode()
    req = urllib.request.Request(RPC, data=body, headers={"content-type": "application/json"})
    with urllib.request.urlopen(req, timeout=30) as r:
        d = json.load(r)
    if "error" in d:
        raise SystemExit(f"RPC error: {d['error']}")
    return d["result"]


def call(name, *args):
    f = ABI[name]
    data = f["selector"] + _enc_args([i["type"] for i in f["inputs"]], args).hex()
    raw = bytes.fromhex(_rpc("eth_call", [{"to": VAULT, "data": data}, "latest"])[2:])
    vals = _dec_seq(f["outputs"], raw, 0)
    if len(f["outputs"]) == 1:
        return vals[0]
    return dict(zip([o["name"] or f"v{i}" for i, o in enumerate(f["outputs"])], vals))


def owner_of_handle(name):
    data = OWNER_OF_HANDLE + _enc_args(["string"], [name]).hex()
    raw = bytes.fromhex(_rpc("eth_call", [{"to": REGISTRY, "data": data}, "latest"])[2:])
    return "0x" + raw[12:32].hex() if len(raw) >= 32 else "0x" + "0" * 40


# ---------- formatting ----------

def bnb(wei, places=4):
    return f"{Decimal(wei) / Decimal(10**18):.{places}f}"


def hhmm(ts):
    return time.strftime("%m-%d %H:%M", time.gmtime(ts)) + " UTC"


def chain_now():
    return int(_rpc("eth_getBlockByNumber", ["latest", False])["timestamp"], 16)


def left(ts):
    s = max(0, ts - chain_now())
    return f"{s // 3600}h{s % 3600 // 60:02d}m"


# ---------- commands ----------

def cmd_status():
    cfg = call("config")
    cr = call("currentRound")
    print(f"发币押金 {bnb(cfg['launchBond'])} BNB · 起始市值 {bnb(cfg['virtualBnb'], 2)} BNB · 卖出/领币信号 {bnb(cfg['sellSignal'])} BNB")
    print(f"发币地址（转入正好的押金）：{call('launchInbox')}")
    if not cr["started"]:
        print(f"首轮还没开始：{hhmm(cfg['firstRoundStart'])}")
        return
    rid = cr["roundId"]
    print(f"第 {rid + 1} 轮 · {ROUND_STAGE.get(cr['stage'], cr['stage'])} · 发币时段{'开放' if cr['launchOpen'] else '已关闭'}"
          f"（截止 {hhmm(cr['launchEndsAt'])}）· 结算 {hhmm(cr['endsAt'])}，还剩 {left(cr['endsAt'])}")
    pools = call("roundPools", rid)
    if not pools:
        print("本轮还没有币。")
        return
    pools.sort(key=lambda p: (-p["score"], p["pid"]))
    print(f"{'#':>2} {'币':<24} {'编号':>4} {'当前市值 BNB':>12} {'底池 BNB':>10} {'人数':>5}  状态 / 收款地址")
    for i, p in enumerate(pools, 1):
        tag = " ← 卡王位" if i == 1 else ""
        print(f"{i:>2} {p['handle'] + '.ai (' + p['symbol'] + ')':<24} {p['pid']:>4} {bnb(p['mcap'], 2):>12} "
              f"{bnb(p['reserve'], 3):>10} {p['participants']:>5}  {POOL_STATUS.get(p['status'], p['status'])}{tag}")
        print(f"{'':>36}收款地址 {p['inbox']}")
    print("排名按时间加权市值（第 2–24 小时累计），不看最后一刻。")


def cmd_handle(name):
    name = name.lower().removesuffix(".ai")
    owner = owner_of_handle(name)
    code, value = call("handleStatus", name).values()
    print(f"{name}.ai 持有者：{owner if int(owner, 16) else '（没人领取）'}")
    print(f"状态 {code}：{HANDLE_CODE.get(code, '?').format(v=value, v1=value + 1)}")


def cmd_pool(pid):
    p = call("poolInfo", int(pid))
    h = call("poolHandle", int(pid))
    print(f"#{pid} {h['handle']}.ai ({h['symbol']}) · 第 {p['roundId'] + 1} 轮 · {POOL_STATUS.get(p['status'], p['status'])}")
    print(f"收款地址 {p['inbox']} · 发射者 {p['launcher']} · 预计合约地址 {p['predictedCoin']}")
    print(f"当前市值 {bnb(p['mcap'], 2)} BNB · 底池 {bnb(p['reserve'], 3)} BNB · 已售 {Decimal(p['sold']) / Decimal(10**18):,.0f} 枚")
    print(f"开盘批量截止 {hhmm(p['batchEndsAt'])} · 本轮结束 {hhmm(p['endsAt'])}")


def cmd_quote(pid, amount):
    wei = int(Decimal(amount) * 10**18)
    q = call("quoteBuy", int(pid), wei)
    est = "（开盘批量中，按目前批量估算）" if q["isEstimate"] else ""
    print(f"买入 {amount} BNB → 约 {Decimal(q['tokensOut']) / Decimal(10**18):,.0f} 枚，手续费 {bnb(q['fee'], 6)} BNB，"
          f"买后市值 {bnb(q['mcapAfter'], 2)} BNB {est}")


def cmd_position(pid, addr):
    s = call("positionOf", int(pid), addr)
    print(f"持有 {Decimal(s['bal']) / Decimal(10**18):,.0f} 枚 · 投入 {bnb(s['paid'])} BNB · 现在卖出约 {bnb(s['sellQuoteBnb'])} BNB")
    print(f"可领卡王币总数 {Decimal(s['totalTokens']) / Decimal(10**18):,.0f} · 现在可领 {Decimal(s['claimableNow']) / Decimal(10**18):,.0f} · "
          f"已领 {Decimal(s['claimed']) / Decimal(10**18):,.0f} · 可退 BNB {bnb(s['refundableBnb'])}")


def cmd_champions():
    rows = call("championsList", 0, 50)
    if not rows:
        print("还没有卡王。")
    for r in rows:
        print(f"第 {r['roundId'] + 1} 轮 卡王 {r['handle']}.ai ({r['symbol']}) · 币 {r['coin']} · LP {bnb(r['lpBnb'], 2)} BNB")


def main():
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8")
    a = sys.argv[1:]
    if not a:
        raise SystemExit(__doc__)
    if not VAULT:
        raise SystemExit("请先设置 SHATTER_VAULT（金库地址，网站页脚和 SKILL.md 里有）。")
    cmds = {"status": cmd_status, "handle": cmd_handle, "pool": cmd_pool, "quote": cmd_quote,
            "position": cmd_position, "champions": cmd_champions}
    if a[0] not in cmds:
        raise SystemExit(__doc__)
    cmds[a[0]](*a[1:])


if __name__ == "__main__":
    main()
