#!/usr/bin/env python3
"""freeze_rewalk.py — ARION league-desk freeze-sheet re-walk (eip155:8453 Base).

Companion to league_watch.py. league_watch proves WHERE the money came from
(undeclared-inflow legs); freeze_rewalk proves WHAT the frozen row claims —
re-derived cold at the row's own pinned day-close block.

Freeze-row shape (settled musemoneychallenge #102125): one row per muse —
muse_id, claimed total, one line per instrument, claim timestamp, and the
pinned day-close block for the whole sheet (ARION's adopted tooth). Marks
are derived from pool reserves at that block per #102018 (indexers are
sanity-only, never the mark source).

For every instrument leg:
  1. balanceOf(wallet) @ close_block            -> vs claimed amount_raw
  2. mark in numeraire via pinned pool reserves  @ close_block (v2-style
     getReserves; token0/token1 pin which side is the numeraire)
  3. token == numeraire marks 1:1
Row verdict:
  HELD    all legs held AND |rederived - claimed| <= tolerance_pct
  BROKE   any leg balance mismatch OR total diff beyond tolerance
  UNKNOWN any unmarkable leg (no pinned pool / call failed) w/o a BROKE leg
Native-ETH balance at close_block is reported; > dust_threshold_wei is a
visible flag (F2: native inflow is invisible to Transfer events).

Honest boundaries (repeat on every published re-walk):
  - v2-style getReserves pools only. A pinned v3/exotic pool is an UNKNOWN
    leg, not a silent fail — extend adapters before trusting such a board.
  - Marks are pool-spot at one block: a manipulated reserve pair marks
    wrong. Stranger reads the SAME reserves the organizer pinned — that's
    the falsifier's point (agreement is checkable, truth needs the pool
    choice itself to be fair; pool fairness is the organizer's filed leg).
  - Claimed amounts are compared as exact raw integers. If the league
    later pins a leg tolerance, it must be published in the sheet.

Usage:
  python3 freeze_rewalk.py sheet.json [-o outdir]     # re-walk a sheet
  python3 freeze_rewalk.py --selftest [-o outdir]     # two-pass selftest at
                                                     # pinned block 51,000,000

sheet.json:
  {"season":"bankr-s1","day":1,"close_block":51900000,
   "numeraire":{"token":"0x833589fC...2913","decimals":6,"symbol":"USDC"},
   "tolerance_pct":1.0,"dust_threshold_wei":"0",
   "pools":{"0xtoken_lc":{"pool":"0x...","kind":"v2"}},
   "rows":[{"muse_id":"muse_x","name":"x","wallet":"0x...",
            "claimed_total":100.0,
            "instruments":[{"token":"0x...","symbol":"WETH","decimals":18,
                            "amount_raw":"123"}]}],
   "rpcs":["https://mainnet.base.org"]}
"""
import json, os, sys, time, urllib.request

sys.path.insert(0, "/workspace/tools")
from eth_call import keccak256, enc_addr  # noqa: E402

DEFAULT_RPCS = ["https://mainnet.base.org",
                "https://base.gateway.tenderly.co",
                "https://base-rpc.publicnode.com"]
USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
WETH_BASE = "0x4200000000000000000000000000000000000006"
AERODROME_FACTORY = "0x420DD381b31aEf6683db6B902084cB0FFECe40Da"
UA = {"Content-Type": "application/json",
      "User-Agent": "ARION-league-desk/1.0 (freeze re-walk)"}
_i = 0


def rpc(rpcs, method, params, tries=9):
    global _i
    body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method,
                       "params": params}).encode()
    last = None
    for _ in range(tries):
        url = rpcs[_i % len(rpcs)]
        _i += 1
        try:
            req = urllib.request.Request(url, data=body, headers=UA)
            d = json.load(urllib.request.urlopen(req, timeout=30))
            if "result" in d:
                time.sleep(0.25)
                return d["result"]
            last = d.get("error")
        except Exception as e:
            last = str(e)
        time.sleep(0.8)
    raise RuntimeError(f"{method} failed on all rpcs: {last}")


def call(rpcs, to, data, blk):
    tag = blk if isinstance(blk, str) else hex(blk)
    return rpc(rpcs, "eth_call", [{"to": to, "data": data}, tag])


def sel(sig):
    return "0x" + keccak256(sig.encode())[:4].hex()


def u256(r):
    return int(r, 16) if r and r != "0x" else 0


def addr_of(r):
    return "0x" + r[-40:].lower()


def balance_of(rpcs, token, wallet, blk):
    return u256(call(rpcs, token,
                     sel("balanceOf(address)") + enc_addr(wallet), blk))


def decimals_of(rpcs, token, blk):
    try:
        return u256(call(rpcs, token, sel("decimals()"), blk)) or 18
    except Exception:
        return 18


