#!/usr/bin/env python3
"""CAIN-42 L5 clean-room bundle verifier (Evolution #1, Part XXVII).

INDEPENDENT: this file imports NO CAIN code. It re-implements canonical JSON and the Ed25519 signature
check from scratch and recomputes the AUTHORITY_ROOT, so it can detect a bundle that was generated by a
broken kernel.

    python3 scripts/cain42_l5/verify_l5_bundle.py CAIN42_L5_EVIDENCE_BUNDLE.json

Verdict is one of: VALID / INVALID / INCOMPLETE / EXPIRED / REVOKED / SUPERSEDED / NOT_PROVEN.
Only `VALID` means every check passed; a NOT_PROVEN check makes the verdict NOT_PROVEN.
"""
from __future__ import annotations

import base64
import hashlib
import json
import sys
from pathlib import Path

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric import ed25519

D_CERT = "CAIN42/L5-AUTONOMY-CERTIFICATE/v1"
D_LEASE = "CAIN42/L5-AUTONOMY-LEASE/v1"
D_AUTHORITY_ROOT = "CAIN42/L5-AUTHORITY-ROOT/v1"

# Mirrored by hand from cain45/l5/certificate.py — the point of a clean room is not to import it.
CERT_FIELDS = (
    "agent_identity", "agent_version", "model_identity", "model_version", "policy_version",
    "tool_set", "memory_policy", "world_model_version", "authority_scope", "resource_scope",
    "delegation_scope", "execution_scope", "environment_scope", "risk_ceiling",
    "blast_radius_ceiling", "time_limit", "budget_limit", "trust_requirement",
    "evidence_requirement", "recovery_requirement", "human_escalation_requirement",
    "autonomy_vector", "cluster_epoch", "cluster_view", "authority_root", "policy_root",
    "trust_root", "evidence_root", "trajectory_root", "timestamp", "expiry",
    "certificate_version", "issuer",
)
LEASE_FIELDS = (
    "lease_id", "subject", "certificate_digest", "task", "resource", "tool", "data", "network",
    "delegation", "spend", "trajectory", "environment", "risk", "blast_radius", "issued_at",
    "expires_at", "issued_by", "renewal_count", "max_renewals",
)
STATE_KEYS = (
    "identity_state", "policy_state", "trust_state", "quorum_state", "delegation_state",
    "resource_state", "risk_state", "trajectory_state", "environment_state", "certificate_state",
)


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


def digest(domain: str, fields: dict) -> str:
    if "domain" in fields:
        raise ValueError("fields must not carry their own domain")
    return hashlib.sha256(canon({"domain": domain, **fields})).hexdigest()


def verify_sig(pub: str, sig: str, domain: str, fields: dict) -> bool:
    try:
        ed25519.Ed25519PublicKey.from_public_bytes(base64.b64decode(pub)).verify(
            base64.b64decode(sig), digest(domain, fields).encode())
        return True
    except (InvalidSignature, ValueError, TypeError):
        return False


def body_of(obj: dict, fields) -> dict:
    out = {}
    for name in fields:
        value = obj.get(name)
        if isinstance(value, list):
            out[name] = sorted(value)
        else:
            out[name] = value
    return out


def main() -> int:
    if len(sys.argv) != 2:
        print("usage: verify_l5_bundle.py <bundle.json>", file=sys.stderr)
        return 2
    bundle_path = Path(sys.argv[1])
    bundle = json.loads(bundle_path.read_text())

    checks: list[dict] = []
    not_proven: list[str] = []

    def check(name: str, ok, detail: str = "") -> None:
        checks.append({"check": name, "ok": bool(ok), "detail": detail})

    pub = bundle.get("public_key", "")
    cert = bundle.get("certificate", {})
    lease = bundle.get("lease", {})
    states = bundle.get("state_roots", {})

    # 1. authority root recomputation
    missing = [k for k in STATE_KEYS if k not in states]
    if missing:
        check("authority_root.complete", False, f"missing state roots: {missing}")
        not_proven.append("authority_root.complete")
    else:
        recomputed = digest(D_AUTHORITY_ROOT, {k: str(states[k]) for k in STATE_KEYS})
        check("authority_root.recomputed", recomputed == bundle.get("authority_root"),
              f"recomputed={recomputed}")

    # 2. certificate signature over the declared body
    cert_body = body_of(cert, CERT_FIELDS)
    check("certificate.signature", verify_sig(pub, cert.get("signature", ""), D_CERT, cert_body))
    check("certificate.digest", digest(D_CERT, cert_body) == cert.get("digest"))

    # 3. lease signature
    lease_body = body_of(lease, LEASE_FIELDS)
    check("lease.signature", verify_sig(pub, lease.get("signature", ""), D_LEASE, lease_body))
    check("lease.digest", digest(D_LEASE, lease_body) == lease.get("digest"))
    check("lease.binds_certificate", lease_body.get("certificate_digest") == cert.get("digest"))

    # 4. certificate <-> lease subject and authority root binding
    check("certificate.lease_subject", cert_body.get("agent_identity") == lease_body.get("subject"))
    check("certificate.authority_root_present", bool(cert_body.get("authority_root")))

    # 5. expiry / revocation (no `now` supplied -> report, do not fail)
    if bundle.get("checked_at") is None:
        checks.append({"check": "temporal.expiry", "ok": None,
                       "detail": "no checked_at supplied; expiry not evaluated"})
    if lease.get("revoked"):
        checks.append({"check": "lease.revoked", "ok": False, "detail": lease.get("revoked_why", "revoked")})

    # 6. invariants matrix internal consistency
    inv = bundle.get("invariants", {})
    check("invariants.count", inv.get("checked") == 20, f"checked={inv.get('checked')}")
    check("invariants.all_hold", inv.get("all_hold") is True, f"failed={inv.get('failed')}")

    # verdict
    failed = [c["check"] for c in checks if c["ok"] is False]
    if failed:
        verdict = "INVALID"
    elif not_proven:
        verdict = "NOT_PROVEN"
    elif any(c["ok"] is None for c in checks):
        verdict = "VALID"   # temporal check deferred, structurally valid
    else:
        verdict = "VALID"

    result = {"bundle": str(bundle_path.name), "schema": bundle.get("schema"),
              "verdict": verdict, "checks_passed": sum(1 for c in checks if c["ok"] is True),
              "checks_failed": len(failed), "failed": failed, "checks": checks,
              "clean_room": True, "imports_cain": False}
    print(json.dumps(result, indent=2))
    return 0 if verdict == "VALID" else 1


if __name__ == "__main__":
    raise SystemExit(main())
