#!/usr/bin/env python3
"""Standalone verifier for CAIN-42 hourly operational proofs. No CAIN imports.
Needs Python 3.8+, `cryptography`, and verify_pbft_qc_bundle.py in the same directory.

    python3 verify_hourly_proofs.py https://clawx.click/evidence/hourly-proof/
    python3 verify_hourly_proofs.py BASE --pin <evidence-root public key b64>

For every proof in the index: proof_hash recomputed from canonical JSON;
Ed25519 signature by the evidence-root key (pinned from the three sites'
published key unless --pin is given); the hash chain (each proof names the
previous one's hash); and for every proof whose verdict is OPERATIONAL, the
embedded COMMIT and PREPARE quorum certificates are re-verified against the
membership (distinct members, quorum, signatures) and the replicas it lists
agree. Final verdict:
  PROVEN OPERATIONAL (last proof N min ago)   -- chain valid, latest OPERATIONAL, < 1 h old (proofs run every 30 min)
  NOT PROVEN FOR X h                           -- chain valid but stale or latest not operational
  FAILED                                       -- any proof does not verify
"""
from __future__ import annotations

import datetime
import hashlib
import json
import os
import sys
import urllib.request

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import verify_pbft_qc_bundle as Q  # noqa: E402

DOMAIN = "CAIN42/HOURLY-PROOF/v1"
KEY_URLS = ["https://cainstudio.online/proof/bundle/claims/evidence-root.pub.json",
            "https://mcpgate.online/proof/bundle/claims/evidence-root.pub.json",
            "https://clawx.click/evidence/claims/evidence-root.pub.json"]


def get(u):
    with urllib.request.urlopen(u, timeout=30) as r:
        return json.loads(r.read())


def canon(o) -> bytes:
    return json.dumps(o, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()


def main(argv):
    if len(argv) < 2:
        print(__doc__); return 2
    base = argv[1].rstrip("/") + "/"
    pin = argv[argv.index("--pin") + 1] if "--pin" in argv else None
    problems = []
    if pin is None:
        keys = set()
        for u in KEY_URLS:
            try:
                keys.add(get(u)["public_key_b64"])
            except Exception as e:  # noqa: BLE001
                problems.append(f"evidence-root key unreachable at {u}: {e}")
        if len(keys) != 1:
            problems.append(f"evidence-root key differs across sites: {keys}")
        pin = next(iter(keys)) if keys else None
    idx = get(base + "index.json")
    mb = Q.Membership(idx["membership"])
    if mb.configuration_hash != idx["configuration_hash"]:
        problems.append("membership does not recompute to the stated configuration hash")
    prev = hashlib.sha256(canon({"domain": DOMAIN, "genesis": idx["cluster_id"]})).hexdigest()
    operational = 0
    last = None
    for i, e in enumerate(idx["proofs"]):
        p = get(base + e["file"])
        b = p["body"]
        d = hashlib.sha256(canon(b)).hexdigest()
        bad = []
        if d != p["proof_hash"]:
            bad.append("proof_hash does not match content")
        if not Q.ed25519_ok(pin, p["evidence_root_signature_b64"], p["proof_hash"].encode()):
            bad.append("evidence-root signature invalid")
        if b["n"] != i or b["previous_proof_hash"] != prev:
            bad.append("hash chain broken")
        if b["configuration_hash"] != mb.configuration_hash:
            bad.append("wrong membership")
        if b["verdict"] == "OPERATIONAL":
            w = b["write"]
            for k in ("commit_qc", "prepare_qc"):
                ok, why = Q.verify_qc(w[k], mb)
                if not ok:
                    bad.append(f"{k}: {why}")
            if w["commit_qc"]["sequence"] != w["sequence"]:
                bad.append("certificate is for another sequence")
            up = [r for r in b["replicas"].values() if r.get("reachable")]
            by_h = {}
            for r in up:
                by_h.setdefault(r["commit_index"], set()).add(r["application_state_hash"])
            if len(up) < 3 or any(len(v) > 1 for v in by_h.values()):
                bad.append("stated OPERATIONAL but replicas do not agree")
            operational += 1
        print(f"[{'VALID' if not bad else 'INVALID'}] {e['file']}  {b['at']}  {b['verdict']}"
              + (f"  seq {b['write'].get('sequence')}" if b['write'].get('committed') else "")
              + ("".join(f"\n    - {x}" for x in bad)))
        problems += [f"{e['file']}: {x}" for x in bad]
        prev = p["proof_hash"]
        last = b
    if problems or last is None:
        print(f"\nFAILED: {len(problems)} problem(s)" if problems else "\nFAILED: no proofs")
        return 1
    age = (datetime.datetime.now(datetime.timezone.utc) -
           datetime.datetime.strptime(last["at"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc)).total_seconds()
    if last["verdict"] == "OPERATIONAL" and age < 3600:
        print(f"\nPROVEN OPERATIONAL (last proof {age / 60:.0f} min ago; {operational}/{len(idx['proofs'])} proofs operational; chain valid)")
        return 0
    print(f"\nNOT PROVEN FOR {age / 3600:.1f} h (latest verdict {last['verdict']}; chain valid)")
    return 3


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