"""CAIN-A75-035 offline verifier (rules embedded verbatim). CAIN-A75-035 real-time-gate rules. Standard library only, no CAIN imports: embedded verbatim in the offline verifier.

A voice agent reasons while it listens. The gate review recomputes whether an action was allowed to fire in real time:

  latency     the gate decision was made within the policy's maximum latency
  ordering    a tool call or a spoken response happens only after a final transcript event
  confidence  an intent that triggers an action meets the minimum confidence
  authority   a tool is not forbidden and carries a live capability token (or an explicit authorization)
  consistency an ALLOW decision has no unexecuted contradiction; a DENY decision has no executed tool
  timeline    event times are monotonic and ids are unique
  claims      every boolean the record asserts matches what this review computed

Verdict: SOUND, else UNSOUND with every failing check named. Malformed input is UNSOUND.
It reviews the gate record; it does not join the audio stream.
"""
from __future__ import annotations

import math
import re

_ID = re.compile(r"^[A-Za-z0-9_.:\-]{1,80}$")
_HEX = re.compile(r"^[0-9a-f]{64}$")
KINDS = {"partial_transcript", "final_transcript", "intent", "tool_call", "response"}


class Bad(ValueError):
    pass


def _num(v, what, lo=0.0, hi=1e12):
    if isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v) or not (lo <= v <= hi):
        raise Bad(f"{what} must be a finite number in [{lo}, {hi}]")
    return float(v)


def evaluate(record):
    checks, summary = [], {"events": {}, "action_event": None}

    def c(cid, ok, detail=""):
        checks.append({"id": cid, "ok": bool(ok), "detail": detail})

    try:
        if not isinstance(record, dict) or record.get("schema") != "cain42.a75.realtime.gate.v1":
            raise Bad("not a cain42.a75.realtime.gate.v1 record")
        stream, gate, pol = record.get("stream"), record.get("gate"), record.get("policy")
        if not all(isinstance(x, dict) for x in (stream, gate, pol)):
            raise Bad("stream, gate and policy must be objects")
        max_lat = _num(pol.get("max_latency_ms"), "policy.max_latency_ms", 0.0)
        min_conf = _num(pol.get("min_intent_confidence"), "policy.min_intent_confidence", 0.0, 1.0)
        forbidden = pol.get("forbidden_tools") if isinstance(pol.get("forbidden_tools"), list) else []
        require_final = pol.get("require_final_transcript_for") if isinstance(pol.get("require_final_transcript_for"), list) else []
        c("policy.hash", isinstance(pol.get("policy_sha256"), str) and bool(_HEX.match(pol["policy_sha256"])),
          "the policy revision is hashed")

        events = record.get("events")
        if not isinstance(events, list) or not events or len(events) > 5000:
            raise Bad("events must be a non-empty list")
        seen, last_t = set(), -1.0
        final_seen_at = None
        intent_conf = None
        action_event = None
        for e in events:
            if not isinstance(e, dict) or not isinstance(e.get("id"), str) or not _ID.match(e["id"]):
                raise Bad("an event needs an id")
            if e["id"] in seen:
                raise Bad(f"duplicate event {e['id']}")
            seen.add(e["id"])
            if e.get("kind") not in KINDS:
                raise Bad(f"{e['id']}: kind must be one of {sorted(KINDS)}")
            t = _num(e.get("t_ms"), f"{e['id']}.t_ms")
            c(f"{e['id']}.monotonic", t >= last_t, f"t={t} after {last_t}")
            last_t = t
            if e["kind"] == "final_transcript":
                final_seen_at = t
            elif e["kind"] == "intent":
                intent_conf = _num(e.get("confidence"), f"{e['id']}.confidence", 0.0, 1.0)
                summary["events"][e["id"]] = {"kind": "intent", "confidence": intent_conf}
            elif e["kind"] in ("tool_call", "response"):
                action_event = action_event or e
                summary["action_event"] = e["id"]

        if action_event is not None:
            kind = action_event["kind"]
            if kind in require_final:
                c(f"{action_event['id']}.after_final", final_seen_at is not None and action_event["t_ms"] >= final_seen_at,
                  "the action follows a final transcript")
            c(f"{action_event['id']}.confidence", intent_conf is not None and intent_conf >= min_conf,
              f"intent confidence {intent_conf} >= {min_conf}")
            if kind == "tool_call":
                tool = action_event.get("tool")
                c(f"{action_event['id']}.tool_allowed", isinstance(tool, str) and tool not in forbidden, f"tool {tool!r}")
                tok = action_event.get("capability_token") if isinstance(action_event.get("capability_token"), dict) else {}
                c(f"{action_event['id']}.token", isinstance(tok.get("sha256"), str) and bool(_HEX.match(tok["sha256"])),
                  "a capability token is present")
                c(f"{action_event['id']}.token_live", _num(tok.get("expires_at_ms", 0.0), f"{action_event['id']}.token.expires_at_ms") > action_event["t_ms"],
                  "the capability token is live at the action time")
                c(f"{action_event['id']}.authorized", action_event.get("authorized") is True or bool(tok), "the call is authorized")

        decided = _num(gate.get("decided_at_ms"), "gate.decided_at_ms")
        latency = _num(gate.get("latency_ms"), "gate.latency_ms", 0.0)
        c("latency.within", latency <= max_lat + 1e-9, f"{latency} <= {max_lat} ms")
        c("latency.matches_timeline", action_event is None or decided <= action_event["t_ms"] + 1e-9,
          "the decision is not after the action it gates")
        decision = gate.get("decision")
        c("decision.value", decision in ("ALLOW", "DENY", "HOLD"), f"decision {decision!r}")
        if action_event is not None:
            executed = action_event.get("executed") is True
            c(f"{action_event['id']}.consistent", (decision == "ALLOW" and executed) or (decision != "ALLOW" and not executed),
              f"decision {decision!r}, executed={executed}")

        claims = record.get("claims") if isinstance(record.get("claims"), dict) else {}
        computed = {
            "within_latency": latency <= max_lat + 1e-9,
            "final_before_action": action_event is None or action_event["kind"] not in require_final
            or (final_seen_at is not None and action_event["t_ms"] >= final_seen_at),
            "tool_authorized": action_event is None or action_event["kind"] != "tool_call"
            or (action_event.get("authorized") is True and action_event.get("tool") not in forbidden),
        }
        for key in sorted(claims):
            c(f"claims.{key}", key in computed and claims[key] == computed[key], f"claimed {claims[key]!r}, computed {computed.get(key)!r}")
        summary["computed"] = computed
    except Bad as e:
        checks.append({"id": "input.well_formed", "ok": False, "detail": str(e)})
        return "UNSOUND", checks, summary
    except Exception as e:  # noqa: BLE001
        checks.append({"id": "input.error", "ok": False, "detail": f"{type(e).__name__}: {e}"[:200]})
        return "UNSOUND", checks, summary
    return ("SOUND" if all(x["ok"] for x in checks) else "UNSOUND"), checks, summary


