#!/usr/bin/env python3
"""PROOF OF FIST - standalone fairness verifier (Python 3.8+, standard library only).

Checks that every house move in a run was fixed *before* you chose yours.

For each throw the server published, before your move:
    commit = sha256("<server_seed>:<round>:<nonce>")
and after your move revealed server_seed. The house move is:
    first byte b < 255 of HMAC-SHA256(key=bytes.fromhex(server_seed),
                                      msg="<client_seed>:<round>:<nonce>:<i>"), i = 0, 1, ...
    move = b % 3   (0 = rock, 1 = paper, 2 = scissors)

Usage:
    python verify_fist.py --url http://localhost:8000/api/runs/<run_id>/proof

Daily-challenge runs also prove that every throw seed is HMAC-SHA256(daily_seed,
"<player_address>:<nonce>") and that sha256("daily:<date>:<daily_seed>") matches the commitment
published when the day began (checkable once the day is over).
    python verify_fist.py --file proof.json
    python verify_fist.py --commit C --server-seed S --client-seed X \
                          --round 3 --nonce 4 [--move rock]
"""

from __future__ import annotations

import argparse
import hashlib
import hmac
import json
import sys
import urllib.request
from typing import Any

MOVES = ("rock", "paper", "scissors")


def commitment(server_seed: str, round_: int, nonce: int) -> str:
    return hashlib.sha256(f"{server_seed}:{round_}:{nonce}".encode()).hexdigest()


def house_move(server_seed: str, client_seed: str, round_: int, nonce: int) -> str:
    key = bytes.fromhex(server_seed)
    counter = 0
    while True:
        msg = f"{client_seed}:{round_}:{nonce}:{counter}".encode()
        for byte in hmac.new(key, msg, hashlib.sha256).digest():
            if byte < 255:
                return MOVES[byte % 3]
        counter += 1


def daily_commitment(date: str, daily_seed: str) -> str:
    return hashlib.sha256(f"daily:{date}:{daily_seed}".encode()).hexdigest()


def daily_throw_seed(daily_seed: str, address: str, nonce: int) -> str:
    msg = f"{address}:{nonce}".encode()
    return hmac.new(bytes.fromhex(daily_seed), msg, hashlib.sha256).hexdigest()


def check_daily(proof: dict[str, Any]) -> bool:
    """Daily runs: every throw seed must derive from the day's committed seed."""
    daily = proof["daily"]
    if not daily.get("seed"):
        print(f"  [WAIT] daily {daily['date']}: seed is revealed after the day ends (UTC)")
        return True
    ok: bool = daily_commitment(daily["date"], daily["seed"]) == daily["commit"]
    print(f"  [{'OK  ' if ok else 'FAIL'}] daily {daily['date']}: seed matches day commitment")
    for throw in proof.get("throws", []):
        if not throw.get("server_seed"):
            continue
        expected = daily_throw_seed(daily["seed"], proof["player"], int(throw["nonce"]))
        if expected != throw["server_seed"]:
            print(f"  [FAIL] nonce {throw['nonce']}: server seed not derived from daily seed")
            ok = False
    return ok


def check_throw(throw: dict[str, Any], client_seed: str) -> tuple[bool, str]:
    seed = throw.get("server_seed")
    if not seed:
        return True, "seed not revealed yet (throw still pending) - skipped"
    round_, nonce = int(throw["round"]), int(throw["nonce"])
    seed_client = throw.get("client_seed") or client_seed
    problems = []
    if commitment(seed, round_, nonce) != str(throw["commit"]).lower():
        problems.append("commit does not match revealed seed")
    expected = house_move(seed, seed_client, round_, nonce)
    claimed = throw.get("house_move")
    if claimed is not None and claimed != expected:
        problems.append(f"house move should be {expected}, server said {claimed}")
    if problems:
        return False, "; ".join(problems)
    return True, f"house played {expected}" + (" (unused)" if claimed is None else "")


def verify_proof(proof: dict[str, Any]) -> bool:
    client_seed = proof.get("client_seed", "")
    print(f"Run {proof.get('run_id', '?')}  client_seed={client_seed}")
    all_ok = True
    for throw in proof.get("throws", []):
        ok, msg = check_throw(throw, client_seed)
        all_ok &= ok
        mark = "OK  " if ok else "FAIL"
        print(f"  [{mark}] round {throw['round']:>2} nonce {throw['nonce']:>3}: {msg}")
    if proof.get("daily"):
        all_ok &= check_daily(proof)
    print("RESULT:", "all throws verified" if all_ok else "VERIFICATION FAILED")
    return all_ok


def main(argv: list[str] | None = None) -> int:
    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
    p.add_argument("--url", help="proof URL, e.g. https://host/api/runs/<id>/proof")
    p.add_argument("--file", help="path to a saved proof JSON")
    p.add_argument("--commit")
    p.add_argument("--server-seed")
    p.add_argument("--client-seed", default="")
    p.add_argument("--round", type=int)
    p.add_argument("--nonce", type=int)
    p.add_argument("--move", choices=MOVES, help="house move the server claimed")
    a = p.parse_args(argv)

    if a.url or a.file:
        if a.url:
            with urllib.request.urlopen(a.url, timeout=15) as resp:
                proof = json.load(resp)
        else:
            with open(a.file, encoding="utf-8") as fh:
                proof = json.load(fh)
        return 0 if verify_proof(proof) else 1

    if not (a.commit and a.server_seed and a.round is not None and a.nonce is not None):
        p.error("give --url, --file, or all of --commit --server-seed --round --nonce")
    throw = {
        "commit": a.commit,
        "server_seed": a.server_seed,
        "round": a.round,
        "nonce": a.nonce,
        "house_move": a.move,
    }
    ok, msg = check_throw(throw, a.client_seed)
    print(("OK: " if ok else "FAIL: ") + msg)
    return 0 if ok else 1


if __name__ == "__main__":
    sys.exit(main())
