#!/usr/bin/env python3
"""
MIZAN starter kit: PROVEN-ONLY MARKETPLACE LISTING
--------------------------------------------------
Write your listing bar in plain words. Show only candidates that meet it.
  1. compile the bar                 POST /api/requirements/compile   (no key)
  2. publish it as your rule         POST /api/requirements           (key: claimed in one call)
  3. check every candidate           GET  /api/check/{candidate}?requirement=<your rule>
  4. verify each signed answer       offline, Ed25519
  5. list only the signed ALLOWs     everyone else stays hidden, with the reason

Live layers: requirement compiler, published rules, check, signed verdict token.
Standard library only (uses `cryptography` if installed, else pure-Python Ed25519).

run:  python3 proven_listing.py
      python3 proven_listing.py candidate-a candidate-b ...
      MIZAN_BAR="..." MIZAN_KEY=mzn_... python3 proven_listing.py
      MIZAN_RULE=req_xxx@1 python3 proven_listing.py   (reuse a published rule; skips publishing)
"""
# ---------------------------------------------------------------------------
# The MIZAN plumbing: HTTP + offline Ed25519 verification. Copy as-is.
# ---------------------------------------------------------------------------
import base64, hashlib, json, os, sys, time, urllib.error, urllib.request

API = os.environ.get("MIZAN_API", "https://studio.mizan.market")
KEYS_URL = API + "/.well-known/mizan-keys.json"


def call(path, body=None, key=None):
    """GET (no body) or POST JSON to the live API. Returns the parsed JSON reply."""
    headers = {"Content-Type": "application/json", "User-Agent": "mizan-starter-kit/1"}
    if key:
        headers["Authorization"] = "Bearer " + key
    data = None if body is None else json.dumps(body).encode()
    req = urllib.request.Request(API + path, data=data, headers=headers, method="POST" if data else "GET")
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:  # MIZAN answers errors in JSON too
        return json.load(e)


def b64u(s):
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))