# ---------------------------------------------------------------------------- offline verifier (no CAIN imports)
# Every name below is prefixed _kit_verifier_ / _b64 / _hl / _json / _sys so it cannot shadow a name in the rules above.
import base64 as _b64, hashlib as _hl, json as _json, sys as _sys
_KIT_VERIFIER_GENESIS = _hl.sha256(b"cain42.a75.realtime.genesis").hexdigest()


def _kit_verifier_canon(o):
    return _json.dumps(o, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()


def _kit_verifier_sha(o):
    return _hl.sha256(_kit_verifier_canon(o)).hexdigest()


def _kit_verifier_main(argv):
    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
    if len(argv) < 2:
        print("usage: verify_offline.py run.json --pinned-key BASE64"); return 2
    run = _json.load(open(argv[1]))
    pin = argv[argv.index("--pinned-key") + 1] if "--pinned-key" in argv else None
    problems, prev, recs = [], _KIT_VERIFIER_GENESIS, run.get("records") or []
    for i, r in enumerate(recs):
        body = {k: v for k, v in r.items() if k not in ("record_hash", "signature_b64")}
        if _kit_verifier_sha(body) != r.get("record_hash"): problems.append(f"record {i}: record_hash does not match")
        if body.get("subject_sha256") != _kit_verifier_sha(body.get("subject")): problems.append(f"record {i}: subject_sha256 does not match")
        if pin and r.get("public_key_b64") != pin: problems.append(f"record {i}: different key than the pinned one")
        try:
            Ed25519PublicKey.from_public_bytes(_b64.b64decode(pin or r.get("public_key_b64"))).verify(
                _b64.b64decode(r.get("signature_b64", "")), str(r.get("record_hash")).encode())
        except Exception:
            problems.append(f"record {i}: signature does not verify")
        if r.get("prev_record_hash") != prev: problems.append(f"record {i}: chain link broken")
        v, checks, _ = evaluate(r.get("subject"))
        if v != r.get("verdict") or checks != r.get("checks"):
            problems.append(f"record {i}: re-running the rules gives {v}, the record says {r.get('verdict')}")
        prev = r.get("record_hash")
    if run.get("run_hash") != _kit_verifier_sha([r.get("record_hash") for r in recs]): problems.append("run_hash does not match")
    for p in problems: print("  -", p)
    if not pin: print("WARNING: no --pinned-key given; the signature was checked against the key inside the file")
    print("VERDICT:", "VERIFIED" if not problems else "TAMPERED OR INVALID", f"(CAIN-A75-035, {len(recs)} record(s), verdicts {run.get('verdicts')})")
    return 0 if not problems else 1


if __name__ == "__main__":
    _sys.exit(_kit_verifier_main(_sys.argv))
