#!/usr/bin/env python3
"""
profile-verify — one verifier for a party's whole trust profile, composing the primitives.

Trust is not a scalar and this tool refuses to make it one. A trust profile carries a party's evidence
across INDEPENDENT axes, and this walks each with the primitive that already grades it — no new logic,
no reimplementation. It literally loads the sibling verifiers and reuses them, so there is one crypto
core and zero drift:

  control_disjointness  → control-depth.py   : how DEEP the control chain re-derives (key→operator→
                                                boot→lineage). Reports depth D; below D, testimony.
  collusion_floor       → collusion-floor.py  : the minimum independent parties who must collude before
                                                a load-bearing surface is false. Reports min k.
  time_interval         → beacon-verify.py    : the entry's server_ts as a checkable [not-before,
                                                not-after] interval (drand → checkpoint → Bitcoin).

Each axis is an UPPER BOUND, graded fail-closed. The profile is CONFORMANT only if every present axis
is — a single over-claim on any axis fails the whole profile. There is deliberately NO combined score:
a relier gates on the axis its decision needs (a payment cares about collusion_floor; a sybil check
cares about control depth; a freshness gate cares about the interval), and each is reported at its own
floor, for the relier to tighten with its own knowledge.

This file is dependency-free but expects control-depth.py, collusion-floor.py and beacon-verify.py
alongside it (all served from touchstone.cv). Fetch the four together, read them, then run.

    python3 profile-verify.py <profile-file-or-url>
    python3 profile-verify.py <profile.json> --offline    # skip the online time-interval check
Exit: 0 = every present axis conformant · 1 = an axis over-claims / fails · 2 = malformed / missing sibling.
"""
import os
import sys
import json
import importlib.util
import urllib.request

_HERE = os.path.dirname(os.path.abspath(__file__))


def _load_sibling(filename):
    """Load a hyphen-named sibling verifier by path, so the composition reuses its exact code."""
    path = os.path.join(_HERE, filename)
    if not os.path.exists(path):
        raise FileNotFoundError(filename)
    spec = importlib.util.spec_from_file_location(filename.replace("-", "_").replace(".py", ""), path)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


def get_json(url):
    req = urllib.request.Request(url, headers={"Accept": "application/json", "User-Agent": "profile-verify/1"})
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read().decode("utf-8"))


def load(src):
    if src.startswith("http://") or src.startswith("https://"):
        return get_json(src)
    with open(src, "r", encoding="utf-8") as f:
        return json.load(f)


def verify_profile(profile, offline):
    """Returns (exit_code, lines). Grades each present axis with its own primitive; fail-closed."""
    lines = ["trust profile — %s\n" % (profile.get("subject") if isinstance(profile, dict) else "?")]
    axes = []   # (name, conformant, summary)

    try:
        cd = _load_sibling("control-depth.py")
        cf = _load_sibling("collusion-floor.py")
    except FileNotFoundError as ex:
        return 2, ["missing sibling verifier: %s — fetch control-depth.py, collusion-floor.py and "
                   "beacon-verify.py alongside this file." % ex]

    # 1. control-disjointness depth
    if isinstance(profile.get("control_depth"), dict):
        r = cd.evaluate(profile["control_depth"])
        if not r["conformant"]:
            axes.append(("control_disjointness", False, "NON-CONFORMANT — a level over-claims"))
        elif r["depth"] == 0:
            axes.append(("control_disjointness", True, "depth 0 — testimony all the way up"))
        else:
            axes.append(("control_disjointness", True, "verified to depth %d (%s); below that, testimony"
                         % (r["depth"], r["depth_name"])))

    # 2. collusion floor over the party's load-bearing surfaces
    surfaces = profile.get("surfaces")
    if isinstance(surfaces, list):
        floor, per, n_fail = cf.collusion_floor(surfaces)
        if n_fail:
            bad = ", ".join(p["status"] for p in per if p["ok"] is False)
            axes.append(("collusion_floor", False, "NON-CONFORMANT — %d surface(s) failed (%s)" % (n_fail, bad)))
        elif floor is None:
            axes.append(("collusion_floor", True, "∞ — every load-bearing surface is re-derivable (uncollusible)"))
        else:
            axes.append(("collusion_floor", True, "min k ≤ %d — at most this many must collude (upper bound)" % floor))

    # 3. time interval (composed from beacon-verify against the anchored entry)
    beacon = profile.get("time_interval") or profile.get("beacon_anchor")
    if beacon is not None:
        try:
            bv = _load_sibling("beacon-verify.py")
            doc = load(beacon) if isinstance(beacon, str) else beacon
            rc, blines = bv.verify(doc, offline)
            verdict = next((l.split("RESULT:", 1)[1].strip() for l in blines if "RESULT:" in l), "checked")
            axes.append(("time_interval", rc == 0, verdict[:120]))
        except FileNotFoundError:
            axes.append(("time_interval", True, "skipped — beacon-verify.py not alongside"))
        except Exception as ex:
            axes.append(("time_interval", False, "could not check the interval (%s)" % ex))

    if not axes:
        return 2, lines + ["no recognized axes in this profile (control_depth / surfaces / time_interval)."]

    width = max(len(n) for n, _, _ in axes)
    for name, ok, summary in axes:
        lines.append("  %s %-*s : %s" % ("✓" if ok else "✗", width, name, summary))
    lines.append("")

    conformant = all(ok for _, ok, _ in axes)
    if conformant:
        lines.append("RESULT: PROFILE CONFORMANT ✓ — %d independent axes, each an upper bound, each fail-closed. "
                     "There is no combined score: gate on the axis your decision needs and tighten its floor "
                     "with what you know." % len(axes))
        return 0, lines
    lines.append("RESULT: PROFILE NON-CONFORMANT ✗ — an axis over-claims what its evidence supports. One bad "
                 "axis fails the profile; a claim you can't back is worse than one you don't make.")
    return 1, lines


def main(argv):
    args = [a for a in argv[1:] if not a.startswith("--")]
    offline = "--offline" in argv[1:]
    if not args:
        print(__doc__)
        return 2
    rc, lines = verify_profile(load(args[0]), offline)
    for ln in lines:
        print(ln)
    return rc


if __name__ == "__main__":
    sys.exit(main(sys.argv))
