#!/usr/bin/env python3
"""
vordium-register — Vordium validator onboarding CLI (chain 713714).

Two subcommands, matching the gated flow (apply -> approve -> download -> register -> seed -> join):

  apply     Submit your APPLICATION with IDENTITY ONLY — operator address, name,
            endpoint (+ optional email). NO node keys, NO bond, NO signature.
            (VRF/ECDSA are NOT needed to apply — they come after you are approved
            and have downloaded + booted the node.)

  register  After approval + download + first boot: submit your VRF (+ ECDSA + endpoint),
            SIGNED with your operator key, to /api/validators/register. The signature
            is a standard EIP-191 personal_sign that must recover to your operator
            (candidate) address. Your private key never leaves this box.

Install:  pip install requests eth-account

Examples:
  # 1) apply (before you have the binary) — identity only
  ./vordium-register.py apply --address 0x<40hex> --name mynode --endpoint 203.0.113.9:9000 --email you@x.io

  # 2) register (after approval + download + first boot) — signs with your operator key
  ./vordium-register.py register --key-file /root/operator.key         # auto: vrf from boot log, endpoint from config/IP
  ./vordium-register.py register --key-file /root/operator.key --vrf 0x<64hex> --endpoint 203.0.113.9:9000 --name mynode
  ./vordium-register.py register --key-file /root/operator.key --dry-run   # build + print the signed payload, do NOT submit
"""
import argparse, json, os, re, sys
try:
    import requests
except ImportError:
    sys.exit("missing dep — run: pip install requests")

CHAIN_ID   = 713714
BASE       = "https://vordscan.io"
APPLY_API  = BASE + "/api/validators/apply"
REGISTER_API = BASE + "/api/validators/register"
STATUS_API = BASE + "/api/onboarding/status"
STATUS_ADDR_API = BASE + "/api/onboarding/status/"   # + 0xaddr
DEFAULT_BOND_WEI = 1_000_000 * (10 ** 18)            # 1,000,000 VORD self-bond
BOOTLOGS = ["/root/vordium.log", "/root/vordium-bft.log", "/root/vordium-boot.log"]
ENV_PATH = "/root/vordium-bft.env"

# The EXACT message the chain verifies (SignedRegisterValidator::signing_digest,
# EIP-191 personal_sign). Field order and labels are load-bearing — do not reorder.
REGISTER_MSG_TEMPLATE = (
    "Vordium Admin\n"
    "Chain: {chain}\n"
    "Action: Register Validator\n"
    "Candidate: 0x{cand}\n"
    "Vrf: 0x{vrf}\n"
    "Ecdsa: 0x{ecdsa}\n"
    "Endpoint: {ep}\n"
    "Bond: {bond}\n"
    "Nonce: {nonce}"
)


def die(msg, code=1):
    print("error:", msg, file=sys.stderr); sys.exit(code)


def name_ok(n):
    return bool(re.fullmatch(r'[A-Za-z0-9_-]{1,32}', n or ""))


def endpoint_ok(e):
    return bool(re.fullmatch(r'[^:]+:\d{1,5}', e or "")) and 1 <= int(e.rsplit(":", 1)[1]) <= 65535


def read_env(path):
    env = {}
    try:
        for line in open(path, errors="replace"):
            s = line.strip()
            if s and not s.startswith("#") and "=" in s:
                k, v = s.split("=", 1); env[k.strip()] = v.strip()
    except Exception:
        pass
    return env


def scan_bootlog(pattern, group=1):
    rx = re.compile(pattern); found = None
    for p in BOOTLOGS:
        if not os.path.exists(p):
            continue
        try:
            for line in open(p, errors="replace"):
                m = rx.search(line)
                if m:
                    found = m.group(group)
        except Exception:
            continue
    return found


def gate_open():
    try:
        return bool(requests.get(STATUS_API, timeout=8).json().get("accepting"))
    except Exception:
        return False


def auto_endpoint(env, address):
    vset = [a.strip().lower() for a in env.get("BFT_VALIDATOR_SET", "").split(",") if a.strip()]
    veps = [e.strip() for e in env.get("BFT_VALIDATOR_ENDPOINTS", "").split(",") if e.strip()]
    if vset and veps and len(vset) == len(veps) and address in vset:
        return veps[vset.index(address)]
    try:
        ip = requests.get("https://api.ipify.org", timeout=6).text.strip()
        if re.fullmatch(r'\d{1,3}(\.\d{1,3}){3}', ip):
            return ip + ":9000"
    except Exception:
        pass
    return None


# ───────────────────────── apply ─────────────────────────
def cmd_apply(args):
    address = (args.address or "").lower()
    if not re.fullmatch(r'0x[0-9a-f]{40}', address):
        die("apply needs a valid --address 0x<40hex>")
    if not name_ok(args.name):
        die("name must be one word [A-Za-z0-9_-], 1-32 chars")
    if not endpoint_ok(args.endpoint):
        die("endpoint must be host:port (port 1-65535)")
    body = {"address": address, "name": args.name, "endpoint": args.endpoint}
    if args.email:
        body["email"] = args.email
    print("--- application (identity only; no keys, no signature) ---")
    print(json.dumps(body, indent=2))
    if args.dry_run:
        print("dry-run: not submitted."); return
    if not gate_open():
        die("validator applications are currently CLOSED. Try again when the operator opens them.", 2)
    r = requests.post(APPLY_API, json=body, timeout=15)
    print("HTTP", r.status_code)
    try: print(json.dumps(r.json(), indent=2))
    except Exception: print(r.text)
    if not r.ok:
        sys.exit(4)
    print("\napplication submitted. Track it:")
    print("  curl -s %s%s" % (STATUS_ADDR_API, address))
    print("When status=approved, the operator sends you a one-time download link; then run: register")


