"""CAIN-A75-019 offline verifier (rules embedded verbatim). CAIN-A75-019 runtime-containment rules. Standard library only, no CAIN imports: embedded verbatim in the offline
verifier.

An agent incident was contained at runtime. The audit recomputes whether containment actually happened:

  coverage    every affected agent is quarantined (or its authority is revoked) by some action
  timeliness  every action completed within the declared SLA from detection
  evidence    every action carries a signed receipt hash and a verifier mark; an unverified action is not contained
  blast radius no action targets an entity outside the declared affected set unless it is explicitly marked and justified
  recovery    recovery is only recorded after every containment action is verified, and the canary passes
  claims      every boolean the record asserts matches what this audit computed

Verdict: CONTAINED, else NOT_CONTAINED with every failing check named. Malformed input is NOT_CONTAINED.
It audits the supplied incident record; it does not query the runtime.
"""
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}$")
ACTIONS = {"quarantine_agent", "revoke_tokens", "isolate_network", "pause_queue", "disable_tool"}


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 = [], {"actions": {}, "contained_agents": []}

    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.containment.record.v1":
            raise Bad("not a cain42.a75.containment.record.v1 record")
        inc, con = record.get("incident"), record.get("containment")
        if not all(isinstance(x, dict) for x in (inc, con)):
            raise Bad("incident and containment must be objects")
        affected = inc.get("affected_agents")
        if not isinstance(affected, list) or not affected or not all(isinstance(a, str) and _ID.match(a) for a in affected):
            raise Bad("incident.affected_agents must be a non-empty list of ids")
        if len(set(affected)) != len(affected):
            raise Bad("affected_agents repeats an id")
        detected = _num(inc.get("detected_at"), "incident.detected_at")
        sla = _num(con.get("sla_seconds"), "containment.sla_seconds", 0.0)
        actions = con.get("actions")
        if not isinstance(actions, list) or not actions or len(actions) > 1000:
            raise Bad("containment.actions must be a non-empty list")
        seen, contained, verified_all, targets = set(), set(), True, set()
        for a in actions:
            if not isinstance(a, dict) or not isinstance(a.get("id"), str) or not _ID.match(a["id"]):
                raise Bad("an action needs an id")
            if a["id"] in seen:
                raise Bad(f"duplicate action {a['id']}")
            seen.add(a["id"])
            if a.get("type") not in ACTIONS:
                raise Bad(f"{a['id']}: type must be one of {sorted(ACTIONS)}")
            target = a.get("target")
            if not isinstance(target, str) or not _ID.match(target):
                raise Bad(f"{a['id']}: target required")
            targets.add(target)
            completed = _num(a.get("completed_at"), f"{a['id']}.completed_at")
            c(f"{a['id']}.within_sla", completed - detected <= sla + 1e-9,
              f"completed {round(completed - detected, 3)}s after detection, SLA {sla}s")
            if target in affected:
                contained.add(target)
            receipt = a.get("receipt")
            c(f"{a['id']}.receipt", isinstance(receipt, dict) and isinstance(receipt.get("sha256"), str) and bool(_HEX.match(receipt["sha256"])),
              "the action carries a receipt hash")
            c(f"{a['id']}.signed", isinstance(receipt, dict) and isinstance(receipt.get("signed_by"), str) and bool(receipt["signed_by"]),
              "the receipt names a signer")
            ok = isinstance(receipt, dict) and receipt.get("verified") is True
            c(f"{a['id']}.verified", ok, "the receipt verifies")
            verified_all = verified_all and ok
            summary["actions"][a["id"]] = {"type": a["type"], "target": target,
                                           "seconds_after_detection": round(completed - detected, 3)}
        for agent in affected:
            c(f"{agent}.contained", agent in contained, "an action targets this affected agent")
        summary["contained_agents"] = sorted(contained)

        outside = sorted(targets - set(affected))
        for target in outside:
            justified = any(isinstance(a, dict) and a.get("target") == target and a.get("lateral_justification")
                            for a in actions)
            c(f"blast_radius.{target}.justified", bool(justified), "an out-of-scope target needs an explicit justification")
        c("blast_radius.no_unjustified_target", not outside or all(
            any(isinstance(a, dict) and a.get("target") == t and a.get("lateral_justification") for a in actions) for t in outside),
          f"outside targets: {outside}")

        rec = record.get("recovery") if isinstance(record.get("recovery"), dict) else {}
        recovered = rec.get("restored_at") is not None
        if recovered:
            _num(rec.get("restored_at"), "recovery.restored_at")
            c("recovery.after_verified_containment", verified_all and len(contained) == len(affected),
              "recovery is only allowed after every containment action verified")
            c("recovery.canary", rec.get("canary_pass") is True, "the post-recovery canary passed")
        else:
            c("recovery.not_yet", True, "no recovery recorded")

        claims = record.get("claims") if isinstance(record.get("claims"), dict) else {}
        computed = {
            "contained_within_sla": all(x["ok"] for x in checks if x["id"].endswith(".within_sla")) if actions else False,
            "all_actions_verified": verified_all,
            "all_agents_contained": len(contained) == len(affected),
            "no_lateral_damage": not outside or all(
                any(isinstance(a, dict) and a.get("target") == t and a.get("lateral_justification") for a in actions) for t in outside),
        }
        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 "NOT_CONTAINED", checks, summary
    except Exception as e:  # noqa: BLE001
        checks.append({"id": "input.error", "ok": False, "detail": f"{type(e).__name__}: {e}"[:200]})
        return "NOT_CONTAINED", checks, summary
    return ("CONTAINED" if all(x["ok"] for x in checks) else "NOT_CONTAINED"), 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.containment.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-019, {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))
