"""CAIN-A75-012 offline verifier (rules embedded verbatim). CAIN-A75-012 research-evidence rules. Standard library only, no CAIN imports: embedded verbatim in the offline
verifier.

A binder-discovery programme claims an experimentally validated binder. The review recomputes the claim from the
evidence record:

  binding      a valid peptide sequence, target, protocol hash and raw-data hash are present
  assay        at least three replicates, positive and negative controls passed, blanks subtracted, a curve fit of at
               least 0.95
  potency      the mean Kd clears the stated potency threshold, and the replicate coefficient of variation is within
               the stated tolerance (a tight mean over wild replicates is not evidence)
  specificity  every declared off-target is at least the stated fold weaker than the target; a flagged off-target blocks
  replication  an independent laboratory repeated the same protocol and landed within the stated tolerance
  claims       every boolean the programme asserts matches what this review computed

Verdict: SUPPORTED, else NOT_SUPPORTED with every failing check named. Malformed input is NOT_SUPPORTED.
It reviews the supplied measurements. It does not run an assay or prove the binder works in an organism.
"""
from __future__ import annotations

import math
import re
import statistics

_ID = re.compile(r"^[A-Za-z0-9_.:\-]{1,80}$")
_HEX = re.compile(r"^[0-9a-f]{64}$")
_AA = re.compile(r"^[ACDEFGHIKLMNPQRSTVWY]{8,80}$")


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 = [], {}

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

    try:
        if not isinstance(record, dict) or record.get("schema") != "cain42.a75.binder.evidence.v1":
            raise Bad("not a cain42.a75.binder.evidence.v1 record")
        claim, assay, methods, raw = record.get("claim"), record.get("assay"), record.get("methods"), record.get("raw_data")
        if not all(isinstance(x, dict) for x in (claim, assay, methods, raw)):
            raise Bad("claim, assay, methods and raw_data must be objects")

        c("binding.sequence", isinstance(claim.get("binder_sequence"), str) and bool(_AA.match(claim["binder_sequence"])),
          "a peptide sequence over the 20 standard residues")
        c("binding.target", isinstance(claim.get("target"), str) and bool(claim["target"].strip()), f"target {claim.get('target')!r}")
        c("binding.protocol_hash", isinstance(methods.get("protocol_sha256"), str) and bool(_HEX.match(methods["protocol_sha256"])),
          "the protocol is named by hash")
        c("binding.raw_data_hash", isinstance(raw.get("sha256"), str) and bool(_HEX.match(raw["sha256"])),
          "the raw data is named by hash")

        reps = assay.get("replicate_kd_nm")
        if not isinstance(reps, list) or len(reps) > 100:
            raise Bad("assay.replicate_kd_nm must be a list")
        reps = [_num(v, "replicate Kd", 0.0001, 1e9) for v in reps]
        c("assay.replicates", len(reps) >= 3, f"{len(reps)} replicate(s)")
        controls = assay.get("controls") if isinstance(assay.get("controls"), dict) else {}
        c("assay.positive_control", controls.get("positive") == "pass", f"positive {controls.get('positive')!r}")
        c("assay.negative_control", controls.get("negative") == "pass", f"negative {controls.get('negative')!r}")
        c("assay.blanks_subtracted", raw.get("blanks_subtracted") is True, "blanks subtracted")
        r2 = _num(raw.get("curve_fit_r2"), "raw_data.curve_fit_r2", 0.0, 1.0)
        c("assay.curve_fit", r2 >= 0.95, f"R^2 {r2} >= 0.95")

        threshold = _num(claim.get("kd_threshold_nm"), "claim.kd_threshold_nm", 0.0001, 1e9)
        max_cv = _num(assay.get("max_cv_pct", 20.0), "assay.max_cv_pct", 0.0, 1000.0)
        mean = statistics.fmean(reps) if reps else float("inf")
        cv = (statistics.pstdev(reps) / mean * 100.0) if len(reps) >= 2 and mean else 0.0
        c("potency.kd_threshold", mean <= threshold, f"mean Kd {round(mean, 4)} nM <= {threshold} nM")
        c("potency.replicate_cv", cv <= max_cv, f"CV {round(cv, 3)}% <= {max_cv}%")
        c("potency.reported_kd_matches", abs(_num(claim.get("reported_kd_nm"), "claim.reported_kd_nm") - mean) <= 1e-6 + 0.01 * mean,
          "the headline Kd matches the replicates")
        summary["mean_kd_nm"] = round(mean, 4)
        summary["cv_pct"] = round(cv, 4)

        fold = _num(claim.get("selectivity_fold"), "claim.selectivity_fold", 1.0, 1e9)
        off = record.get("off_targets") if isinstance(record.get("off_targets"), list) else []
        weak = []
        flagged = []
        for o in off:
            if not isinstance(o, dict) or not isinstance(o.get("target"), str):
                raise Bad("an off-target needs a target")
            kd = _num(o.get("kd_nm"), f"off_targets.{o['target']}.kd_nm", 0.0001, 1e9)
            if kd < mean * fold:
                weak.append(o["target"])
            if o.get("flagged") is True:
                flagged.append(o["target"])
        c("specificity.fold", not weak, f"off-targets below {fold}x: {weak}" if weak else "every off-target clears the fold")
        c("specificity.no_flagged_off_target", not flagged, f"flagged: {flagged}" if flagged else "no off-target flagged")

        rep = record.get("independent_replication") if isinstance(record.get("independent_replication"), dict) else {}
        c("replication.independent_lab", rep.get("performed") is True and isinstance(rep.get("lab_id"), str) and bool(rep["lab_id"]),
          f"lab {rep.get('lab_id')!r}")
        c("replication.same_protocol", rep.get("same_protocol") is True, "the same protocol was used")
        rep_kd = _num(rep.get("kd_nm"), "independent_replication.kd_nm", 0.0001, 1e9)
        tol = _num(claim.get("replication_tolerance_pct"), "claim.replication_tolerance_pct", 0.0, 1000.0)
        delta = abs(rep_kd - mean) / mean * 100.0 if mean else float("inf")
        c("replication.within_tolerance", delta <= tol, f"delta {round(delta, 3)}% <= {tol}%")

        claims = record.get("claims") if isinstance(record.get("claims"), dict) else {}
        computed = {
            "validated": mean <= threshold and cv <= max_cv and r2 >= 0.95 and len(reps) >= 3,
            "specific": not weak and not flagged,
            "replicated": rep.get("performed") is True and rep.get("same_protocol") is True and delta <= tol,
        }
        for k in sorted(claims):
            c(f"claims.{k}", k in computed and claims[k] == computed[k], f"claimed {claims[k]!r}, computed {computed.get(k)!r}")
        summary["computed"] = computed
    except Bad as e:
        checks.append({"id": "input.well_formed", "ok": False, "detail": str(e)})
        return "NOT_SUPPORTED", checks, summary
    except Exception as e:  # noqa: BLE001
        checks.append({"id": "input.error", "ok": False, "detail": f"{type(e).__name__}: {e}"[:200]})
        return "NOT_SUPPORTED", checks, summary
    return ("SUPPORTED" if all(x["ok"] for x in checks) else "NOT_SUPPORTED"), 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.binder.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-012, {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))
