#!/usr/bin/env python3
"""hb_work — keyless HASHBATTLE mining helper (stdlib only, no keys, no deps).

Fetches the live work package from the public chain, builds the enlist
calldata, and verifies a mined proof locally BEFORE you spend gas.

This tool never holds or asks for a private key. Submission is done by
YOU, with YOUR wallet tooling (browser wallet, cast --interactive, etc.).

Chain target — NO DEFAULT CONTRACT SHIPS WITH THIS KIT. Point it at a
live deployment either way:
  1. chain.json next to this script:
     {"rpc": "https://your-rpc", "contract": "0x...", "chainId": 46630}
  2. or pass --rpc / --contract (and optionally --chain) per command.
The production contract addresses will be published on the official site
and dropped into chain.json when the mainnet deployment goes live.

Commands:
  work     fetch anchor/prev/target/price for a miner address
  calldata build mine(nonce, anchorBlock) calldata hex
  verify   recompute keccak256(preimage) and check < target
  price    print the live enlistPrice in wei and ETH

Examples:
  python3 hb_work.py work --miner 0xYourWallet
  python3 hb_work.py work --rpc https://YOUR_RPC --contract 0xYour --miner 0x..
  python3 hb_work.py calldata --nonce 123456789 --anchor-block 126331234
  python3 hb_work.py verify --miner 0x.. --prev 0x.. --anchor 0x.. \
      --target 0x.. --nonce 123 --hash 0x..
"""
from __future__ import annotations

import argparse
import json
import os
import sys
import urllib.request

CHAIN_CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                 "chain.json")


def load_chain_config():
    """chain.json (if present) supplies rpc/contract/chainId defaults."""
    if os.path.exists(CHAIN_CONFIG_PATH):
        try:
            with open(CHAIN_CONFIG_PATH, "r") as f:
                cfg = json.load(f)
        except Exception as e:
            raise SystemExit(f"chain.json unreadable: {e}")
        if not isinstance(cfg, dict):
            raise SystemExit("chain.json must be a JSON object")
        return cfg
    return {}

SEL = {
    "prev": "a4da5da2",            # prevWork()
    "challengeBlock": "cefc2977",  # challengeBlock()
    "challengeAnchor": "b5da0777", # challengeAnchor()
    "target": "16ccc8c0",          # targetFor(address)
    "price": "9eb5ee97",           # enlistPrice()
    "mine": "071e9503",            # mine(uint256,uint256) payable
}


# ---------------------------------------------------------------- hash kernel
# All keccak-256 in this kit is computed by the bundled C kernel
# (keccak256_miner), which is byte-verified against Ethereum keccak vectors.
# No hand-rolled python hash ships here.
import subprocess

_MINER_BIN = os.environ.get("HB_MINER_BIN",
                            os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                         "keccak256_miner"))


def c_keccak_list(hexlines):
    """keccak256 of each hex preimage, via the bundled C kernel."""
    if not os.path.exists(_MINER_BIN):
        raise SystemExit(
            f"hash kernel not built: {_MINER_BIN}\n"
            "build it first: gcc -O2 -o keccak256_miner keccak256_miner.c")
    inp = "\n".join(hexlines) + "\n"
    r = subprocess.run([_MINER_BIN, "--test"], input=inp,
                       capture_output=True, text=True, timeout=300)
    if r.returncode != 0:
        raise SystemExit(f"hash kernel failed: {r.stderr.strip()}")
    out = r.stdout.strip().splitlines()
    if len(out) != len(hexlines):
        raise SystemExit("hash kernel I/O mismatch")
    return out


def keccak256_hex(preimage_hex: str) -> str:
    return c_keccak_list([preimage_hex])[0]


# ---------------------------------------------------------------- rpc
def rpc(url, method, params):
    body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode()
    req = urllib.request.Request(
        url, data=body,
        headers={"Content-Type": "application/json",
                 # the testnet RPC rejects the default Python-urllib UA; an
                 # explicit product UA is required (verified live).
                 "User-Agent": "hb-miner-kit/1.0"})
    with urllib.request.urlopen(req, timeout=20) as resp:
        out = json.load(resp)
    if "error" in out:
        raise RuntimeError(f"{method}: {out['error']}")
    return out["result"]


def call(url, contract, selector_with_args):
    raw = rpc(url, "eth_call", [{"to": contract, "data": "0x" + selector_with_args}, "latest"])
    return raw or "0x"


def pad32(hexaddr):
    return hexaddr.lower().replace("0x", "").rjust(64, "0")


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


