#!/usr/bin/env python3
"""CATJESUS holder giveaway, provably fair.

  announce  --snapshot "2026-10-07 12:00" [--winners 21 --prize 5000 --min-hold 1000 --seed-offset 1000]
            writes giveaway/giveaway.json, prints the announcement post, ships giveaway.json to the site box
  snapshot  resolves the snapshot block (first block at/after the timestamp), replays every Transfer since launch,
            writes eligible.json = wallets holding >= min_hold at that block (contracts, heir, giveaway wallet excluded)
  draw      [--entrants entrants.txt]  seed = hash of block (snapshot_block + seed_offset); picks winners without
            replacement from eligible (intersected with entrants if given); writes winners.json + results post
  pay       sends prize CATJESUS to each winner from the giveaway wallet (PRIVATE_KEY in .env) via cast; records tx hashes
  verify    recomputes the draw from public data and prints whether it matches winners.json (anyone can run this)

Fairness: snapshot time, seed block and this script are published before the seed block exists. The seed is a chain
block hash nobody can know in advance. Anyone can re-run `snapshot` + `draw` and get the same winners.
"""
import argparse, datetime, hashlib, json, os, subprocess, sys, time, urllib.request

TOKEN = "0x27d50412398db5f405e5738545f6493e345ac2fe"
TOKEN_CS = "0x27D50412398db5f405e5738545f6493E345ac2FE"
LAUNCH_BLOCK = 75791286
GIVEAWAY_WALLET = "0x8c90b6fd370afd52f9129927cb48d10702927fd5"
SYSTEM = {"0x8366a39cc670b4001a1121b8f6a443a643e40951",   # Uniswap v4 PoolManager (the pool)
          "0x6422265291d1876c70ab7c97010b0577fbe02357",   # Tomb
          "0x25e388eb9fc6c2de1cd5b1c0f6fafefd3cd2e2f1",   # Genesis
          "0x92970d7da580343feb94b8b5d0dde8047f32cabc",   # Ark
          "0xf64429acf52955397550636103bcd9dc6550af09",   # heir (creator)
          GIVEAWAY_WALLET, "0x" + "0" * 40}
TOPIC_XFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
RPCS = ["https://rpc-robinhood.blockmachine.io", "https://robinhood.drpc.org", "https://rpc.mainnet.chain.robinhood.com"]
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/128 Safari/537.36"
HERE = os.path.dirname(os.path.abspath(__file__))
GIVE = os.path.join(HERE, "giveaway")
BOX = os.environ.get("CATJESUS_HOST", "root@45.79.189.176")
BOX_DIR = "/opt/catjesus"
HANDLE = "@CATJESUS___"
CHUNK = 9000


def rpc(method, params):
    err = None
    for u in RPCS * 2:
        try:
            r = urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": UA, "Content-Type": "application/json"},
                data=json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode()), timeout=40)
            d = json.loads(r.read())
            if "error" in d:
                err = d["error"]; continue
            return d["result"]
        except Exception as e:
            err = e
    raise SystemExit(f"rpc {method} failed: {err}")


def block(n):
    b = rpc("eth_getBlockByNumber", [hex(n), False])
    return int(b["timestamp"], 16), b["hash"]


def latest():
    return int(rpc("eth_blockNumber", []), 16)


def block_at(ts):
    lo, hi = LAUNCH_BLOCK, latest()
    if block(hi)[0] < ts:
        return None
    while lo < hi:
        mid = (lo + hi) // 2
        if block(mid)[0] >= ts:
            hi = mid
        else:
            lo = mid + 1
    return lo


def balances_at(n):
    bal = {}
    frm = LAUNCH_BLOCK
    while frm <= n:
        to = min(frm + CHUNK - 1, n)
        for l in rpc("eth_getLogs", [{"fromBlock": hex(frm), "toBlock": hex(to), "address": TOKEN, "topics": [TOPIC_XFER]}]):
            a = "0x" + l["topics"][1][-40:]; b = "0x" + l["topics"][2][-40:]; v = int(l["data"], 16)
            bal[a] = bal.get(a, 0) - v; bal[b] = bal.get(b, 0) + v
        frm = to + 1
    return bal


def load(name, default=None):
    p = os.path.join(GIVE, name)
    return json.load(open(p)) if os.path.exists(p) else default


def save(name, o):
    os.makedirs(GIVE, exist_ok=True)
    json.dump(o, open(os.path.join(GIVE, name), "w"), indent=1)


def to_box(name):
    subprocess.run(["scp", "-q", os.path.join(GIVE, name), f"{BOX}:{BOX_DIR}/{name}"], check=False)


def announce_text(g):
    return (f"CATJESUS giveaway: {g['winners']} holders win {g['prize']:,} CATJESUS each.\n\n"
            f"To enter:\n1. Hold at least {g['min_hold']:,} CATJESUS at the snapshot ({g['snapshot_utc']} UTC)\n"
            f"2. Follow {HANDLE}\n3. Reply to this post with your wallet address\n\n"
            f"Winners come from the hash of Robinhood Chain block (snapshot block + {g['seed_offset']}). "
            f"Nobody can know it in advance. Anyone can verify the draw: script and results on the site.")


def results_text(g, winners):
    return (f"CATJESUS giveaway results: {len(winners)} winners, {g['prize']:,} CATJESUS each.\n\n"
            + "\n".join(w[:8] + "…" + w[-4:] for w in winners)
            + f"\n\nSeed: block {g['seed_block']} hash {g['seed_hash'][:14]}…\nSnapshot: block {g['snapshot_block']}, {g['eligible']} eligible wallets. Prizes sent, tx hashes on the site.")


