"""CAIN-A75-007 offline verifier (rules embedded verbatim). CAIN-A75-007 physical-action-twin rules. Standard library only, no CAIN imports: embedded verbatim in the offline
verifier.

A robot agent proposes one action against a declared digital twin. The plan is evidence with a subject, so the rules
recompute every safety condition from the supplied numbers instead of trusting the plan's own summary:

  binding       simulator is labelled SIMULATED; model id/version and an uncertainty budget are present
  authority     a named principal, a scope that covers this action, and an expiry after the supplied "now"
  envelope      speed, force and payload are within the action's declared envelope; the whole path stays inside the
                allowed zones and outside the forbidden zones; the action kind is permitted
  simulation    the twin ran deterministically, the horizon covers the action, there were no collisions, and
                penetration is within tolerance
  humans        every person is at least (speed * reaction + uncertainty + stop margin) away from the path
  claims        every boolean the plan asserts matches what was computed here
  honesty       the twin is explicitly labelled as simulation, not hardware evidence

Verdict: SAFE, else REFUSED with every failing check named. Malformed input is REFUSED.
This is a simulation review. It does not contact an instrument or robot, and a wrong state gives a wrong verdict.
"""
from __future__ import annotations

import math
import re

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


class Bad(ValueError):
    pass


def _num(v, what, lo=0.0, hi=1e9):
    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 _in_zone(point, zone):
    x, y = point
    x0, y0, x1, y1 = zone["bounds"]
    return x0 - _TOL <= x <= x1 + _TOL and y0 - _TOL <= y <= y1 + _TOL


def _segment_point_distance(a, b, p):
    ax, ay = a; bx, by = b; px, py = p
    dx, dy = bx - ax, by - ay
    l2 = dx * dx + dy * dy
    if l2 == 0:
        return math.hypot(px - ax, py - ay)
    t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / l2))
    return math.hypot(px - (ax + t * dx), py - (ay + t * dy))


def _path_length(path):
    return sum(math.hypot(b[0] - a[0], b[1] - a[1]) for a, b in zip(path, path[1:]))