def reserves_v2(rpcs, pool, blk):
    """-> (reserve_token_wei, reserve_num_wei) ordering by token0/token1."""
    t0 = addr_of(call(rpcs, pool, sel("token0()"), blk))
    t1 = addr_of(call(rpcs, pool, sel("token1()"), blk))
    r = call(rpcs, pool, sel("getReserves()"), blk)
    return t0, t1, u256("0x" + r[2:66]), u256("0x" + r[66:130])


def mark_price_v2(rpcs, token, tok_dec, num, num_dec, pool, blk):
    """Price of `token` in numeraire units from a v2-style pool."""
    t0, t1, r0, r1 = reserves_v2(rpcs, pool, blk)
    if token.lower() == t0 and num.lower() == t1:
        r_tok, r_num = r0, r1
    elif token.lower() == t1 and num.lower() == t0:
        r_tok, r_num = r1, r0
    else:
        raise RuntimeError(f"pool {pool} does not contain {token}/{num}")
    if r_tok == 0:
        raise RuntimeError(f"pool {pool} token-side reserve is 0")
    return (r_num / 10 ** num_dec) / (r_tok / 10 ** tok_dec)


def rewalk_row(rpcs, row, cfg, blk):
    num = cfg["numeraire"]
    num_tok, num_dec = num["token"].lower(), int(num.get("decimals", 6))
    tol = float(cfg.get("tolerance_pct", 1.0))
    dust = int(cfg.get("dust_threshold_wei", "0"))
    pools = {k.lower(): v for k, v in (cfg.get("pools") or {}).items()}
    w = row["wallet"].lower()
    legs, total, broke, unknown = [], 0.0, False, False
    for ins in row.get("instruments") or []:
        tok = ins["token"].lower()
        dec = int(ins.get("decimals") or decimals_of(rpcs, tok, blk))
        sym = ins.get("symbol") or tok[:10]
        leg = {"token": tok, "symbol": sym}
        try:
            bal = balance_of(rpcs, tok, w, blk)
            leg["balance_raw"] = str(bal)
            claimed_amt = ins.get("amount_raw")
            if claimed_amt is not None:
                leg["balance_check"] = ("HELD" if str(bal) == str(claimed_amt)
                                        else "BROKE")
                if leg["balance_check"] == "BROKE":
                    broke = True
            else:
                leg["balance_check"] = "unclaimed"
            if tok == num_tok:
                leg["mark"] = bal / 10 ** dec
                leg["mark_source"] = "numeraire-1:1"
            else:
                p = pools.get(tok)
                if not p:
                    leg["mark"] = None
                    leg["mark_source"] = "no-pinned-pool"
                    unknown = True
                elif p.get("kind", "v2") != "v2":
                    leg["mark"] = None
                    leg["mark_source"] = f"unsupported-kind:{p.get('kind')}"
                    unknown = True
                else:
                    price = mark_price_v2(rpcs, tok, dec, num_tok, num_dec,
                                          p["pool"], blk)
                    leg["price_numeraire"] = price
                    leg["mark"] = bal / 10 ** dec * price
                    leg["mark_source"] = f"v2-reserves@{p['pool']}"
            if leg["mark"] is not None:
                total += leg["mark"]
        except Exception as e:
            leg["error"] = str(e)[:200]
            unknown = True
        legs.append(leg)
    native = int(rpc(rpcs, "eth_getBalance", [w, hex(blk)]), 16)
    claimed = row.get("claimed_total")
    diff = (total - float(claimed)) if claimed is not None else None
    diff_pct = (abs(diff) / float(claimed) * 100) if claimed else None
    if broke or (diff_pct is not None and diff_pct > tol):
        verdict = "BROKE"
    elif unknown or claimed is None:
        verdict = "UNKNOWN"
    else:
        verdict = "HELD"
    return {"muse_id": row.get("muse_id"), "name": row.get("name"),
            "wallet": row["wallet"], "claimed_total": claimed,
            "rederived_total": round(total, num_dec),
            "diff": (round(diff, num_dec) if diff is not None else None),
            "diff_pct": (round(diff_pct, 4) if diff_pct is not None else None),
            "tolerance_pct": tol, "legs": legs,
            "native_balance_wei": str(native),
            "native_flag": native > dust, "verdict": verdict}


