#!/usr/bin/env python3
"""
CarCheckHQ Giveaway #1 draw script (CarCheckHQ, 23 Oct 2026).
Verifiably random draw for CAP Code rule 8.24. Anyone can rerun it and get the same winners.

Inputs
  entries.csv  one row per VALID entry after de-duplication, column "entry_id" only
               (random IDs such as "E-7f3a9c", never names or emails). Order does not matter:
               the script sorts IDs so the result can't depend on export order.
Steps
  1. Before the draw, publish SHA-256 of the sorted entry list and the count (no personal data).
  2. Seed = the drand "quicknet" public randomness beacon round fixed in the T&Cs
     (the first round at or after the draw time). Nobody at CarCheckHQ can know or influence it in advance.
  3. Winners and the reserve list are picked by a seeded Fisher-Yates shuffle on SHA-256 output.
Usage
  python3 draw.py entries.csv --draw-time 2026-11-16T12:00:00Z --winners 10 --reserves 10
  python3 draw.py entries.csv --round 12345678 --randomness <hex>   (offline re-check)
"""
import argparse, csv, hashlib, json, sys, urllib.request
from datetime import datetime, timezone

GENESIS = 1692803367   # drand quicknet genesis_time (api.drand.sh/v2/beacons/quicknet/info, read 23 Oct 2026)
PERIOD = 3             # seconds per round

def round_at(ts):
    return max(1, -(-(ts - GENESIS) // PERIOD) + 1)   # first round at or after ts

def fetch_round(r):
    with urllib.request.urlopen(f"https://api.drand.sh/v2/beacons/quicknet/rounds/{r}", timeout=20) as f:
        sig = json.load(f)["signature"]
    return hashlib.sha256(bytes.fromhex(sig)).hexdigest()   # quicknet randomness = sha256(signature)

def rand_below(n, seed, counter):
    # unbiased: rejection sampling on 256-bit SHA-256 output
    limit = (2**256 // n) * n
    while True:
        x = int(hashlib.sha256(f"{seed}:{counter[0]}".encode()).hexdigest(), 16)
        counter[0] += 1
        if x < limit:
            return x % n

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("entries"); ap.add_argument("--draw-time"); ap.add_argument("--round", type=int)
    ap.add_argument("--randomness"); ap.add_argument("--winners", type=int, required=True)
    ap.add_argument("--reserves", type=int, default=10)
    a = ap.parse_args()
    ids = sorted({row["entry_id"].strip() for row in csv.DictReader(open(a.entries)) if row["entry_id"].strip()})
    list_hash = hashlib.sha256("\n".join(ids).encode()).hexdigest()
    if a.round is None:
        ts = int(datetime.fromisoformat(a.draw_time.replace("Z", "+00:00")).timestamp())
        a.round = round_at(ts)
    rnd = a.randomness or fetch_round(a.round)
    seed = hashlib.sha256(f"{rnd}:{list_hash}".encode()).hexdigest()
    order, counter = ids[:], [0]
    for i in range(len(order) - 1, 0, -1):        # Fisher-Yates
        j = rand_below(i + 1, seed, counter)
        order[i], order[j] = order[j], order[i]
    k = min(a.winners, len(order))
    out = {"entries": len(ids), "entry_list_sha256": list_hash, "drand_beacon": "quicknet",
           "drand_round": a.round, "drand_randomness": rnd, "seed": seed,
           "winners": order[:k], "reserves_in_order": order[k:k + a.reserves]}
    json.dump(out, sys.stdout, indent=1); print()

if __name__ == "__main__":
    main()
