#!/usr/bin/env python3
"""Verify a CAIN auditor pack offline. Imports nothing from CAIN.

Needs Python 3.9+, the `cryptography` package, and the two standalone verifiers next to this file:
scitt_verify_offline.py and contract_verify_offline.py (all three are published at /proof/bundle/auditor-kit/ on cainstudio.online and mcpgate.online, and
/evidence/auditor-kit/ on clawx.click).

    python3 auditor_pack_verify.py pack.json [--scitt-keys keys.json] [--root-key BASE64] [--previous-checkpoint cp.json]

pack.json            the response of POST /fabric/scitt/auditor-pack
--scitt-keys         notary keys you pinned yourself ({"keys": {key_id: base64}}); without it the pack's own keys are
                     used and the result says SELF_ASSERTED (internal consistency only, not who signed)
--root-key           the contract root public key you pinned yourself; same rule
--previous-checkpoint  the checkpoint you kept from your last audit: proves the log was only appended to since then
                     (the pack must have been requested with since_tree_size = that checkpoint's tree_size)

Checks:
  SCITT  every receipt verifies (Ed25519 checkpoint + RFC 9162 inclusion) against ONE checkpoint; the receipts are
         contiguous leaf indices (nothing in the range can be dropped silently); the consistency proof (if present)
         links the previous checkpoint to this one and the previous checkpoint matches yours.
  CONTRACTS  each export passes the clean-room contract verifier (signatures, chain, replayed decisions).
Prints a JSON summary. Exit 0 = everything verified, 1 = something did not, 2 = usage error.
It proves the evidence was not altered or rewritten -- not that the recorded events are true.
"""
import json
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
try:
    import contract_verify_offline as CV  # noqa: E402
    import scitt_verify_offline as SV  # noqa: E402
except ImportError as exc:  # pragma: no cover
    print(f"put scitt_verify_offline.py and contract_verify_offline.py next to this file ({exc})", file=sys.stderr)
    sys.exit(2)


def verify_pack(pack, scitt_keys=None, root_key="", previous_checkpoint=None):
    problems, out = [], {"schema": "cain.auditor_pack.verification.v1"}
    if not isinstance(pack, dict) or pack.get("schema") != "cain.auditor_pack.v1":
        return {"ok": False, "problems": ["not a cain.auditor_pack.v1 document"]}
    sc = pack.get("scitt") or {}
    keys = scitt_keys if scitt_keys is not None else (sc.get("keys") or {})
    out["scitt_key_status"] = "PINNED" if scitt_keys is not None else "SELF_ASSERTED"
    cp = sc.get("checkpoint")
    receipts = sc.get("receipts") or []
    good = 0
    if cp is None:
        if receipts:
            problems.append("receipts without a checkpoint")
    else:
        if not SV.checkpoint_ok(keys, cp):
            problems.append("pack checkpoint signature does not verify")
        expect = int(sc.get("offset", 0))
        for i, rc in enumerate(receipts):
            try:
                if rc["checkpoint"] != cp:
                    problems.append(f"receipt {i}: not against the pack's checkpoint")
                    continue
                if int(rc["leaf_index"]) != expect:
                    problems.append(f"receipt {i}: leaf {rc['leaf_index']} where {expect} was expected (gap or "
                                    f"reordering)")
                    expect = int(rc["leaf_index"])
                ok, why = SV.verify_receipt(keys, rc)
                if ok:
                    good += 1
                else:
                    problems.append(f"receipt {i} ({rc.get('statement_id')}): {why}")
            except (KeyError, TypeError, ValueError) as exc:
                problems.append(f"receipt {i}: malformed ({type(exc).__name__})")
            expect += 1
        out["scitt_range"] = [int(sc.get("offset", 0)), int(sc.get("offset", 0)) + len(receipts)]
        out["scitt_tree_size"] = cp.get("tree_size")
        out["scitt_complete_log"] = int(sc.get("offset", 0)) == 0 and len(receipts) == cp.get("tree_size")
    out["scitt_receipts_verified"] = f"{good}/{len(receipts)}"
    cons = sc.get("consistency")
    if previous_checkpoint is not None:
        if cons is None:
            problems.append("a previous checkpoint was given but the pack has no consistency proof "
                            "(request it with since_tree_size)")
        elif cons.get("first_checkpoint") != previous_checkpoint:
            problems.append("the pack's earlier checkpoint is not the one you kept: the log may have been forked")
    if cons is not None:
        ok, why = SV.verify_consistency_doc(keys, cons)
        if not ok:
            problems.append(f"consistency: {why}")
        elif cp is not None and cons.get("second_checkpoint") != cp:
            problems.append("consistency proof does not end at the pack's checkpoint")
        out["scitt_append_only"] = why if ok else "NOT VERIFIED"
    ct = pack.get("contracts") or {}
    root = root_key or ct.get("root_public_key_b64", "")
    out["contract_key_status"] = "PINNED" if root_key else "SELF_ASSERTED"
    tenant_ok, cgood = 0, 0
    exports = ct.get("exports") or []
    for i, ex in enumerate(exports):
        try:
            res = CV.verify(ex, root_key_b64=root)
        except Exception as exc:  # noqa: BLE001 -- malformed input is a failed check
            res = {"ok": False, "problems": [type(exc).__name__]}
        if res.get("ok"):
            cgood += 1
        else:
            problems.append(f"contract export {i}: " + "; ".join(res.get("problems", [])[:3]))
    out["contract_exports_verified"] = f"{cgood}/{len(exports)}"
    out["ok"] = not problems
    out["problems"] = problems[:50]
    out["does_not_prove"] = "that the recorded events are true; only that the evidence was not altered or rewritten"
    return out


def main(argv):
    if len(argv) < 2 or argv[1].startswith("-"):
        print(__doc__)
        return 2

    def opt(name):
        return argv[argv.index(name) + 1] if name in argv else None
    with open(argv[1]) as f:
        pack = json.load(f)
    sk = opt("--scitt-keys")
    keys = None
    if sk:
        with open(sk) as f:
            keys = json.load(f)
        keys = keys.get("keys", keys)
    prev = None
    if opt("--previous-checkpoint"):
        with open(opt("--previous-checkpoint")) as f:
            prev = json.load(f)
    res = verify_pack(pack, keys, opt("--root-key") or "", prev)
    print(json.dumps(res, indent=1))
    return 0 if res["ok"] else 1


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