# ---------------------------------------------------------------- commands
def cmd_work(a):
    chain = u256(rpc(a.rpc, "eth_chainId", []))
    if a.chain is not None and chain != a.chain:
        raise SystemExit(f"rpc chain {chain} != expected {a.chain}")
    # The canonical anchor comes from the contract's own views, exactly like the
    # web miner's getWorkPackage(). On an ArbSys L2 the anchor hash is
    # arbBlockHash(n), which differs from eth_getBlockByNumber().hash — so the
    # challengeAnchor view is authoritative and must NOT be cross-checked against
    # the L1-style RPC header. mine() re-derives _l2BlockHash(anchorBlock) itself.
    blockTag = rpc(a.rpc, "eth_blockNumber", [])
    prev = u256(call(a.rpc, a.contract, SEL["prev"]))
    cblock = u256(call(a.rpc, a.contract, SEL["challengeBlock"]))
    cancel = call(a.rpc, a.contract, SEL["challengeAnchor"])
    target = u256(call(a.rpc, a.contract, SEL["target"] + pad32(a.miner)))
    price = u256(call(a.rpc, a.contract, SEL["price"]))
    if not cblock or not u256(cancel):
        raise SystemExit("canonical mining anchor unavailable")
    bits = 256 - bin(target).__len__() + 2 if target else 0
    print(json.dumps({
        "chainId": chain,
        "contract": a.contract,
        "miner": a.miner,
        "blockNumber": u256(blockTag),
        "anchorBlock": cblock,
        "anchorHash": cancel,
        "prev": prev,
        "prevHex": "0x" + format(prev, "064x"),
        "target": target,
        "targetHex": "0x" + format(target, "064x"),
        "targetBits": bits,
        "priceWei": price,
        "priceEth": price / 1e18,
        "mineSelector": SEL["mine"],
    }, indent=2))


def cmd_calldata(a):
    data = "0x" + SEL["mine"] + format(a.nonce, "064x") + format(a.anchor_block, "064x")
    print(data)


def _as32(v, name):
    if isinstance(v, bytes):
        b = v
    elif isinstance(v, int):
        b = v.to_bytes(32, "big")
    else:
        b = bytes.fromhex(str(v).replace("0x", ""))
    if len(b) != 32:
        raise SystemExit(f"{name} must be 32 bytes")
    return b


def make_preimage(miner, nonce, prev, anchor):
    msg = bytearray(116)
    m = bytes.fromhex(miner.lower().replace("0x", ""))
    if len(m) != 20:
        raise SystemExit("miner must be 20 bytes")
    msg[0:20] = m
    msg[20:52] = int(nonce).to_bytes(32, "big")
    msg[52:84] = _as32(prev, "prev")
    msg[84:116] = _as32(anchor, "anchor")
    return bytes(msg)


def cmd_verify(a):
    pre = make_preimage(a.miner, a.nonce, a.prev, a.anchor)
    dhex = keccak256_hex(pre.hex())
    ok_hash = a.hash and a.hash.replace("0x", "").lower() == dhex
    d = int(dhex, 16)
    t = int(a.target, 16) if isinstance(a.target, str) else a.target
    under = d < t
    print(json.dumps({
        "keccak256": "0x" + dhex,
        "matchesClaimedHash": bool(ok_hash) if a.hash else None,
        "workUnderTarget": under,
        "targetBits": 256 - bin(t).__len__() + 2 if t else 0,
    }, indent=2))
    if a.hash and not ok_hash:
        raise SystemExit("VERIFICATION FAILED: recomputed hash != claimed hash")
    if not under:
        raise SystemExit("VERIFICATION FAILED: work >= target — proof would revert")


def cmd_price(a):
    price = u256(call(a.rpc, a.contract, SEL["price"]))
    print(json.dumps({"priceWei": price, "priceEth": price / 1e18}, indent=2))


def main():
    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = p.add_subparsers(dest="cmd", required=True)

    w = sub.add_parser("work")
    w.add_argument("--rpc", default=None)
    w.add_argument("--contract", default=None)
    w.add_argument("--miner", required=True)
    w.add_argument("--chain", type=int, default=None,
                   help="expected chain id (default: from chain.json; skip check if absent)")
    w.set_defaults(fn=cmd_work)

    c = sub.add_parser("calldata")
    c.add_argument("--nonce", type=int, required=True)
    c.add_argument("--anchor-block", type=int, required=True)
    c.set_defaults(fn=cmd_calldata)

    v = sub.add_parser("verify")
    v.add_argument("--miner", required=True)
    v.add_argument("--prev", required=True)
    v.add_argument("--anchor", required=True)
    v.add_argument("--target", required=True)
    v.add_argument("--nonce", type=int, required=True)
    v.add_argument("--hash", default=None)
    v.set_defaults(fn=cmd_verify)

    pr = sub.add_parser("price")
    pr.add_argument("--rpc", default=None)
    pr.add_argument("--contract", default=None)
    pr.set_defaults(fn=cmd_price)

    a = p.parse_args()
    # no default contract ships with the kit: resolve rpc/contract/chain from
    # chain.json (or CLI flags) and fail loudly with instructions if missing.
    cfg = load_chain_config()
    for attr, key in (("rpc", "rpc"), ("contract", "contract"), ("chain", "chainId")):
        if getattr(a, attr, None) is None and key in cfg:
            setattr(a, attr, cfg[key])
    if a.cmd in ("work", "price"):
        missing = [f"--{n}" for n in ("rpc", "contract") if getattr(a, n, None) is None]
        if missing:
            raise SystemExit(
                "no chain target configured. Either:\n"
                f"  1. create {CHAIN_CONFIG_PATH} with:\n"
                '     {"rpc": "https://your-rpc", "contract": "0x...", "chainId": 46630}\n'
                f"  2. or pass {', '.join(missing)} on the command line.\n"
                "Production addresses will be published on the official site when "
                "the mainnet deployment goes live.")
    a.fn(a)


if __name__ == "__main__":
    main()
