#!/usr/bin/env python3
"""Reference verifier — AP2 Attestation Binding Profile 0.1 (draft).

Usage: verify-ap2.py <chain.json>
Deps:  pip install rfc8785 requests

Input is a JSON document with `intent`, `cart` and `payment` mandates, where
the payment mandate carries the `rubric_binding` member (spec section 2).

Exit codes:  0 bound-verified   1 hash-mismatch   2 unbound
             3 unresolvable     4 chain-mismatch   5 anchor-mismatch
"""
import sys, json, hashlib, datetime, requests

TAG_MANDATE = b"\x10"
TAG_CHAIN   = b"\x11"


def jcs(obj) -> bytes:
    import rfc8785
    return rfc8785.dumps(obj)


def mandate_hash(m: dict) -> str:
    h = hashlib.sha3_256()
    h.update(TAG_MANDATE)
    h.update(jcs(m))
    return "sha3-256:" + h.hexdigest()


def chain_hash(i: str, c: str, p: str) -> str:
    """Order-binding: intent, then cart, then payment. Hex bytes, no separators."""
    h = hashlib.sha3_256()
    h.update(TAG_CHAIN)
    for v in (i, c, p):
        h.update(v.split(":", 1)[1].encode("ascii"))
    return "sha3-256:" + h.hexdigest()


def parse_ts(s: str) -> datetime.datetime:
    return datetime.datetime.fromisoformat(s.replace("Z", "+00:00"))


def main(path):
    doc = json.load(open(path))
    payment = doc["payment"]
    rb = payment.get("rubric_binding")
    if not rb:
        print("UNBOUND: payment mandate carries no rubric_binding")
        sys.exit(2)

    # Circularity rule: the payment hash covers the mandate WITHOUT the binding.
    payment_core = {k: v for k, v in payment.items() if k != "rubric_binding"}
    local = {
        "intent":  mandate_hash(doc["intent"]),
        "cart":    mandate_hash(doc["cart"]),
        "payment": mandate_hash(payment_core),
    }

    bound = rb["mandate_chain"]
    bad = [k for k in ("intent", "cart", "payment") if local[k] != bound[k]["hash"]]
    if bad:
        print("HASH-MISMATCH: altered after signing -> " + ", ".join(bad))
        for k in bad:
            print(f"  {k:8s} local {local[k]}\n  {'':8s} bound {bound[k]['hash']}")
        sys.exit(1)

    # Individually intact but the chain does not agree = reordered or recombined.
    # Distinct from hash-mismatch: this is the substituted-cart case.
    local_chain = chain_hash(local["intent"], local["cart"], local["payment"])
    if local_chain != rb["chain_hash"]:
        print("CHAIN-MISMATCH: mandates intact but chain reordered or recombined")
        print(f"  local {local_chain}\n  bound {rb['chain_hash']}")
        sys.exit(4)

    anchor = rb.get("anchor") or {}
    if not anchor.get("hcs_seq"):
        print("UNRESOLVABLE: no ledger anchor (within the 5 min window, retry)")
        sys.exit(3)

    try:
        v = requests.get(rb["verify_url"], timeout=15).json()
    except Exception as e:
        print(f"UNRESOLVABLE: {str(e)[:120]}")
        sys.exit(3)
    if not (v.get("found") is True or v.get("valid") is True):
        print("UNRESOLVABLE: " + json.dumps(v)[:200])
        sys.exit(3)

    # Anchor consistency: a producer controls the binding, so a claimed hcs_seq
    # proves nothing until it agrees with the resolved attestation. Checking that
    # the anchor merely EXISTS would accept any integer here.
    remote_seq = v.get("sequenceNumber")
    if remote_seq is None:
        # The binding claims an anchor the attestation does not yet report. Skipping
        # the comparison here would accept ANY claimed sequence number whenever the
        # attestation is still pending -- a check that cannot fail is not a check.
        print(f"ANCHOR-MISMATCH: binding claims hcs_seq={anchor['hcs_seq']} but the "
              f"attestation reports no sequence yet (status={v.get('status')})")
        sys.exit(5)
    if int(remote_seq) != int(anchor["hcs_seq"]):
        print(f"ANCHOR-MISMATCH: binding claims hcs_seq={anchor['hcs_seq']}, "
              f"attestation resolves to {remote_seq}")
        sys.exit(5)

    # The temporal gap AP2 names. Reported, never judged: what counts as stale
    # authorization is a policy decision this profile deliberately does not make.
    gap = None
    if anchor.get("consensus_timestamp"):
        latest = max(parse_ts(bound[k]["issued_at"]) for k in ("intent", "cart", "payment"))
        gap = (parse_ts(anchor["consensus_timestamp"]) - latest).total_seconds()

    agent = rb.get("agent") or {}
    if agent and agent.get("verified") is not True:
        print("UNRESOLVABLE: agent block present but not verified=true")
        sys.exit(3)

    print(f"BOUND-VERIFIED  {rb['attestation_id']}  hcs_seq={anchor['hcs_seq']}")
    print(f"  chain     {rb['chain_hash']}")
    if agent:
        print(f"  agent     {agent.get('algorithm')} {str(agent.get('publicKey'))[:16]}… verified")
    if gap is not None:
        print(f"  exec gap  {gap:.0f}s between latest mandate and ledger consensus")
    sys.exit(0)


if __name__ == "__main__":
    main(sys.argv[1])
