#!/usr/bin/env python3
"""
MIZAN starter kit: PAY ON PROOF
-------------------------------
Before you pay anyone, ask MIZAN one question: is this payee proven?
  1. check the payee                 GET  /api/check/{payee}
  2. verify the signed answer        offline, Ed25519, key pinned once
  3. pay only on a signed ALLOW      a REFUSE prints the `next` step the payee can take

Live layers: check, signed verdict token, published key set.
No key, no signup. Standard library only (uses `cryptography` if installed,
otherwise a built-in pure-Python Ed25519 verify). No money moves: pay() is simulated.

run:  python3 pay_on_proof.py                       (two example payees)
      python3 pay_on_proof.py some-agent.example.com
      MIZAN_RULE=req_xxx@1 python3 pay_on_proof.py  (check against your own published bar)
"""
# ---------------------------------------------------------------------------
# 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.
# ---------------------------------------------------------------------------
PAYEES = sys.argv[1:] or ["try-me", "unknown-payee.example.com"]
AMOUNT_USD = 120.00
RULE = os.environ.get("MIZAN_RULE")  # None = MIZAN's default bar


def pay(payee, usd):
    # Replace with your real payment call (card, bank, invoice, payout API...).
    print("  -> PAID USD %s to %s  (simulated)" % (format(usd, ",.2f"), payee))


def main():
    keys = load_keys()
    print("key set pinned: %s" % ", ".join(keys))
    for payee in PAYEES:
        v = check(payee, RULE)
        print("\n%s" % payee)
        try:
            claims = verify_token(v["token"], keys)
        except (KeyError, ValueError) as e:
            print("  token did not verify (%s): not paying" % e)
            continue
        if RULE and claims.get("rule_id") != RULE:
            print("  token is for another rule: not paying")
            continue
        print("  signed %s, verified offline (kid %s, rule %s)" % (claims["decision"], claims["kid"], claims["rule_id"]))
        if claims["decision"] == "ALLOW":
            print("  met: %s" % v.get("met"))
            pay(payee, AMOUNT_USD)
        else:
            print("  -> NOT PAID. reason: %s" % claims.get("reason"))
            nxt = v.get("next") or {}
            pr = nxt.get("proof_request") or {}
            if pr:
                print("  next: %s %s %s" % (pr.get("method"), pr.get("url"), json.dumps(pr.get("body"))))
                print("        %s" % pr.get("returns"))
            for w in nxt.get("what_would_pass") or []:
                print("  would pass: %s" % w)


if __name__ == "__main__":
    main()
