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

An always-on agent works under a lease. The review recomputes whether the lease is still valid and still inside delegated
authority:

  term        the lease has not expired at the supplied clock and is not longer than the policy maximum
  delegation  the principal's delegation chain starts at a human, every edge is unexpired, and the lease scope is a
              subset of the authority that reached the agent (no escalation)
  renewal     renewals are within the policy limit; a renewal past the approval threshold carries approval by someone
              other than the agent
  heartbeat   missed heartbeats are within the policy; past the limit the lease must be revoked
  attestation the running agent version is named by hash and the attestation verified
  claims      every boolean the record asserts matches what this review computed

Verdict: ACTIVE, else REVOKED with every failing check named. Malformed input is REVOKED.
It reviews the lease record; it does not contact the agent.
"""
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 = [], {"renewals_used": 0, "missed_heartbeats": 0}

    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.lease.v1":
            raise Bad("not a cain42.a75.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")
        if not isinstance(lease.get("id"), str) or not _ID.match(lease["id"]):
            raise Bad("lease.id must be an identifier")
        now = _num(lease.get("now"), "lease.now")
        issued = _num(lease.get("issued_at"), "lease.issued_at")
        exp = _num(lease.get("expires_at"), "lease.expires_at")
        max_hours = _num(pol.get("max_lease_hours"), "policy.max_lease_hours", 1.0, 87600.0)
        duration_h = (exp - issued) / 3600.0
        c("term.not_expired", exp > now, f"expires {exp} > now {now}")
        c("term.within_max", duration_h <= max_hours + 1e-9, f"{round(duration_h, 3)}h <= {max_hours}h")
        summary["duration_hours"] = round(duration_h, 3)

        # delegation: human -> ... -> agent
        chain = record.get("delegation")
        if not isinstance(chain, list) or not chain:
            raise Bad("delegation must be a non-empty chain")
        prev_to = None
        granted = None
        for i, e in enumerate(chain):
            if not isinstance(e, dict) or not isinstance(e.get("from"), str) or not isinstance(e.get("to"), str):
                raise Bad("a delegation edge needs from and to")
            if prev_to is not None and e["from"] != prev_to:
                raise Bad(f"delegation edge {i} does not continue the chain")
            prev_to = e["to"]
            c(f"delegation.{i}.from_human", e["from"] == "human" if i == 0 else True,
              f"root is {e['from']!r}")
            c(f"delegation.{i}.not_expired", _num(e.get("expires_at"), f"delegation.{i}.expires_at") > now,
              "edge unexpired")
            scope = e.get("scope") if isinstance(e.get("scope"), dict) else {}
            tools = set(scope.get("tools") or [])
            resources = set(scope.get("resources") or [])
            if granted is None:
                granted = {"tools": tools, "resources": resources}
            else:
                if not tools <= granted["tools"] or not resources <= granted["resources"]:
                    c(f"delegation.{i}.attenuated", False, "an edge widened authority")
                else:
                    c(f"delegation.{i}.attenuated", True, "scope narrows or matches the parent")
                granted = {"tools": tools, "resources": resources}
        c("delegation.chain_ends_at_agent", prev_to == lease.get("agent"), f"chain ends at {prev_to!r}")
        lscope = lease.get("scope") if isinstance(lease.get("scope"), dict) else {}
        lt = set(lscope.get("tools") or [])
        lr = set(lscope.get("resources") or [])
        c("delegation.lease_scope_subset", granted is None or (lt <= granted["tools"] and lr <= granted["resources"]),
          f"lease scope tools={sorted(lt)} resources={sorted(lr)}")

        ren = lease.get("renewal") if isinstance(lease.get("renewal"), dict) else {}
        used = _int(ren.get("renewals_used"), "renewal.renewals_used", 0)
        max_ren = _int(pol.get("max_renewals"), "policy.max_renewals", 0)
        summary["renewals_used"] = used
        c("renewal.within_limit", used <= max_ren, f"{used} <= {max_ren}")
        threshold = _int(pol.get("approval_after_renewals"), "policy.approval_after_renewals", 0)
        if used > threshold:
            c("renewal.approved", isinstance(ren.get("approved_by"), str) and ren["approved_by"] not in ("", lease.get("agent")),
              f"approved by {ren.get('approved_by')!r}")
        else:
            c("renewal.approval_not_required", True, "below the approval threshold")

        hb = lease.get("heartbeat") if isinstance(lease.get("heartbeat"), dict) else {}
        missed = _int(hb.get("missed"), "heartbeat.missed", 0)
        summary["missed_heartbeats"] = missed
        allowed_missed = _int(pol.get("max_missed_heartbeats", 2), "policy.max_missed_heartbeats", 0)
        c("heartbeat.within_limit", missed <= allowed_missed, f"{missed} <= {allowed_missed} missed")
        _num(hb.get("last_seen_seconds_ago"), "heartbeat.last_seen_seconds_ago", 0.0)
        _num(hb.get("interval_s"), "heartbeat.interval_s", 1.0)

        att = lease.get("attestation") if isinstance(lease.get("attestation"), dict) else {}
        c("attestation.version_hash", isinstance(att.get("agent_version_sha256"), str) and bool(_HEX.match(att["agent_version_sha256"])),
          "the running version is hashed")
        c("attestation.verified", att.get("verified") is True, "the attestation verified")

        claims = record.get("claims") if isinstance(record.get("claims"), dict) else {}
        computed = {
            "lease_valid": exp > now and duration_h <= max_hours and missed <= allowed_missed,
            "scope_attenuated": granted is not None and lt <= granted["tools"] and lr <= granted["resources"],
            "human_approved": used <= threshold or (isinstance(ren.get("approved_by"), str) and ren["approved_by"] not in ("", lease.get("agent"))),
        }
        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 "REVOKED", checks, summary
    except Exception as e:  # noqa: BLE001
        checks.append({"id": "input.error", "ok": False, "detail": f"{type(e).__name__}: {e}"[:200]})
        return "REVOKED", checks, summary
    return ("ACTIVE" if all(x["ok"] for x in checks) else "REVOKED"), 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.lease.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-027, {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))
