"""CAIN-A75-013 offline verifier (rules embedded verbatim). CAIN-A75-013 model-orchestration rules. Standard library only, no CAIN imports: embedded verbatim in the offline
verifier.

A multi-model orchestrator routes tasks across providers. The review recomputes whether every route was allowed and
whether the run stayed inside its policy:

  models      unique ids, known providers, declared capabilities, context window and cost
  routes      the chosen model exists, covers the task's required capabilities, fits the context, uses an allowed
              provider, and satisfies data-residency
  cost        estimated cost (tokens x price) is within the per-task ceiling; the run total is within the run budget
  fallback    every fallback exists, differs from the primary and from the other fallbacks
  review      a task flagged high-risk must be reviewed by a model from a DIFFERENT provider than the actor
  outcomes    every routed task has an outcome; success rate and total cost meet the gate
  claims      every boolean the record asserts matches what this review computed

Verdict: SOUND, else UNSOUND with every failing check named. Malformed input is UNSOUND.
It reviews supplied routing and outcome records; it does not call any model.
"""
from __future__ import annotations

import math
import re

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


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 ** 12):
    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 = [], {"routes": {}, "total_cost": 0.0}

    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.orchestration.record.v1":
            raise Bad("not a cain42.a75.orchestration.record.v1 record")
        models, routes, policy = record.get("models"), record.get("routes"), record.get("policy")
        if not isinstance(models, list) or not models or not isinstance(routes, list) or not routes:
            raise Bad("models and routes must be non-empty lists")
        if not isinstance(policy, dict):
            raise Bad("policy must be an object")
        M = {}
        for m in models:
            if not isinstance(m, dict) or not isinstance(m.get("id"), str) or not _ID.match(m["id"]):
                raise Bad("a model needs an id")
            if m["id"] in M:
                raise Bad(f"duplicate model {m['id']}")
            if not isinstance(m.get("provider"), str) or not m["provider"]:
                raise Bad(f"{m['id']}: provider required")
            if not isinstance(m.get("capabilities"), list) or not all(isinstance(x, str) for x in m["capabilities"]):
                raise Bad(f"{m['id']}: capabilities must be strings")
            _int(m.get("context_window"), f"{m['id']}.context_window", 1)
            _num(m.get("cost_per_1k"), f"{m['id']}.cost_per_1k", 0.0)
            M[m["id"]] = m
        allowed = policy.get("allowed_providers")
        if not isinstance(allowed, list) or not allowed:
            raise Bad("policy.allowed_providers must be a non-empty list")
        max_task = _num(policy.get("max_cost_per_task"), "policy.max_cost_per_task", 0.0)
        run_budget = _num(policy.get("run_budget"), "policy.run_budget", 0.0)
        min_success = _num(policy.get("min_success_rate"), "policy.min_success_rate", 0.0, 1.0)

        tasks = {}
        for r in routes:
            if not isinstance(r, dict) or not isinstance(r.get("task_id"), str) or not _ID.match(r["task_id"]):
                raise Bad("a route needs a task_id")
            if r["task_id"] in tasks:
                raise Bad(f"task {r['task_id']} routed twice")
            tasks[r["task_id"]] = r
            chosen = r.get("model")
            m = M.get(chosen)
            c(f"{r['task_id']}.model_exists", m is not None, f"model {chosen!r}")
            if m is None:
                continue
            req = r.get("required_capabilities") if isinstance(r.get("required_capabilities"), list) else []
            c(f"{r['task_id']}.capabilities", set(req) <= set(m["capabilities"]), f"needs {req}, model has {m['capabilities']}")
            tokens = _int(r.get("tokens"), f"{r['task_id']}.tokens", 1)
            c(f"{r['task_id']}.context_fits", tokens <= m["context_window"], f"{tokens} <= {m['context_window']}")
            c(f"{r['task_id']}.provider_allowed", m["provider"] in allowed, f"provider {m['provider']!r} in {allowed}")
            residency = r.get("data_residency")
            c(f"{r['task_id']}.residency", residency is None or residency in (m.get("residency") or [residency]),
              f"data residency {residency!r}")
            cost = tokens / 1000.0 * m["cost_per_1k"]
            summary["routes"][r["task_id"]] = {"model": chosen, "provider": m["provider"], "cost": round(cost, 6)}
            c(f"{r['task_id']}.cost_ceiling", cost <= max_task + 1e-12, f"{round(cost, 6)} <= {max_task}")
            summary["total_cost"] += cost
            chain = r.get("fallback_chain") if isinstance(r.get("fallback_chain"), list) else []
            c(f"{r['task_id']}.fallback_exists", all(f in M for f in chain) and bool(chain), f"chain {chain}")
            c(f"{r['task_id']}.fallback_distinct", len(set(chain)) == len(chain) and chosen not in chain,
              f"primary {chosen!r}, chain {chain}")
            if r.get("high_risk") is True:
                rev = r.get("reviewer") if isinstance(r.get("reviewer"), dict) else {}
                rm = M.get(rev.get("model"))
                c(f"{r['task_id']}.review_present", rm is not None, f"reviewer {rev.get('model')!r}")
                c(f"{r['task_id']}.review_cross_lab", rm is not None and rm["provider"] != m["provider"],
                  f"reviewer provider {rm and rm['provider']!r} vs actor {m['provider']!r}")
            else:
                c(f"{r['task_id']}.review_not_required", True, "not flagged high-risk")

        outcomes = record.get("outcomes") if isinstance(record.get("outcomes"), list) else []
        got = {}
        for o in outcomes:
            if not isinstance(o, dict) or o.get("task_id") not in tasks:
                raise Bad("an outcome must name a routed task")
            if o["task_id"] in got:
                raise Bad(f"duplicate outcome for {o['task_id']}")
            if o.get("success") not in (True, False):
                raise Bad(f"{o['task_id']}: success must be true or false")
            got[o["task_id"]] = o
        missing = sorted(set(tasks) - set(got))
        c("outcomes.cover_routes", not missing, f"missing {missing}" if missing else "every route has an outcome")
        rate = (sum(1 for o in got.values() if o["success"]) / len(tasks)) if tasks else 0.0
        c("outcomes.success_rate", rate >= min_success, f"{round(rate, 4)} >= {min_success}")
        summary["success_rate"] = round(rate, 4)
        summary["total_cost"] = round(summary["total_cost"], 6)
        c("cost.run_budget", summary["total_cost"] <= run_budget + 1e-9, f"{summary['total_cost']} <= {run_budget}")

        claims = record.get("claims") if isinstance(record.get("claims"), dict) else {}
        computed = {
            "all_routes_valid": all(x["ok"] for x in checks if "." in x["id"] and not x["id"].startswith(("outcomes.", "cost.", "claims."))),
            "within_budget": summary["total_cost"] <= run_budget + 1e-9,
            "success_meets_gate": rate >= min_success,
        }
        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 "UNSOUND", checks, summary
    except Exception as e:  # noqa: BLE001
        checks.append({"id": "input.error", "ok": False, "detail": f"{type(e).__name__}: {e}"[:200]})
        return "UNSOUND", checks, summary
    return ("SOUND" if all(x["ok"] for x in checks) else "UNSOUND"), 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.orchestration.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-013, {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))
