"""CAIN-A75-042 offline verifier (rules embedded verbatim). CAIN-A75-042 background-lease rules. Standard library only, no CAIN imports: embedded verbatim in the offline verifier.

A creative agent runs continuously in the background. The review recomputes whether the lease is still valid and whether
it respected its schedule and publication rules:

  term       the lease has not expired at the supplied clock
  quota      runs and cost are within the lease's limits
  schedule   a run falls inside the allowed window and outside quiet hours
  failures   after the failure threshold the job must be paused
  publication an output is published only with independent approval, and carries its content hash
  claims     every boolean the record asserts matches what this review computed

Verdict: ACTIVE, else BLOCKED with every failing check named. Malformed input is BLOCKED.
It reviews the lease record; it does not control the scheduler.
"""
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 _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 _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 evaluate(record):
    checks, summary = [], {"outputs": {}}

    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.background.lease.v1":
            raise Bad("not a cain42.a75.background.lease.v1 record")
        lease, pol = record.get("lease"), record.get("policy")
        if not all(isinstance(x, dict) for x in (lease, pol)):
            raise Bad("lease and policy must be objects")
        now = _num(lease.get("now"), "lease.now")
        exp = _num(lease.get("expires_at"), "lease.expires_at")
        c("term.not_expired", exp > now, f"expires {exp} > now {now}")
        c("term.paused", lease.get("paused") is not True, "the job is not paused")

        quota = lease.get("quota") if isinstance(lease.get("quota"), dict) else {}
        runs = _int(quota.get("runs_used"), "quota.runs_used", 0)
        max_runs = _int(pol.get("max_runs"), "policy.max_runs", 1)
        cost = _num(quota.get("cost_used"), "quota.cost_used", 0.0)
        max_cost = _num(pol.get("max_cost_usd"), "policy.max_cost_usd", 0.0)
        c("quota.runs", runs <= max_runs, f"{runs} <= {max_runs}")
        c("quota.cost", cost <= max_cost + 1e-9, f"{cost} <= {max_cost}")
        summary["runs_used"] = runs
        summary["cost_used"] = cost

        sched = record.get("schedule") if isinstance(record.get("schedule"), dict) else {}
        hour = _num(sched.get("next_run_hour"), "schedule.next_run_hour", 0.0, 23.0)
        start = _num(pol.get("window_start_hour"), "policy.window_start_hour", 0.0, 23.0)
        end = _num(pol.get("window_end_hour"), "policy.window_end_hour", 0.0, 23.0)
        in_window = (start <= hour <= end) if start <= end else (hour >= start or hour <= end)
        c("schedule.in_window", in_window, f"hour {hour} in [{start}, {end}]")
        quiet = pol.get("quiet_hours") if isinstance(pol.get("quiet_hours"), list) else []
        c("schedule.not_quiet", not any(isinstance(q, dict) and _num(q.get("start"), "quiet.start", 0.0, 23.0) <= hour <= _num(q.get("end"), "quiet.end", 0.0, 23.0) for q in quiet),
          "the run is outside quiet hours")

        failures = record.get("failures") if isinstance(record.get("failures"), dict) else {}
        consecutive = _int(failures.get("consecutive"), "failures.consecutive", 0)
        threshold = _int(pol.get("failure_threshold"), "policy.failure_threshold", 1)
        if consecutive >= threshold:
            c("failures.paused", lease.get("paused") is True, f"{consecutive} >= {threshold}: job must be paused")
        else:
            c("failures.below_threshold", True, f"{consecutive} < {threshold}")

        outputs = record.get("outputs") if isinstance(record.get("outputs"), list) else []
        for o in outputs:
            if not isinstance(o, dict) or not isinstance(o.get("id"), str) or not _ID.match(o["id"]):
                raise Bad("an output needs an id")
            c(f"{o['id']}.hash", isinstance(o.get("sha256"), str) and bool(_HEX.match(o["sha256"])), "output hash present")
            if o.get("published") is True:
                ap = o.get("approval") if isinstance(o.get("approval"), dict) else {}
                c(f"{o['id']}.approval", ap.get("by") not in (None, "", lease.get("agent")), f"approved by {ap.get('by')!r}")
                c(f"{o['id']}.approval_time", isinstance(ap.get("at"), str) and bool(ap["at"]), "approval timestamp")
            summary["outputs"][o["id"]] = {"published": o.get("published") is True}

        claims = record.get("claims") if isinstance(record.get("claims"), dict) else {}
        computed = {
            "within_quota": runs <= max_runs and cost <= max_cost + 1e-9,
            "quiet_hours_respected": not any(isinstance(q, dict) and _num(q.get("start"), "quiet.start", 0.0, 23.0) <= hour <= _num(q.get("end"), "quiet.end", 0.0, 23.0) for q in quiet),
            "publish_approved": all(
                o.get("published") is not True or (isinstance(o.get("approval"), dict)
                                                   and o["approval"].get("by") not in (None, "", lease.get("agent")))
                for o in outputs),
        }
        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
        summary["in_window"] = in_window
    except Bad as e:
        checks.append({"id": "input.well_formed", "ok": False, "detail": str(e)})
        return "BLOCKED", checks, summary
    except Exception as e:  # noqa: BLE001
        checks.append({"id": "input.error", "ok": False, "detail": f"{type(e).__name__}: {e}"[:200]})
        return "BLOCKED", checks, summary
    return ("ACTIVE" if all(x["ok"] for x in checks) else "BLOCKED"), 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.background.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-042, {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))
