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

A coding agent works inside a native development tool. The gate recomputes whether every proposed action was allowed:

  authority   a named principal, a scope that covers this tool and workspace, and an expiry after the supplied clock
  kinds       every action kind is in the policy's allowed set
  paths       an edit target is inside the allowlist and not under a forbidden path (.git, .env, secrets, system dirs)
  commands    a run command matches one allowlisted prefix and carries no shell metacharacters
  network     a network destination is in the allowlist, with no catch-all
  approvals   a kind that requires approval has a live approval, for this action, from someone other than the agent
  effects     an applied action carries a diff hash; a blocked action is not marked applied
  claims      every boolean the record asserts matches what this gate computed

Verdict: ALLOWED, else BLOCKED with every failing check named. Malformed input is BLOCKED.
It reviews the action record; it does not execute the commands or touch the workspace.
"""
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}$")
KINDS = {"read", "edit", "run", "network", "install"}
SHELL_META = re.compile(r"[;&|`$><\n\r]")
FORBIDDEN_PARTS = (".git/", ".env", "id_rsa", "secrets/", "/etc/", "/root/", "Library/Keychains")


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 _under(path, roots):
    return any(path == r or path.startswith(r.rstrip("/") + "/") for r in roots)


def evaluate(record):
    checks, summary = [], {"actions": {}, "blocked": []}

    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.coding.gate.v1":
            raise Bad("not a cain42.a75.coding.gate.v1 record")
        sess, pol, auth = record.get("session"), record.get("policy"), record.get("authority")
        if not all(isinstance(x, dict) for x in (sess, pol, auth)):
            raise Bad("session, policy and authority must be objects")
        if sess.get("tool") not in ("xcode", "vscode", "jetbrains", "cursor", "terminal"):
            raise Bad("session.tool must be a known development tool")
        if not isinstance(sess.get("agent"), str) or not _ID.match(sess["agent"]):
            raise Bad("session.agent must be an identifier")
        if not isinstance(sess.get("workspace"), str) or not sess["workspace"].strip():
            raise Bad("session.workspace required")
        now, exp = _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.not_expired", exp > now, f"expires {exp} > now {now}")
        scope = auth.get("scope") if isinstance(auth.get("scope"), dict) else {}
        c("authority.tool_scope", sess["tool"] in (scope.get("tools") or []), f"tools {scope.get('tools')!r}")
        c("authority.workspace_scope", sess["workspace"] in (scope.get("workspaces") or []), f"workspaces {scope.get('workspaces')!r}")

        allowed_kinds = pol.get("allowed_kinds")
        if not isinstance(allowed_kinds, list) or not all(k in KINDS for k in allowed_kinds):
            raise Bad("policy.allowed_kinds must be a subset of the known kinds")
        path_allow = pol.get("path_allowlist") if isinstance(pol.get("path_allowlist"), list) else []
        net_allow = pol.get("network_allowlist") if isinstance(pol.get("network_allowlist"), list) else []
        run_allow = pol.get("command_allowlist") if isinstance(pol.get("command_allowlist"), list) else []
        require = pol.get("require_approval") if isinstance(pol.get("require_approval"), list) else []
        c("policy.allowlists_present", bool(path_allow) and bool(run_allow), "path and command allowlists exist")
        c("policy.network_no_catch_all", not any(x in ("0.0.0.0/0", "::/0", "*") for x in net_allow),
          f"network allowlist {net_allow}")

        approvals = {}
        for ap in record.get("approvals") or []:
            if not isinstance(ap, dict) or not isinstance(ap.get("action_id"), str):
                raise Bad("an approval needs an action_id")
            approvals[ap["action_id"]] = ap

        actions = record.get("actions")
        if not isinstance(actions, list) or not actions or len(actions) > 500:
            raise Bad("actions must be a non-empty list")
        effects = {e.get("action_id"): e for e in (record.get("effects") or []) if isinstance(e, dict)}
        seen = set()
        for a in actions:
            if not isinstance(a, dict) or not isinstance(a.get("id"), str) or not _ID.match(a["id"]):
                raise Bad("an action needs an id")
            if a["id"] in seen:
                raise Bad(f"duplicate action {a['id']}")
            seen.add(a["id"])
            kind = a.get("kind")
            if kind not in KINDS:
                raise Bad(f"{a['id']}: kind must be one of {sorted(KINDS)}")
            ok_kind = kind in allowed_kinds
            c(f"{a['id']}.kind", ok_kind, f"kind {kind!r}, allowed {allowed_kinds}")
            target = a.get("target")
            if not isinstance(target, str) or not target.strip():
                raise Bad(f"{a['id']}: target required")
            ok_target = True
            if kind == "read":
                ok_target = not any(p in target for p in FORBIDDEN_PARTS) or _under(target, path_allow)
            elif kind == "edit":
                forbidden = any(p in target for p in FORBIDDEN_PARTS)
                ok_target = _under(target, path_allow) and not forbidden
                c(f"{a['id']}.not_forbidden_path", not forbidden, f"target {target!r}")
            elif kind == "run":
                meta = bool(SHELL_META.search(target))
                ok_target = any(target.startswith(p) for p in run_allow) and not meta
                c(f"{a['id']}.no_shell_meta", not meta, "no shell metacharacters")
            elif kind == "network":
                ok_target = target in net_allow and target not in ("0.0.0.0/0", "::/0", "*")
            c(f"{a['id']}.target", ok_target, f"target {target!r}")

            needs = kind in require
            ap = approvals.get(a["id"])
            if needs:
                c(f"{a['id']}.approval_present", ap is not None, "approval required")
                if ap is not None:
                    c(f"{a['id']}.approval_live", _num(ap.get("expires_at"), f"approval.{a['id']}.expires_at") > now,
                      "approval not expired")
                    c(f"{a['id']}.approval_independent", ap.get("approved_by") != sess["agent"], f"approved by {ap.get('approved_by')!r}")
                    c(f"{a['id']}.approval_scope", a["id"] in (ap.get("action_ids") or [a["id"]]), "approval covers this action")
            else:
                c(f"{a['id']}.approval_not_required", True, "approval not required for this kind")

            eff = effects.get(a["id"])
            applied = bool(eff and eff.get("applied") is True)
            allowed = ok_kind and ok_target
            c(f"{a['id']}.effect_consistent", (applied and allowed) or (not applied),
              f"applied={applied}, allowed_by_rules={allowed}")
            if applied:
                if kind == "edit":
                    c(f"{a['id']}.diff_hash", isinstance(eff.get("diff_sha256"), str) and bool(_HEX.match(eff["diff_sha256"])),
                      "an applied edit carries a diff hash")
            summary["actions"][a["id"]] = {"kind": kind, "target": target, "allowed": allowed, "applied": applied}
            if not allowed:
                summary["blocked"].append(a["id"])

        claims = record.get("claims") if isinstance(record.get("claims"), dict) else {}
        computed = {
            "all_actions_allowed": not summary["blocked"],
            "no_forbidden_write": not any(v["kind"] == "edit" and not v["allowed"] for v in summary["actions"].values()),
            "approvals_valid": all(not (v["kind"] in require and v["kind"] == "install") or aid in approvals
                                   for aid, v in summary["actions"].items()),
        }
        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 "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 ("ALLOWED" 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.codinggate.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-024, {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))
