"""CAIN-A75-009 offline verifier (rules embedded verbatim). CAIN-A75-009 autonomous-R&D-change rules. Standard library only, no CAIN imports: embedded verbatim in the offline
verifier.

An autonomous R&D loop proposes a change on the strength of one experiment. The review recomputes the claim:

  binding     the change names its files and hashes; the diff hash equals SHA-256 over the sorted file hashes; the
              rationale is non-empty; the linked experiment is the one evaluated
  authority   a named principal whose scope covers "change", not expired at the supplied clock
  experiment  preregistered, enough trials, a seed recorded, a known direction, and an improvement that clears the
              reported confidence interval (the point estimate alone is not evidence)
  tests       at least one test failed before and passes after (that is what shows the change did something); no test
              that passed before fails after (regression)
  held-out    a held-out check is present and was not used to select the change
  claims      every boolean the record asserts matches what this review computed

Verdict: ACCEPT, else REJECT with every failing check named. Malformed input is REJECT.
It reviews supplied numbers and test results; it does not re-run the experiment or the code.
"""
from __future__ import annotations

import hashlib
import json
import math
import re

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


class Bad(ValueError):
    pass


def diff_hash(files):
    body = [{"path": f["path"], "before_sha256": f["before_sha256"], "after_sha256": f["after_sha256"]} for f in files]
    body.sort(key=lambda x: x["path"])
    return hashlib.sha256(json.dumps(body, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()).hexdigest()


def _int(v, what, lo=0, hi=10 ** 9):
    if isinstance(v, bool) or not isinstance(v, int) or not (lo <= v <= hi):
        raise Bad(f"{what} must be an integer in [{lo}, {hi}]")
    return v


def _num(v, what, lo=-1e12, 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.rnd.change.v1":
            raise Bad("not a cain42.a75.rnd.change.v1 record")
        ch, exp, auth = record.get("change"), record.get("experiment"), record.get("authority")
        if not all(isinstance(x, dict) for x in (ch, exp, auth)):
            raise Bad("change, experiment and authority must be objects")
        if not isinstance(ch.get("id"), str) or not _ID.match(ch["id"]):
            raise Bad("change.id must be an identifier")
        if not isinstance(ch.get("rationale"), str) or len(ch["rationale"].strip()) < 12:
            raise Bad("change.rationale must be a substantive sentence")

        # binding
        files = ch.get("files")
        if not isinstance(files, list) or not (1 <= len(files) <= 500):
            raise Bad("change.files must be a list of 1-500 entries")
        for f in files:
            if not isinstance(f, dict) or not isinstance(f.get("path"), str) or not f["path"].strip():
                raise Bad("a file entry needs a path")
            for k in ("before_sha256", "after_sha256"):
                if not isinstance(f.get(k), str) or not _HEX.match(f[k]):
                    raise Bad(f"{f['path']}: {k} must be 64 hex characters")
        if len({f["path"] for f in files}) != len(files):
            raise Bad("a file path appears twice")
        c("binding.diff_hash", ch.get("diff_sha256") == diff_hash(files), "the diff hash covers exactly these files")
        c("binding.experiment_linked", ch.get("experiment_id") == exp.get("id"), f"linked {ch.get('experiment_id')!r}")
        c("binding.artifact_hash", isinstance(ch.get("artifact_sha256"), str) and bool(_HEX.match(ch["artifact_sha256"])),
          "the built artifact is named by hash")

        # authority
        now, exp_at = _num(auth.get("now"), "authority.now"), _num(auth.get("expires_at"), "authority.expires_at")
        c("authority.principal", isinstance(auth.get("principal"), str) and bool(auth["principal"]), "named principal")
        c("authority.scope", isinstance(auth.get("scope"), list) and "change" in auth["scope"], f"scope {auth.get('scope')!r}")
        c("authority.not_expired", exp_at > now, f"expires {exp_at} > now {now}")

        # experiment
        c("experiment.preregistered", exp.get("preregistered") is True, "the hypothesis and metric were fixed before the run")
        trials = _int(exp.get("trials"), "experiment.trials", 1, 1_000_000)
        min_trials = _int(exp.get("min_trials"), "experiment.min_trials", 1, 1_000_000)
        c("experiment.trials", trials >= min_trials, f"{trials} >= {min_trials}")
        c("experiment.seed", isinstance(exp.get("seed"), int) and not isinstance(exp.get("seed"), bool), f"seed {exp.get('seed')!r}")
        direction = exp.get("direction")
        c("experiment.direction", direction in ("higher_better", "lower_better"), f"direction {direction!r}")
        base = _num(exp.get("baseline_value"), "experiment.baseline_value")
        cand = _num(exp.get("candidate_value"), "experiment.candidate_value")
        ci = abs(_num(exp.get("ci95"), "experiment.ci95", 0.0, 1e12))
        gain = cand - base if direction == "higher_better" else base - cand
        clears = gain > ci + 1e-9          # floating point must not turn "equal to the CI" into "clears the CI"
        c("experiment.clears_ci", clears, f"gain {round(gain, 6)} > 95% CI {ci}")
        c("experiment.metric_matches", isinstance(exp.get("metric"), str) and bool(exp["metric"]), f"metric {exp.get('metric')!r}")
        summary["gain"] = round(gain, 6)
        summary["ci95"] = ci

        # tests
        tests = record.get("tests")
        if not isinstance(tests, list) or not tests or len(tests) > 500:
            raise Bad("tests must be a non-empty list")
        fixed, regressed = [], []
        for t in tests:
            if not isinstance(t, dict) or not isinstance(t.get("name"), str) or not _ID.match(t["name"]):
                raise Bad("a test needs a name")
            b, a = t.get("before"), t.get("after")
            if b not in ("pass", "fail") or a not in ("pass", "fail"):
                raise Bad(f"{t['name']}: before/after must be pass or fail")
            if b == "fail" and a == "pass":
                fixed.append(t["name"])
            if b == "pass" and a == "fail":
                regressed.append(t["name"])
        c("tests.one_fix_demonstrated", bool(fixed), f"failed then passed: {fixed}")
        c("tests.no_regression", not regressed, f"passed then failed: {regressed}")
        summary["fixed_tests"] = fixed
        summary["regressed_tests"] = regressed

        # held-out
        held = record.get("held_out") if isinstance(record.get("held_out"), dict) else {}
        c("held_out.present", held.get("result") in ("pass", "fail"), f"result {held.get('result')!r}")
        c("held_out.not_used_for_selection", held.get("used_for_selection") is False, "held-out was not used to pick the change")
        c("held_out.passed", held.get("result") == "pass", "the held-out check passes")

        # claims
        claims = record.get("claims") if isinstance(record.get("claims"), dict) else {}
        computed = {
            "improves_metric": clears,
            "tests_pass": not regressed and bool(fixed),
            "no_regression": not regressed,
            "held_out_passed": held.get("result") == "pass",
        }
        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 "REJECT", checks, summary
    except Exception as e:  # noqa: BLE001
        checks.append({"id": "input.error", "ok": False, "detail": f"{type(e).__name__}: {e}"[:200]})
        return "REJECT", checks, summary
    return ("ACCEPT" if all(x["ok"] for x in checks) else "REJECT"), 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.rnd.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-009, {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))
