#!/usr/bin/env python3
"""Standalone E72 evidence verifier.

Imports only the standard library and `cryptography`. It does not import CAIN, does not contact a coordinator, and
does not accept a self-reported PASS: each gate that claims PASS or PARTIAL has its supporting file checked, and the
key arithmetic claims are recomputed from the shipped records.

    python3 verify_e72_offline.py <bundle-dir> [--pinned-pub BASE64] [--json]

Exit 0 = VERIFIED, 1 = NOT_VERIFIED, 2 = usage error.
"""
from __future__ import annotations

import base64
import hashlib
import json
import math
import os
import sys
from typing import Any, Dict, List, Mapping, Optional

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric import ed25519

SCHEMA = "cain72.evidence.bundle.v1"
DOMAIN = "cain72.evidence.manifest.v1"
MISSION_SCHEMA = "cain68.mission.evidence.bundle.v5"
MISSION_DOMAIN = "cain68.evidence.bundle.v5"
GATE_EVIDENCE_PREFIX = "artifacts/e72/"


def norm(o: Any) -> Any:
    if isinstance(o, float):
        if not math.isfinite(o):
            raise ValueError("NON_FINITE_NUMBER")
        return int(o) if o.is_integer() else o
    if isinstance(o, Mapping):
        return {str(k): norm(v) for k, v in o.items()}
    if isinstance(o, (list, tuple)):
        return [norm(v) for v in o]
    return o


