"""CAIN-A75-008 offline verifier (rules embedded verbatim). CAIN-A75-008 robotics-release-gate rules. Standard library only, no CAIN imports: embedded verbatim in the offline
verifier.

A robot policy is proposed for release. The gate recomputes the decision from the evidence instead of trusting the
release note:

  binding     the policy artifact hash is present and matches the evaluated artifact; a model card and training-data
              provenance are named; every sign-off is bound to the same artifact hash
  coverage    every required scenario has at least the declared minimum number of trials
  statistics  the reported success rate must equal successes/trials; a Wilson 95% lower bound on each required
              scenario must clear the gate threshold (so 97/100 does not pass a 0.99 gate)
  safety      total safety violations are within the gate; any critical violation blocks regardless of the budget
  regression  no scenario may fall more than the allowed margin below the previous release
  sign-off    every required role accepted, and no sign-off is stale (artifact hash must match)
  honesty     a SIMULATED-only run cannot claim hardware validation; the release's own "passed" claim must match
              what this gate computed

Verdict: PASS, else BLOCK with every failing check named. Malformed input is BLOCK.
This is a release-evidence gate on supplied numbers. It does not run the policy or the robot.
"""
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}$")


class Bad(ValueError):
    pass


def wilson_lower(successes, trials, z=1.96):
    if trials <= 0:
        return 0.0
    p = successes / trials
    denom = 1 + z * z / trials
    centre = p + z * z / (2 * trials)
    margin = z * math.sqrt((p * (1 - p) + z * z / (4 * trials)) / trials)
    return max(0.0, (centre - margin) / denom)


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=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(release):
    checks, summary = [], {"scenarios": {}, "wilson_lower": {}}

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

    try:
        if not isinstance(release, dict) or release.get("schema") != "cain42.a75.robotics.release.v1":
            raise Bad("not a cain42.a75.robotics.release.v1 release")
        pol, gate, evals = release.get("policy"), release.get("gate"), release.get("evaluations")
        if not all(isinstance(x, dict) for x in (pol, gate)):
            raise Bad("policy and gate must be objects")
        if not isinstance(evals, list) or not (1 <= len(evals) <= 100):
            raise Bad("evaluations must be a list of 1-100 entries")

        # binding
        digest = pol.get("artifact_sha256")
        c("binding.artifact_hash", isinstance(digest, str) and bool(_HEX.match(digest)), f"artifact {digest!r}")
        c("binding.model_card", isinstance(pol.get("model_card"), str) and bool(pol["model_card"].strip()),
          "a model card is named")
        prov = pol.get("training_data_provenance")
        c("binding.data_provenance", isinstance(prov, dict) and bool(prov.get("source")) and bool(prov.get("license")),
          f"source {prov.get('source')!r}, license {prov.get('license')!r}")

        # gate inputs
        min_trials = _int(gate.get("min_trials"), "gate.min_trials", 1, 1_000_000)
        threshold = _num(gate.get("min_success_rate"), "gate.min_success_rate", 0.0, 1.0)
        max_safety = _int(gate.get("max_safety_violations"), "gate.max_safety_violations", 0, 1_000_000)
        max_reg = _num(gate.get("max_regression"), "gate.max_regression", 0.0, 1.0)
        required = gate.get("required_scenarios")
        if not isinstance(required, list) or not required or not all(isinstance(x, str) and _ID.match(x) for x in required):
            raise Bad("gate.required_scenarios must be a non-empty list of identifiers")
        if len(set(required)) != len(required):
            raise Bad("gate.required_scenarios repeats a scenario")

        seen, total_safety, critical = {}, 0, 0
        for e in evals:
            if not isinstance(e, dict) or not isinstance(e.get("scenario"), str) or not _ID.match(e["scenario"]):
                raise Bad("each evaluation needs a scenario identifier")
            name = e["scenario"]
            if name in seen:
                raise Bad(f"scenario {name} is evaluated twice")
            seen[name] = e
            trials = _int(e.get("trials"), f"{name}.trials", 1, 1_000_000)
            success = _int(e.get("successes"), f"{name}.successes", 0, trials)
            rate = _num(e.get("reported_rate"), f"{name}.reported_rate", 0.0, 1.0)
            c(f"{name}.rate_arithmetic", abs(rate - success / trials) <= 1e-9,
              f"reported {rate}, successes/trials {success}/{trials}")
            lb = wilson_lower(success, trials)
            summary["scenarios"][name] = {"trials": trials, "successes": success, "rate": round(success / trials, 6)}
            summary["wilson_lower"][name] = round(lb, 6)
            if name in required:
                c(f"{name}.min_trials", trials >= min_trials, f"{trials} >= {min_trials}")
                c(f"{name}.wilson_lower", lb >= threshold, f"lower bound {round(lb, 6)} >= gate {threshold}")
            sv = _int(e.get("safety_violations"), f"{name}.safety_violations", 0, 1_000_000)
            cv = _int(e.get("critical_violations"), f"{name}.critical_violations", 0, 1_000_000)
            total_safety += sv
            critical += cv
            c(f"{name}.no_critical_violation", cv == 0, f"{cv} critical violation(s)")

        missing = [s for s in required if s not in seen]
        c("coverage.required_scenarios", not missing, f"missing {missing}" if missing else "all required scenarios present")
        c("safety.budget", total_safety <= max_safety, f"{total_safety} <= {max_safety} total violation(s)")
        c("safety.any_critical_blocks", critical == 0, f"{critical} critical violation(s) across the run")

        # regression against the previous release
        base = release.get("baseline") if isinstance(release.get("baseline"), dict) else {}
        regressions = []
        for name, e in seen.items():
            if name in base:
                b = _num(base[name], f"baseline.{name}", 0.0, 1.0)
                r = e["successes"] / e["trials"]
                if b - r > max_reg + 1e-12:
                    regressions.append(f"{name}: {round(b - r, 4)} below previous release")
        c("regression.within_margin", not regressions, "; ".join(regressions))

        # sign-offs bound to this artifact
        signoffs = release.get("signoffs") if isinstance(release.get("signoffs"), list) else []
        roles = {}
        for s in signoffs:
            if not isinstance(s, dict) or not isinstance(s.get("role"), str):
                raise Bad("a sign-off needs a role")
            roles[s["role"]] = s
        required_roles = gate.get("required_signoff_roles")
        if not isinstance(required_roles, list) or not required_roles:
            raise Bad("gate.required_signoff_roles must be a non-empty list")
        for role in required_roles:
            s = roles.get(role)
            c(f"signoff.{role}", bool(s) and s.get("decision") == "accept", f"{role}: {s and s.get('decision')}")
            c(f"signoff.{role}.current", bool(s) and s.get("artifact_sha256") == digest,
              f"{role} signed {s and s.get('artifact_sha256')}")

        # honesty
        sim_only = release.get("simulation_only") is True
        hardware = release.get("hardware_validated") is True
        claims = release.get("claims") if isinstance(release.get("claims"), dict) else {}
        computed = {
            "passed": all(x["ok"] for x in checks),
            "hardware_validated": hardware and not sim_only,
            "simulation_only": sim_only,
        }
        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}")
        c("honesty.no_hardware_claim_from_simulation", not (sim_only and hardware),
          "a SIMULATED-only run cannot be hardware validated")
        summary["computed"] = computed
    except Bad as e:
        checks.append({"id": "input.well_formed", "ok": False, "detail": str(e)})
        return "BLOCK", checks, summary
    except Exception as e:  # noqa: BLE001
        checks.append({"id": "input.error", "ok": False, "detail": f"{type(e).__name__}: {e}"[:200]})
        return "BLOCK", checks, summary
    return ("PASS" if all(x["ok"] for x in checks) else "BLOCK"), 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.robotics.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-008, {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))