def cmd_announce(a):
    ts = int(datetime.datetime.strptime(a.snapshot, "%Y-%m-%d %H:%M").replace(tzinfo=datetime.timezone.utc).timestamp())
    g = {"status": "open", "created": time.time(), "snapshot_ts": ts, "snapshot_utc": a.snapshot, "winners": a.winners,
         "prize": a.prize, "min_hold": a.min_hold, "seed_offset": a.seed_offset, "total": a.winners * a.prize, "wallet": GIVEAWAY_WALLET}
    g["announce_text"] = announce_text(g)
    save("giveaway.json", g); to_box("giveaway.json")
    print(announce_text(g))
    print(f"\n[giveaway.json written; fund {GIVEAWAY_WALLET} with {g['total']:,} CATJESUS before `pay`]")


def cmd_snapshot(a):
    g = load("giveaway.json"); assert g, "announce first"
    n = block_at(g["snapshot_ts"])
    assert n, "snapshot time not reached yet"
    bal = balances_at(n)
    elig = sorted(addr for addr, v in bal.items() if addr not in SYSTEM and v >= g["min_hold"] * 10**18)
    g.update({"snapshot_block": n, "eligible": len(elig), "status": "snapshot"})
    save("eligible.json", {"block": n, "min_hold": g["min_hold"], "wallets": elig, "balances": {x: bal[x] / 1e18 for x in elig}})
    save("giveaway.json", g); to_box("giveaway.json")
    print(f"snapshot block {n}: {len(elig)} eligible wallets (>= {g['min_hold']:,} CATJESUS)")


def pick(seed_hash, pool, k):
    pool = sorted(set(pool)); out = []; i = 0
    while pool and len(out) < k:
        h = int(hashlib.sha256(bytes.fromhex(seed_hash[2:]) + i.to_bytes(4, "big")).hexdigest(), 16)
        out.append(pool.pop(h % len(pool))); i += 1
    return out


def cmd_draw(a):
    g = load("giveaway.json"); e = load("eligible.json"); assert g and e and g.get("snapshot_block"), "snapshot first"
    seed_block = g["snapshot_block"] + g["seed_offset"]
    assert latest() >= seed_block + 5, f"seed block {seed_block} not final yet"
    _, seed_hash = block(seed_block)
    pool = e["wallets"]
    if a.entrants:
        ent = {ln.strip().lower() for ln in open(a.entrants) if ln.strip().lower().startswith("0x")}
        pool = [w for w in pool if w in ent]; g["entrants"] = len(ent)
    winners = pick(seed_hash, pool, g["winners"])
    g.update({"seed_block": seed_block, "seed_hash": seed_hash, "pool": len(pool), "status": "drawn"})
    g["results_text"] = results_text(g, winners)
    save("winners.json", {"winners": winners, "prize": g["prize"], "seed_block": seed_block, "seed_hash": seed_hash, "paid": {}})
    save("giveaway.json", g); to_box("giveaway.json"); to_box("winners.json")
    print(f"seed block {seed_block} {seed_hash}\npool {len(pool)} -> {len(winners)} winners:")
    for w in winners: print(" ", w)
    print("\n" + results_text(g, winners))


def cmd_pay(a):
    g = load("giveaway.json"); w = load("winners.json"); assert w, "draw first"
    env = {}
    for ln in open(os.path.join(HERE, ".env")):
        if "=" in ln and not ln.startswith("#"):
            k, v = ln.strip().split("=", 1); env[k] = v
    pk = env["PRIVATE_KEY"]; rpc_url = RPCS[2]
    cast = os.path.expanduser("~/.foundry/bin/cast")
    amt = str(g["prize"] * 10**18)
    for win in w["winners"]:
        if win in w["paid"]:
            continue
        r = subprocess.run([cast, "send", TOKEN_CS, "transfer(address,uint256)", win, amt, "--private-key", pk, "--rpc-url", rpc_url, "--json"], capture_output=True, text=True)
        if r.returncode != 0:
            print("FAILED", win, r.stderr[:200]); break
        tx = json.loads(r.stdout)["transactionHash"]; w["paid"][win] = tx; save("winners.json", w)
        print("paid", win, tx)
    g["status"] = "paid" if len(w["paid"]) == len(w["winners"]) else "paying"
    save("giveaway.json", g); to_box("giveaway.json"); to_box("winners.json")


def cmd_verify(a):
    g = load("giveaway.json"); w = load("winners.json"); assert g and w
    bal = balances_at(g["snapshot_block"])
    elig = sorted(x for x, v in bal.items() if x not in SYSTEM and v >= g["min_hold"] * 10**18)
    _, seed_hash = block(g["seed_block"])
    pool = elig
    if a.entrants:
        ent = {ln.strip().lower() for ln in open(a.entrants) if ln.strip().lower().startswith("0x")}
        pool = [x for x in elig if x in ent]
    mine = pick(seed_hash, pool, g["winners"])
    print("seed hash matches:", seed_hash == g["seed_hash"]); print("winners match:", mine == w["winners"])


if __name__ == "__main__":
    p = argparse.ArgumentParser(); sp = p.add_subparsers(dest="cmd", required=True)
    s = sp.add_parser("announce"); s.add_argument("--snapshot", required=True, help='UTC "YYYY-MM-DD HH:MM"')
    s.add_argument("--winners", type=int, default=21); s.add_argument("--prize", type=int, default=5000)
    s.add_argument("--min-hold", type=int, default=1000); s.add_argument("--seed-offset", type=int, default=1000)
    sp.add_parser("snapshot")
    s = sp.add_parser("draw"); s.add_argument("--entrants")
    sp.add_parser("pay")
    s = sp.add_parser("verify"); s.add_argument("--entrants")
    a = p.parse_args(); globals()["cmd_" + a.cmd](a)
