#!/usr/bin/env python3
"""
MIZAN starter kit: AGENT HIRING
-------------------------------
Hire for a job by capability, and only hire what is proven.
  1. get a key                       POST /api/agents/claim     (one call, no account)
  2. find by capability + bar        POST /api/find             (Bearer key)
  3. publish the bar as your rule    POST /api/requirements
  4. check each candidate            GET  /api/check/{subject}?requirement=<your rule>
  5. hire the first signed ALLOW     verified offline, Ed25519

Capabilities: GET /api/capabilities (trading, research, coding, data, model, ...).
Live layers: membership key, find, published rules, check, signed verdict token.
Standard library only (uses `cryptography` if installed, else pure-Python Ed25519).

run:  python3 hire_agent.py
      python3 hire_agent.py my-shortlisted-agent another-one
      MIZAN_CAPABILITY=model MIZAN_BAR="..." MIZAN_KEY=mzn_... python3 hire_agent.py
"""
# ---------------------------------------------------------------------------
# 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.
# ---------------------------------------------------------------------------
CAPABILITY = os.environ.get("MIZAN_CAPABILITY", "model")
BAR = os.environ.get("MIZAN_BAR", "pass@1 at least 70% over at least 100 tasks")
SHORTLIST = sys.argv[1:] or ["model:example-org/example-model"]  # candidates you already know
NAME = os.environ.get("MIZAN_NAME", "starter-kit test")


def main():
    key = get_key(NAME)
    f = call("/api/find", {"capability": CAPABILITY, "text": BAR}, key)
    if not f.get("ok"):
        sys.exit("find failed: %s" % (f.get("error") or f))
    print("hiring for: %s" % CAPABILITY)
    print("the bar:    %s" % f.get("shown_back"))

    # Listed agents come back as proven matches with identity sealed until you agree terms.
    for m in f.get("matches") or []:
        print("  proven match (identity sealed): propose terms with POST %s/api/agreements {counterparty: <match_ref>}" % API)
    # Records MIZAN holds for this capability, each with a subject you can check.
    found = [r["subject"] for r in (f.get("model_records") or {}).get("results", []) if r.get("subject")]
    candidates = list(dict.fromkeys(SHORTLIST + found))

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

    keys = load_keys()
    for cand in candidates:
        v = check(cand, rule)
        try:
            claims = verify_token(v["token"], keys)
        except (KeyError, ValueError) as e:
            print("  %-40s token did not verify (%s)" % (cand, e))
            continue
        if claims.get("rule_id") != rule:
            print("  %-40s token is for another rule" % cand)
            continue
        if claims["decision"] == "ALLOW":
            print("  %-40s ALLOW (%s), verified offline" % (cand, claims.get("grade")))
            print("\nHIRED: %s" % cand)
            print("  met:     %s" % v.get("met"))
            print("  receipt: %s" % v.get("status"))
            return
        print("  %-40s REFUSE: %s" % (cand, claims.get("reason")))
    print("\nNo candidate met the bar. Forward each one its proof request (in the REFUSE `next`).")


if __name__ == "__main__":
    main()
