"""CAIN-A75-034 offline verifier (rules embedded verbatim). CAIN-A75-034 regulated-workflow rules. Standard library only, no CAIN imports: embedded verbatim in the offline
verifier.

A clinical-trial oversight workflow claims it produced a compliant evidence pack. The review recomputes the pack:

  records     every step names an actor and a control, and carries at least one signed, hashed, timestamped record
  approvals   a step marked as requiring approval has an approval from someone other than the actor
  controls    every control is met or marked not-applicable, and its evidence ids exist
  deviations  every deviation has a CAPA that addresses it and is closed; a high-severity deviation cannot be open
  retention   the pack is retained for at least the regulation's minimum, immutably, with a backup
  claims      every boolean the pack asserts matches what this review computed

Verdict: COMPLIANT, else NON_COMPLIANT with every failing check named. Malformed input is NON_COMPLIANT.
It reviews the pack record; it does not audit the trial.
"""
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}$")
MIN_YEARS = {"GxP": 10, "HIPAA": 6, "EU-AI-ACT": 10}


class Bad(ValueError):
    pass


def _num(v, what, lo=0.0, hi=1e9):
    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 = [], {"steps": {}, "deviations": {}}

    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.regulated.pack.v1":
            raise Bad("not a cain42.a75.regulated.pack.v1 record")
        wf, steps, controls = record.get("workflow"), record.get("steps"), record.get("controls")
        if not all(isinstance(x, dict) for x in (wf,)) or not isinstance(steps, list) or not isinstance(controls, list):
            raise Bad("workflow object, steps list and controls list are required")
        regulation = wf.get("regulation")
        if regulation not in MIN_YEARS:
            raise Bad(f"workflow.regulation must be one of {sorted(MIN_YEARS)}")
        c("workflow.named", isinstance(wf.get("id"), str) and bool(wf["id"]) and isinstance(wf.get("version"), str) and bool(wf["version"]),
          "workflow id and version")

        S = {}
        record_ids = set()
        for s in steps:
            if not isinstance(s, dict) or not isinstance(s.get("id"), str) or not _ID.match(s["id"]):
                raise Bad("a step needs an id")
            if s["id"] in S:
                raise Bad(f"duplicate step {s['id']}")
            S[s["id"]] = s
            c(f"{s['id']}.actor", isinstance(s.get("actor"), str) and bool(s["actor"]), "an actor is named")
            c(f"{s['id']}.control", isinstance(s.get("control"), str) and bool(s["control"]), "a control is named")
            recs = s.get("records")
            if not isinstance(recs, list) or not recs:
                c(f"{s['id']}.records", False, "no record")
                continue
            good = True
            for r in recs:
                if not isinstance(r, dict) or not isinstance(r.get("id"), str) or not _ID.match(r["id"]):
                    raise Bad(f"{s['id']}: a record needs an id")
                record_ids.add(r["id"])
                ok = (isinstance(r.get("sha256"), str) and bool(_HEX.match(r["sha256"])) and r.get("signed") is True
                      and isinstance(r.get("timestamp"), str) and bool(r["timestamp"]))
                good = good and ok
            c(f"{s['id']}.records", good, "every record is hashed, signed and timestamped")
            if s.get("requires_approval") is True:
                ap = s.get("approval") if isinstance(s.get("approval"), dict) else {}
                c(f"{s['id']}.approval", ap.get("by") not in (None, "", s.get("actor")), f"approved by {ap.get('by')!r}")
                c(f"{s['id']}.approval_time", isinstance(ap.get("at"), str) and bool(ap["at"]), "approval timestamp")
            summary["steps"][s["id"]] = {"actor": s.get("actor"), "requires_approval": s.get("requires_approval") is True}

        C = {}
        for ct in controls:
            if not isinstance(ct, dict) or not isinstance(ct.get("id"), str) or not _ID.match(ct["id"]):
                raise Bad("a control needs an id")
            C[ct["id"]] = ct
            status = ct.get("status")
            c(f"control.{ct['id']}.status", status in ("met", "na", "gap"), f"status {status!r}")
            if status == "met":
                ev = ct.get("evidence_ids")
                c(f"control.{ct['id']}.evidence", isinstance(ev, list) and bool(ev) and all(x in record_ids for x in ev),
                  f"evidence {ev!r}")
            else:
                c(f"control.{ct['id']}.not_gap", status != "gap", "no control gap")

        deviations = record.get("deviations") if isinstance(record.get("deviations"), list) else []
        capa = {capa["id"]: capa for capa in record.get("capa", []) if isinstance(capa, dict) and isinstance(capa.get("id"), str)}
        for d in deviations:
            if not isinstance(d, dict) or not isinstance(d.get("id"), str):
                raise Bad("a deviation needs an id")
            cid = d.get("capa_id")
            cp = capa.get(cid)
            c(f"deviation.{d['id']}.capa", cp is not None and d["id"] in (cp.get("addresses") or []),
              f"CAPA {cid!r} addresses the deviation")
            c(f"deviation.{d['id']}.closed", cp is not None and cp.get("status") == "closed", f"CAPA status {cp and cp.get('status')!r}")
            if d.get("severity") == "high":
                c(f"deviation.{d['id']}.high_closed", cp is not None and cp.get("status") == "closed",
                  "a high-severity deviation needs a closed CAPA")
            summary["deviations"][d["id"]] = {"severity": d.get("severity"), "capa": cid}

        ret = record.get("retention") if isinstance(record.get("retention"), dict) else {}
        years = _num(ret.get("years"), "retention.years", 0.0, 1000.0)
        minimum = MIN_YEARS[regulation]
        c("retention.years", years >= minimum, f"{years} >= {minimum} years for {regulation}")
        c("retention.immutable", ret.get("immutable") is True, "immutable storage")
        c("retention.backup", ret.get("backup") is True, "backup exists")

        claims = record.get("claims") if isinstance(record.get("claims"), dict) else {}
        computed = {
            "all_controls_met": all(ct.get("status") in ("met", "na") for ct in controls),
            "all_deviations_capa": all(
                (capa.get(d.get("capa_id")) is not None and d["id"] in (capa[d["capa_id"]].get("addresses") or [])
                 and capa[d["capa_id"]].get("status") == "closed")
                for d in deviations),
            "retention_sufficient": years >= minimum and ret.get("immutable") is True,
        }
        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 "NON_COMPLIANT", checks, summary
    except Exception as e:  # noqa: BLE001
        checks.append({"id": "input.error", "ok": False, "detail": f"{type(e).__name__}: {e}"[:200]})
        return "NON_COMPLIANT", checks, summary
    return ("COMPLIANT" if all(x["ok"] for x in checks) else "NON_COMPLIANT"), 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.regulated.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-034, {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))