def _ed25519_pure(pub, msg, sig):
    """RFC 8032 Ed25519 verify in pure Python (used only if `cryptography` is missing)."""
    q = 2**255 - 19
    L = 2**252 + 27742317777372353535851937790883648493
    d = -121665 * pow(121666, q - 2, q) % q

    def add(P, Q):
        A = (P[1] - P[0]) * (Q[1] - Q[0]) % q
        B = (P[1] + P[0]) * (Q[1] + Q[0]) % q
        C = 2 * P[3] * Q[3] * d % q
        D = 2 * P[2] * Q[2] % q
        E, F, G, H = B - A, D - C, D + C, B + A
        return (E * F % q, G * H % q, F * G % q, E * H % q)

    def mul(s, P):
        Q = (0, 1, 1, 0)
        while s:
            if s & 1:
                Q = add(Q, P)
            P, s = add(P, P), s >> 1
        return Q

    def point(b):
        y = int.from_bytes(b, "little")
        sign, y = y >> 255, y & ((1 << 255) - 1)
        if y >= q:
            return None
        x2 = (y * y - 1) * pow(d * y * y + 1, q - 2, q) % q
        x = pow(x2, (q + 3) // 8, q)
        if (x * x - x2) % q:
            x = x * pow(2, (q - 1) // 4, q) % q
        if (x * x - x2) % q or (x == 0 and sign):
            return None
        if (x & 1) != sign:
            x = q - x
        return (x, y, 1, x * y % q)

    if len(pub) != 32 or len(sig) != 64:
        return False
    A, R, s = point(pub), point(sig[:32]), int.from_bytes(sig[32:], "little")
    if A is None or R is None or s >= L:
        return False
    B = point((4 * pow(5, q - 2, q) % q).to_bytes(32, "little"))
    h = int.from_bytes(hashlib.sha512(sig[:32] + pub + msg).digest(), "little") % L
    lhs, rhs = mul(s, B), add(R, mul(h, A))
    return (lhs[0] * rhs[2] - rhs[0] * lhs[2]) % q == 0 and (lhs[1] * rhs[2] - rhs[1] * lhs[2]) % q == 0


def ed25519_verify(pub, msg, sig):
    try:
        from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
    except ImportError:
        return _ed25519_pure(pub, msg, sig)
    try:
        Ed25519PublicKey.from_public_bytes(pub).verify(sig, msg)
        return True
    except Exception:
        return False


def load_keys():
    """Fetch the key set once and pin it. After this, verifying needs no call to MIZAN."""
    ks = call("/.well-known/mizan-keys.json")
    return {k["key_id"]: base64.b64decode(k["public_key_b64"]) for k in ks["keys"] if k.get("alg") == "Ed25519"}


def verify_token(token, keys):
    """Offline. A verdict token is mzv1.<claims>.<sig>, Ed25519 over 'mzv1.' + claims.
    A release token is <header>.<payload>.<sig>, Ed25519 over 'header.payload'.
    Returns the claims, or raises ValueError."""
    parts = token.split(".")
    if len(parts) != 3:
        raise ValueError("malformed")
    if parts[0] == "mzv1":
        claims, signed = json.loads(b64u(parts[1])), ("mzv1." + parts[1]).encode()
        kid = claims.get("kid")
    else:
        header, claims = json.loads(b64u(parts[0])), json.loads(b64u(parts[1]))
        signed, kid = (parts[0] + "." + parts[1]).encode(), header.get("kid")
    if kid not in keys:
        raise ValueError("unknown_kid")
    if not ed25519_verify(keys[kid], signed, b64u(parts[2])):
        raise ValueError("bad_signature")
    if claims.get("exp", 0) <= time.time():
        raise ValueError("expired")
    return claims


def get_key(name):
    """Your MIZAN key: $MIZAN_KEY if set, else one call claims one (no account, no form)."""
    if os.environ.get("MIZAN_KEY"):
        return os.environ["MIZAN_KEY"]
    body = {"name": name}
    if os.environ.get("MIZAN_TEST"):  # marks a test identity, kept out of public counts
        body["test"] = True
    d = call("/api/agents/claim", body)
    if not d.get("api_key"):
        sys.exit("could not claim a key: %s" % d)
    print("claimed a key for %r (set MIZAN_KEY=... to reuse it)" % name)
    return d["api_key"]


def check(subject, rule=None):
    from urllib.parse import quote
    q = "?requirement=" + quote(rule, safe="@") if rule else ""
    return call("/api/check/" + quote(subject, safe=":/") + q)


# ---------------------------------------------------------------------------
# Your app. Change these.
# ---------------------------------------------------------------------------
BAR = os.environ.get("MIZAN_BAR", "Sharpe after costs at least 1 and max drawdown at most 25%")
CANDIDATES = sys.argv[1:] or ["try-me", "fresh-listing.example.com", "model:example-org/example-model"]
NAME = os.environ.get("MIZAN_NAME", "starter-kit test")


def main():
    c = call("/api/requirements/compile", {"text": BAR})
    print("your bar:   %s" % BAR)
    print("read back:  %s" % c.get("shown_back"))
    if not c.get("publishable"):
        sys.exit("not publishable yet: %s" % (c.get("errors") or c.get("unmatched")))

    rule = os.environ.get("MIZAN_RULE")
    if not rule:
        key = get_key(NAME)
        p = call("/api/requirements", {"text": BAR, "confirm": True, "name": NAME + ": listing bar"}, key)
        if not p.get("ok"):
            sys.exit("publish failed: %s" % (p.get("errors") or p.get("error")))
        rule = p["ref"]
        print("published:  rule %s  hash %s" % (rule, p["hash"]))

    keys = load_keys()
    listed, hidden = [], []
    for cand in CANDIDATES:
        v = check(cand, rule)
        try:
            claims = verify_token(v["token"], keys)
        except (KeyError, ValueError) as e:
            hidden.append((cand, "token did not verify: %s" % e))
            continue
        if claims.get("rule_id") != rule:
            hidden.append((cand, "token is for another rule"))
        elif claims["decision"] == "ALLOW":
            listed.append((cand, claims.get("grade")))
        else:
            hidden.append((cand, claims.get("reason")))

    print("\nLISTING (signed ALLOW under %s, each verified offline)" % rule)
    for cand, grade in listed:
        print("  [listed]  %-32s %s" % (cand, grade))
    print("\nHIDDEN until they meet the bar")
    for cand, why in hidden:
        print("  [hidden]  %-32s %s" % (cand, why))


if __name__ == "__main__":
    main()