def canon(o: Any) -> bytes:
    return json.dumps(norm(o), sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()


def sha(o: Any) -> str:
    return hashlib.sha256(canon(o)).hexdigest()


def digest_file(path: str) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()


def verify_sig(pub: str, sig: str, domain: str, body: Mapping[str, Any]) -> bool:
    try:
        ed25519.Ed25519PublicKey.from_public_bytes(base64.b64decode(pub)).verify(
            base64.b64decode(sig), domain.encode() + b"\0" + canon(body))
        return True
    except (InvalidSignature, ValueError, TypeError):
        return False


def read(path: str) -> Any:
    with open(path, "rb") as f:
        return json.loads(f.read().decode())


def main(argv=None) -> int:
    import argparse
    ap = argparse.ArgumentParser()
    ap.add_argument("bundle")
    ap.add_argument("--pinned-pub", default=None)
    ap.add_argument("--json", action="store_true")
    a = ap.parse_args(argv)
    b = os.path.abspath(a.bundle)
    problems: List[str] = []
    checks: List[str] = []
    manifest_path = os.path.join(b, "manifest.json")
    if not os.path.isfile(manifest_path):
        print(json.dumps({"ok": False, "problems": ["manifest_missing"], "checks": []}))
        return 1
    m = read(manifest_path)
    if m.get("schema") != SCHEMA:
        problems.append("schema_invalid")
    body = {k: v for k, v in m.items() if k != "signature"}
    pinned = read(os.path.join(b, "pinned-key.json")) if os.path.isfile(os.path.join(b, "pinned-key.json")) else {}
    pub = str(pinned.get("pub") or "")
    if a.pinned_pub and pub != a.pinned_pub:
        problems.append("pinned_key_mismatch")
    if not pub:
        problems.append("pinned_key_missing")
    elif not verify_sig(pub, str(m.get("signature") or ""), DOMAIN, body):
        problems.append("manifest_signature_invalid")
    else:
        checks.append("manifest_signature")
    fingerprint = hashlib.sha256(base64.b64decode(pub)).hexdigest() if pub else ""

    files = m.get("files") or {}
    for rel, meta in files.items():
        path = os.path.join(b, rel)
        if not os.path.isfile(path):
            problems.append(f"file_missing:{rel}")
            continue
        d = digest_file(path)
        if d != meta.get("sha256"):
            problems.append(f"file_digest_mismatch:{rel}")
        elif int(meta.get("bytes") or -1) != os.path.getsize(path):
            problems.append(f"file_size_mismatch:{rel}")
    checks.append(f"files_checked:{len(files)}")
    if not files:
        problems.append("no_files")

    def data(rel: str) -> Any:
        p = os.path.join(b, rel)
        return read(p) if os.path.isfile(p) else None

    # ---------------- gate evidence presence and recomputed claims
    gates = data(GATE_EVIDENCE_PREFIX + "gates.json") or {}
    gate_rows = gates.get("gates") or []
    if not gate_rows:
        problems.append("gates_missing")
    valid_status = {"PASS", "PARTIAL", "BLOCKED", "FAIL"}
    for g in gate_rows:
        if g.get("status") not in valid_status:
            problems.append(f"gate_status_invalid:{g.get('id')}")
        if g.get("status") in ("PASS", "PARTIAL"):
            ev = [e for e in (g.get("evidence") or []) if e]
            if not ev:
                problems.append(f"gate_without_evidence:{g.get('id')}")
            for e in ev:
                rel = e
                if rel not in files and not any(k.startswith(rel.rstrip("/") + "/") for k in files):
                    problems.append(f"gate_evidence_missing:{g.get('id')}:{e}")
        if g.get("status") == "FAIL":
            problems.append(f"gate_failed:{g.get('id')}")
    checks.append(f"gates_checked:{len(gate_rows)}")

    def expect(cond: bool, name: str) -> None:
        if cond:
            checks.append(name)
        else:
            problems.append(name)

    sched = data(GATE_EVIDENCE_PREFIX + "scheduling.json") or {}
    if sched:
        levels = sched.get("levels") or []
        caps = [int((r.get("controller_snapshot") or {}).get("policy", {}).get("hard_cap") or 0) for r in levels]
        expect(all(r.get("max_observed_busy", 0) <= c for r, c in zip(levels, caps)) if caps else False,
               "scheduling_hard_cap_respected")
        expect(int(sched.get("comparison", {}).get("lease_expiries_controller", -1)) == 0,
               "scheduling_zero_lease_expiries")
        expect(int(sched.get("comparison", {}).get("lease_expiries_naive_lease_failure", 0)) > 0,
               "naive_lease_failure_preserved")
        expect(all(r.get("mission_state") == "COMPLETED" for r in levels), "scheduling_missions_completed")

    models = data(GATE_EVIDENCE_PREFIX + "models.json") or {}
    if models:
        expect(int(models.get("real_remote_calls", -1)) == 0, "no_remote_model_call")
        expect(set((models.get("execution_classes") or {}).keys()) <= {"REAL_LOCAL_MODEL"}, "local_execution_only")
        expect((models.get("fallback") or {}).get("recovered") is True, "fallback_recovered")

    mem = data(GATE_EVIDENCE_PREFIX + "memory-eval.json") or {}
    if mem:
        expect(int(mem.get("authority_violations", -1)) == 0, "memory_no_authority")
        expect((mem.get("injection") or {}).get("recalled") is False, "memory_injection_not_recalled")
        expect((mem.get("arms", {}).get("B_verified_memory", {}).get("accuracy", 0)
                > mem.get("arms", {}).get("A_no_memory", {}).get("accuracy", 0)), "memory_heldout_improvement")

    cross = data(GATE_EVIDENCE_PREFIX + "cross-host.json") or {}
    if cross:
        r = cross.get("result_from_host_b") or {}
        expect(cross.get("distinct_hosts") is True, "cross_host_distinct")
        expect(len(r.get("verified_execute_by_host") or {}) == 2, "cross_host_both_executed")
        expect(int(r.get("duplicate_results", -1)) == 0, "cross_host_no_duplicates")

    rec = data(GATE_EVIDENCE_PREFIX + "recovery.json") or {}
    if rec:
        expect(rec.get("host_reboot") == "BLOCKED", "host_reboot_blocked")
        expect((rec.get("worker_kill") or {}).get("mission_state") == "COMPLETED", "worker_kill_recovered")
        expect((rec.get("database_lock") or {}).get("failed_closed") is True, "db_lock_fails_closed")

    econ = data(GATE_EVIDENCE_PREFIX + "economics.json") or {}
    if econ:
        for r in econ.get("runs") or []:
            expect(float(r.get("verified_productive_hours", 0)) <= float(r.get("worker_hours_process_alive", 0)) + 1e-6,
                   f"productive_le_alive:{r.get('workers')}")

    lh = data(GATE_EVIDENCE_PREFIX + "long-horizon.json") or {}
    if lh:
        expect(float(lh.get("verified_productive_hours", 0)) <= float(lh.get("worker_hours_process_alive", 0)) + 1e-6,
               "long_horizon_productive_le_alive")
        expect(float(lh.get("actual_elapsed_seconds", 0)) < 24 * 3600, "no_multiday_claim")
        expect(int(lh.get("cycles", 0)) > 0 and int(lh.get("verified_execute_tasks", 0)) > 0,
               "long_horizon_cycles_present")

    bench = data(GATE_EVIDENCE_PREFIX + "benchmark.json") or {}
    if bench:
        for name, f in (bench.get("families") or {}).items():
            if isinstance(f, dict) and "n" in f:
                expect(int(f.get("n", 0)) > 0 and abs(float(f.get("accuracy", -1)) - f.get("correct", 0) / f["n"]) < 1e-9,
                       f"benchmark_denominator:{name}")

    base = data(GATE_EVIDENCE_PREFIX + "baseline.json") or {}
    if base:
        r = base.get("e70_reconciliation") or {}
        expect(abs(float(r.get("recomputed_all_workers_hours", 0)) - 163.894) < 0.02, "e70_all_workers_recomputed")
        expect(r.get("delta_matches_excluded_workers") is True, "e70_delta_explained")

    si = data(GATE_EVIDENCE_PREFIX + "self-improvement.json") or {}
    if si:
        expect((si.get("history") or {}).get("ok") is True, "experiment_history_chain")
        expect(all(e.get("status") == "MEASURED" for e in si.get("experiments") or []), "no_self_promotion")

    lab = data(GATE_EVIDENCE_PREFIX + "security-lab.json") or {}
    if lab:
        expect(int(lab.get("bypassed", -1)) == 0 and lab.get("blocked") == lab.get("scenarios"), "security_lab_all_blocked")

    # ---------------- E72.1 cross-host / reboot / long-run / endurance / benchmark
    xh = data(GATE_EVIDENCE_PREFIX + "crosshost/crosshost.json") or {}
    if xh:
        ph = xh.get("phases") or {}
        m1 = ph.get("m1") or {}
        fo = ph.get("failover") or {}
        rb = ph.get("reboot") or {}
        expect((m1.get("mission") or {}).get("state") == "COMPLETED" and m1.get("all_independent") is True,
               "crosshost_m1_independent")
        expect(int(m1.get("cross_host_verified") or 0) == int(m1.get("tasks") or -1), "crosshost_m1_all_verified")
        expect(fo.get("b_took_over") is True and (fo.get("mission") or {}).get("state") == "COMPLETED",
               "crosshost_failover_completed")
        expect(int(fo.get("fencing_after") or 0) >= int(fo.get("fencing_before") or 0), "crosshost_fencing_monotonic")
        expect(rb.get("boot_id_changed") is True and rb.get("uptime_reset") is True, "host_reboot_real")
        expect((rb.get("mission_m4") or {}).get("state") == "COMPLETED"
               and int(rb.get("m4_verified") or 0) == int(rb.get("m4_tasks") or -1), "reboot_mission_completed")
        expect(int(rb.get("m3_blocked_expired") or 0) > 0, "expired_authority_refused")
        expect((xh.get("chain") or {}).get("ok") is True, "crosshost_event_chain")
        expect(int(xh.get("duplicate_results", -1)) == 0, "crosshost_no_duplicate_results")
        expect(((ph.get("security_probes") or {}).get("stale_fencing") or {}).get("accepted") is False,
               "stale_fencing_rejected")
        for tk in (xh.get("tickets") or []):
            pass
    lr = data(GATE_EVIDENCE_PREFIX + "longrun/longrun.json") or {}
    if lr:
        m = lr.get("mission") or {}
        acc = lr.get("accounting") or {}
        expect(float(m.get("elapsed_s") or 0) >= 3600, "longrun_one_hour_elapsed")
        expect(m.get("state") == "COMPLETED", "longrun_completed")
        expect(int(m.get("verified") or 0) == int(m.get("tasks") or -1), "longrun_all_verified")
        expect((acc.get("checkpoints") or {}).get("ok") is True, "longrun_checkpoint_chain")
        expect(float(acc.get("productive_worker_hours") or 0) <= float(acc.get("elapsed_s") or 0) / 3600.0 + 1e-6,
               "longrun_productive_le_elapsed")
    en = data(GATE_EVIDENCE_PREFIX + "endurance/endurance.json") or {}
    if en:
        expect(float(en.get("verified_productive_worker_hours") or 0)
               <= float(en.get("process_alive_worker_hours") or 0) + 1e-6, "endurance_productive_le_alive")
        expect(en.get("evidence_verified") is True, "endurance_bundle_verified")
        expect(en.get("cluster_restarted") is True and en.get("mission_state") == "COMPLETED", "endurance_restart_completed")
    b2 = data(GATE_EVIDENCE_PREFIX + "benchmark/benchmark.json") or {}
    if b2:
        expect(len(b2.get("models") or {}) == 3, "benchmark_three_models")
        for name, row in (b2.get("models") or {}).items():
            t = row.get("test") or {}
            expect(int(t.get("n") or 0) > 0
                   and abs(float(t.get("accuracy", -1)) - float(t.get("correct") or 0) / int(t.get("n") or 1)) < 1e-9,
                   f"benchmark_denominator:{name}")
        expect("wilson95" in (b2.get("tool_assisted") or {}), "benchmark_tool_assisted_ci")
    mem2 = data(GATE_EVIDENCE_PREFIX + "memory-eval-v2.json") or {}
    if mem2:
        expect(float(mem2.get("verified_recency_accuracy") or 0) > float(mem2.get("stale_first_accuracy") or 0),
               "memory_v2_improvement")
        expect(mem2.get("grants_authority") is False, "memory_v2_no_authority")

    # ---------------- mission bundles shipped in the E72 bundle
    mission_dirs = sorted({k.split("/")[3] for k in files
                           if k.startswith("artifacts/e72/missions/") and k.count("/") >= 5})
    for name in mission_dirs:
        mp = os.path.join(b, "artifacts/e72/missions", name, "evidence", "manifest.json")
        if not os.path.isfile(mp):
            continue
        mm = read(mp)
        if mm.get("schema") != MISSION_SCHEMA:
            problems.append(f"mission_schema:{name}")
            continue
        mb = {k: v for k, v in mm.items() if k != "signature"}
        if not verify_sig(str(mm.get("authority_pub") or ""), str(mm.get("signature") or ""), MISSION_DOMAIN, mb):
            problems.append(f"mission_signature:{name}")
        for rel, meta in (mm.get("files") or {}).items():
            if not (meta or {}).get("sha256"):
                continue
            p = os.path.join(b, "artifacts/e72/missions", name, "evidence", rel)
            if not os.path.isfile(p) or digest_file(p) != (meta or {}).get("sha256"):
                problems.append(f"mission_file:{name}:{rel}")
        checks.append(f"mission_bundle:{name}")
    if mission_dirs:
        checks.append(f"mission_bundles:{len(mission_dirs)}")

    ok = not problems
    out = {"ok": ok, "schema": SCHEMA, "bundle": b, "pinned_fingerprint": fingerprint,
           "git_head": m.get("git_head"), "branch": m.get("branch"), "checks": checks,
           "problems": problems, "files": len(files), "gates": len(gate_rows),
           "gate_counts": {s: sum(1 for g in gate_rows if g.get("status") == s)
                           for s in ("PASS", "PARTIAL", "BLOCKED", "FAIL")},
           "note": ("A self-generated key proves internal consistency and tamper detection, not a third-party "
                    "identity. Pin the fingerprint out of band for that use.")}
    print(json.dumps(out, indent=1) if a.json or True else json.dumps(out))
    return 0 if ok else 1


if __name__ == "__main__":
    raise SystemExit(main())