def evaluate(plan):
    checks, summary = [], {}

    def c(cid, ok, detail=""):
        checks.append({"id": cid, "ok": bool(ok), "detail": detail})
        return ok

    try:
        if not isinstance(plan, dict) or plan.get("schema") != "cain42.a75.twin.plan.v1":
            raise Bad("not a cain42.a75.twin.plan.v1 plan")
        twin, world, act, sim, auth = plan.get("twin"), plan.get("world"), plan.get("action"), plan.get("simulation"), plan.get("authorization")
        if not all(isinstance(x, dict) for x in (twin, world, act, sim, auth)):
            raise Bad("twin, world, action, simulation and authorization must be objects")
        if not isinstance(act.get("id"), str) or not _ID.match(act["id"]):
            raise Bad("action.id must be an identifier")
        if not isinstance(act.get("kind"), str) or act["kind"] not in ("move", "pick", "place", "dispense", "scan"):
            raise Bad("action.kind must be one of move, pick, place, dispense, scan")

        # binding
        c("binding.simulator_label", twin.get("simulator") == "SIMULATED", f"simulator {twin.get('simulator')!r}")
        c("binding.model_version", isinstance(twin.get("model_id"), str) and bool(twin["model_id"]) and isinstance(twin.get("version"), str) and bool(twin["version"]),
          "the twin names a model and a version")
        unc = twin.get("uncertainty") if isinstance(twin.get("uncertainty"), dict) else {}
        unc_m = _num(unc.get("position_m"), "twin.uncertainty.position_m", 0.0, 1.0)
        c("binding.uncertainty_declared", unc_m > 0, f"position uncertainty {unc_m} m")
        summary["uncertainty_m"] = unc_m

        # authority (deterministic: compare against the supplied clock)
        now = _num(auth.get("now"), "authorization.now", 0.0)
        exp = _num(auth.get("expires_at"), "authorization.expires_at", 0.0)
        c("authority.principal", isinstance(auth.get("principal"), str) and bool(auth["principal"]), "named principal")
        c("authority.scope", isinstance(auth.get("scope"), list) and act["kind"] in auth["scope"], f"scope {auth.get('scope')!r}")
        c("authority.not_expired", exp > now, f"expires {exp} > now {now}")
        c("authority.delegated", auth.get("delegated_from") in (None, "human") or isinstance(auth.get("delegated_from"), str),
          "delegation is recorded (or absent)")

        # envelope and geometry
        env = act.get("envelope") if isinstance(act.get("envelope"), dict) else {}
        params = act.get("params") if isinstance(act.get("params"), dict) else {}
        max_speed = _num(env.get("max_speed_mps"), "envelope.max_speed_mps", 0.0, 100.0)
        max_force = _num(env.get("max_force_n"), "envelope.max_force_n", 0.0, 10000.0)
        speed = _num(params.get("speed_mps"), "params.speed_mps", 0.0, 100.0)
        force = _num(params.get("force_n"), "params.force_n", 0.0, 10000.0)
        c("envelope.speed", speed <= max_speed, f"{speed} <= {max_speed} m/s")
        c("envelope.force", force <= max_force, f"{force} <= {max_force} N")
        c("envelope.kind_allowed", act["kind"] in (env.get("allowed_kinds") or []), f"kind {act['kind']} in {env.get('allowed_kinds')}")

        zones = world.get("zones") if isinstance(world.get("zones"), list) else []
        allowed = [z for z in zones if isinstance(z, dict) and z.get("kind") == "allowed"]
        forbidden = [z for z in zones if isinstance(z, dict) and z.get("kind") == "forbidden"]
        for z in allowed + forbidden:
            b = z.get("bounds")
            if not (isinstance(b, list) and len(b) == 4):
                raise Bad("a zone needs bounds [x0, y0, x1, y1]")
            for v in b:
                _num(v, "zone bound", -1e6, 1e6)
        path = params.get("path")
        if not isinstance(path, list) or not (2 <= len(path) <= _MAX_PATH):
            raise Bad(f"params.path must be 2-{_MAX_PATH} points")
        path = [[_num(p[0], "path x", -1e6, 1e6), _num(p[1], "path y", -1e6, 1e6)] for p in path
                if isinstance(p, (list, tuple)) and len(p) == 2]
        if len(path) != len(params["path"]):
            raise Bad("every path point is [x, y]")
        c("geometry.path_in_allowed", all(any(_in_zone(p, z) for z in allowed) for p in path),
          "every waypoint is inside an allowed zone")
        c("geometry.path_clear_of_forbidden", not any(_in_zone(p, z) for p in path for z in forbidden),
          "no waypoint is inside a forbidden zone")
        c("geometry.zones_declared", bool(allowed) and bool(forbidden), f"{len(allowed)} allowed, {len(forbidden)} forbidden")

        # simulation
        c("simulation.ran", sim.get("ran") is True, "the twin was asked")
        c("simulation.deterministic", sim.get("deterministic") is True, "two runs give one effect")
        steps = sim.get("steps")
        c("simulation.steps", isinstance(steps, int) and not isinstance(steps, bool) and 1 <= steps <= 100000, f"steps {steps!r}")
        horizon = _num(sim.get("horizon_s"), "simulation.horizon_s", 0.0, 100000.0)
        needed = (_path_length(path) / speed) if speed > 0 else float("inf")
        c("simulation.horizon_covers_action", needed <= horizon + _TOL, f"needs {round(needed, 4)} s, horizon {horizon} s")
        collisions = sim.get("collisions")
        c("simulation.no_collision", isinstance(collisions, list) and not collisions, f"collisions {collisions!r}")
        penetration = _num(sim.get("max_penetration_m"), "simulation.max_penetration_m", 0.0, 10.0)
        c("simulation.penetration_within_tolerance", penetration <= _num(env.get("max_penetration_m"), "envelope.max_penetration_m", 0.0, 1.0),
          f"penetration {penetration} m")
        c("simulation.effect_matches_intent", sim.get("predicted_effect") == act.get("expected_effect"),
          f"twin predicts {sim.get('predicted_effect')!r}, plan intends {act.get('expected_effect')!r}")

        # humans within the stopping distance of the path
        reaction = _num(env.get("reaction_s", 0.25), "envelope.reaction_s", 0.0, 5.0)
        margin = _num(env.get("stop_margin_m", 0.10), "envelope.stop_margin_m", 0.0, 10.0)
        stop_distance = speed * reaction + unc_m + margin
        humans = world.get("humans") if isinstance(world.get("humans"), list) else []
        closest = None
        for h in humans:
            loc = h.get("location") if isinstance(h, dict) else None
            if not (isinstance(loc, list) and len(loc) == 2):
                raise Bad("a human needs a [x, y] location")
            p = [_num(loc[0], "human x", -1e6, 1e6), _num(loc[1], "human y", -1e6, 1e6)]
            d = min(_segment_point_distance(a, b, p) for a, b in zip(path, path[1:]))
            closest = d if closest is None else min(closest, d)
        c("humans.standoff", closest is None or closest >= stop_distance - _TOL,
          f"closest person {round(closest, 4)} m, required {round(stop_distance, 4)} m")
        summary["stop_distance_m"] = round(stop_distance, 4)
        summary["closest_human_m"] = None if closest is None else round(closest, 4)

        # claims honesty
        claims = plan.get("claims") if isinstance(plan.get("claims"), dict) else {}
        computed = {
            "within_envelope": speed <= max_speed and force <= max_force and act["kind"] in (env.get("allowed_kinds") or []),
            "no_collision": isinstance(collisions, list) and not collisions,
            "effect_matches": sim.get("predicted_effect") == act.get("expected_effect"),
            "simulation_ran": sim.get("ran") is True,
        }
        for k in sorted(claims):
            v = claims[k]
            c(f"claims.{k}", k in computed and v == computed[k], f"claimed {v!r}, computed {computed.get(k)!r}")
        c("honesty.simulation_not_hardware", twin.get("simulator") == "SIMULATED" and plan.get("hardware_connected") is False,
          "plan states no hardware was contacted")
        summary["computed"] = computed
    except Bad as e:
        checks.append({"id": "input.well_formed", "ok": False, "detail": str(e)})
        return "REFUSED", checks, summary
    except Exception as e:  # noqa: BLE001
        checks.append({"id": "input.error", "ok": False, "detail": f"{type(e).__name__}: {e}"[:200]})
        return "REFUSED", checks, summary
    return ("SAFE" if all(x["ok"] for x in checks) else "REFUSED"), 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.twin.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-007, {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))