# ───────────────────────── register ─────────────────────────
def cmd_register(args):
    try:
        from eth_account import Account
        from eth_account.messages import encode_defunct
    except ImportError:
        die("register needs signing — run: pip install eth-account")
    try:
        raw = open(args.key_file, errors="replace").read().strip()
    except Exception as e:
        die("cannot read operator key file %s: %s" % (args.key_file, e))
    raw = raw[2:] if raw.startswith("0x") else raw
    try:
        acct = Account.from_key(bytes.fromhex(raw))
    except Exception as e:
        die("bad operator key in %s: %s" % (args.key_file, e))
    address = acct.address.lower()

    # ECDSA compressed pubkey (33B) derived locally from the operator key
    try:
        from eth_keys import keys as ek
        ecdsa = ek.PrivateKey(bytes.fromhex(raw)).public_key.to_compressed_bytes().hex()
    except ImportError:
        die("register needs eth-keys for the ECDSA pubkey — run: pip install eth-keys")

    env = read_env(args.env)
    vrf = (args.vrf or scan_bootlog(r'VRF keypair LOADED.*vrf_pubkey=([0-9a-fA-F]{64})') or "").lower().replace("0x", "")
    if not re.fullmatch(r'[0-9a-f]{64}', vrf):
        die("could not get VRF pubkey — boot the node first, or pass --vrf <64hex>")
    endpoint = args.endpoint or auto_endpoint(env, address)
    if not endpoint_ok(endpoint or ""):
        die("could not determine endpoint — pass --endpoint host:9000")
    name = args.name or scan_bootlog(r'name=([A-Za-z0-9_-]{1,32})') or "node"
    if not name_ok(name):
        die("name must be one word [A-Za-z0-9_-], 1-32 chars — pass --name")
    bond = int(args.bond) if args.bond else DEFAULT_BOND_WEI
    nonce = int(args.nonce)

    msg = REGISTER_MSG_TEMPLATE.format(chain=CHAIN_ID, cand=address[2:], vrf=vrf, ecdsa=ecdsa,
                                       ep=endpoint, bond=bond, nonce=nonce)
    signed = Account.sign_message(encode_defunct(text=msg), private_key=bytes.fromhex(raw))
    sig64 = signed.signature[:64]  # chain uses r||s (64B); it recovers the v itself
    body = {
        "op": "RegisterValidator",
        "candidate": address,
        "vrf_pubkey": "0x" + vrf,
        "ecdsa_pubkey": "0x" + ecdsa,
        "name": name,
        "endpoint": endpoint,
        "bond": str(bond),
        "nonce": nonce,
        "signature": "0x" + sig64.hex(),
    }
    print("--- signed message (EIP-191 personal_sign; recovers to your operator address) ---")
    print(msg)
    print("--- RegisterValidator payload ---")
    print(json.dumps(body, indent=2))
    if args.dry_run:
        print("dry-run: not submitted."); return
    if not gate_open():
        die("validator onboarding is currently CLOSED.", 2)
    r = requests.post(REGISTER_API, json=body, timeout=20)
    print("HTTP", r.status_code)
    try: print(json.dumps(r.json(), indent=2))
    except Exception: print(r.text)
    if not r.ok:
        sys.exit(4)
    print("\nregister submitted. At the next epoch boundary an approved+bonded candidate joins the active set.")


def main():
    ap = argparse.ArgumentParser(description="Vordium validator onboarding (apply + register).")
    sub = ap.add_subparsers(dest="cmd", required=True)

    a = sub.add_parser("apply", help="submit an identity-only application (no keys)")
    a.add_argument("--address", required=True, help="operator address 0x<40hex>")
    a.add_argument("--name", required=True, help="node name, one word [A-Za-z0-9_-], 1-32")
    a.add_argument("--endpoint", required=True, help="host:port peers dial, e.g. 203.0.113.9:9000")
    a.add_argument("--email", help="contact email (optional)")
    a.add_argument("--dry-run", action="store_true")
    a.set_defaults(func=cmd_apply)

    r = sub.add_parser("register", help="submit signed VRF+endpoint after boot")
    r.add_argument("--key-file", required=True, help="operator key file (signs locally; never sent)")
    r.add_argument("--vrf", help="VRF pubkey 64 hex (else auto from boot log)")
    r.add_argument("--endpoint", help="host:port (else auto from BFT config / public IP)")
    r.add_argument("--name", help="node name (else auto from boot log / 'node')")
    r.add_argument("--bond", help="self-bond in wei (default 1,000,000 VORD). Ignore if the admin seeds your bond.")
    r.add_argument("--nonce", default="0", help="replay nonce (default 0 for first registration)")
    r.add_argument("--env", default=ENV_PATH, help="BFT env path (default %s)" % ENV_PATH)
    r.add_argument("--dry-run", action="store_true")
    r.set_defaults(func=cmd_register)

    args = ap.parse_args()
    args.func(args)


if __name__ == "__main__":
    main()