def run_sheet(cfg, outdir):
    rpcs = cfg.get("rpcs") or DEFAULT_RPCS
    blk = int(cfg["close_block"])
    board = {"season": cfg.get("season"), "day": cfg.get("day"),
             "chain": "eip155:8453", "close_block": blk,
             "numeraire": cfg["numeraire"],
             "walk": {"method": "balanceOf+v2-getReserves @ close_block",
                      "pools": cfg.get("pools") or {},
                      "rpcs": rpcs,
                      "pulled_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ",
                                                time.gmtime())},
             "rows": []}
    for row in cfg.get("rows") or []:
        r = rewalk_row(rpcs, row, cfg, blk)
        board["rows"].append(r)
        print(f"[{r['name'] or r['muse_id']}] claimed={r['claimed_total']} "
              f"rederived={r['rederived_total']} ({r['diff_pct']}%) "
              f"native={native_fmt(r['native_balance_wei'])} "
              f"-> {r['verdict']}")
    n_held = sum(1 for r in board["rows"] if r["verdict"] == "HELD")
    n_broke = sum(1 for r in board["rows"] if r["verdict"] == "BROKE")
    n_unk = sum(1 for r in board["rows"] if r["verdict"] == "UNKNOWN")
    board["summary"] = {"held": n_held, "broke": n_broke, "unknown": n_unk}
    os.makedirs(outdir, exist_ok=True)
    out = os.path.join(outdir, "rewalk_board.json")
    json.dump(board, open(out, "w"), indent=1)
    print(f"rewalk_board -> {out}  held={n_held} broke={n_broke} "
          f"unknown={n_unk} @close_block {blk}")
    return board


def native_fmt(wei):
    return f"{int(wei) / 1e18:.6f}ETH"


def selftest(outdir):
    """Deterministic two-pass check @ pinned blk 51,000,000.

    Wallet = Aerodrome USDC/WETH volatile pool 0xcdac…5c43 (a live contract
    holding both tokens; balances == reserves, nonzero at the pinned block).
    Pass 1 re-derives; pass 2 re-runs with claimed_total = rederived (expect
    HELD) then claimed_total x1.5 and a corrupted amount_raw (expect BROKE).
    Any stranger re-running at the same pinned block gets the same rows."""
    rpcs = DEFAULT_RPCS
    blk = 51000000
    pool_w = "0xcdac0d6c6c59727a65f871236188350531885c43"
    num = {"token": USDC_BASE, "decimals": 6, "symbol": "USDC"}
    pool = mark = None
    # discover pool first-party via Aerodrome factory (same method a sheet
    # filler would use to PIN a pool): getPool(USDC, WETH, stable=false)
    r = call(rpcs, AERODROME_FACTORY,
             sel("getPool(address,address,bool)") + enc_addr(USDC_BASE) +
             enc_addr(WETH_BASE) + format(0, "064x"), hex(blk))
    pool = addr_of(r)
    assert pool == pool_w, f"factory pool {pool} != expected {pool_w}"
    weth_bal = balance_of(rpcs, WETH_BASE, pool_w, hex(blk))
    usdc_bal = balance_of(rpcs, USDC_BASE, pool_w, hex(blk))
    assert weth_bal > 0 and usdc_bal > 0, "pool balances zero at pinned blk"
    cfg = {"season": "selftest", "day": 0, "close_block": blk,
           "numeraire": num, "tolerance_pct": 1.0,
           "pools": {WETH_BASE: {"pool": pool, "kind": "v2"}},
           "rpcs": rpcs,
           "rows": [{"muse_id": "selftest", "name": "aerodrome-pool",
                     "wallet": pool_w, "claimed_total": None,
                     "instruments": [
                         {"token": WETH_BASE, "symbol": "WETH",
                          "decimals": 18, "amount_raw": str(weth_bal)},
                         {"token": USDC_BASE, "symbol": "USDC",
                          "decimals": 6, "amount_raw": str(usdc_bal)}]}]}
    b1 = run_sheet(dict(cfg), outdir)
    red = b1["rows"][0]["rederived_total"]
    assert b1["rows"][0]["legs"][0]["price_numeraire"], "no WETH mark"
    px = b1["rows"][0]["legs"][0]["price_numeraire"]
    assert 500 < px < 20000, f"WETH mark {px} outside sanity band"
    results = {"pass1_rederived": red, "weth_price": px, "pool": pool}
    for name, tot, amt_weth, expect in [
            ("held", red, str(weth_bal), "HELD"),
            ("broke-total", red * 1.5, str(weth_bal), "BROKE"),
            ("broke-bal", red, str(weth_bal - 1), "BROKE")]:
        c2 = dict(cfg)
        c2["rows"] = [dict(cfg["rows"][0], claimed_total=tot,
                           instruments=[
                               dict(cfg["rows"][0]["instruments"][0],
                                    amount_raw=amt_weth),
                               cfg["rows"][0]["instruments"][1]])]
        v = run_sheet(c2, outdir)["rows"][0]["verdict"]
        results[name] = v
        assert v == expect, f"{name}: got {v}, want {expect}"
    json.dump(results, open(os.path.join(outdir, "selftest_result.json"),
                            "w"), indent=1)
    print("SELFTEST PASS:", json.dumps(results))


def main():
    args = sys.argv[1:]
    outdir = "."
    if "-o" in args:
        outdir = args[args.index("-o") + 1]
    if args and args[0] == "--selftest":
        selftest(outdir)
        return
    cfg = json.load(open(args[0]))
    run_sheet(cfg, cfg.get("outdir") or outdir or
               os.path.dirname(os.path.abspath(args[0])))


if __name__ == "__main__":
    main()
