#!/usr/bin/env python3
"""ZANOS compute worker: registers an M2M agent and solves proof-of-compute
micro-tasks on a steady schedule. Pure stdlib — runs on a bare install.

Usage (any OS with python3, zero flags needed):
  python3 agent_worker.py
      [--name my-worker] [--endpoint https://rpc.zanos.tech:7777]
      [--state worker.json] [--referrer CODE]

The agent's accrued balance and settlement history are publicly visible
in the explorer: https://zanos.tech/explorer
"""
import argparse, hashlib, json, os, random, struct, sys, time
import urllib.request

_Q = 2**255 - 19
_L = 2**252 + 27742317777372353535851937790883648493
def _inv(x): return pow(x, _Q - 2, _Q)
_D = -121665 * _inv(121666) % _Q
_I = pow(2, (_Q - 1) // 4, _Q)
def _xrec(y):
    xx = (y*y - 1) * _inv(_D*y*y + 1) % _Q
    x = pow(xx, (_Q + 3) // 8, _Q)
    if (x*x - xx) % _Q: x = x * _I % _Q
    return x if x % 2 == 0 else _Q - x
_BY = 4 * _inv(5)
_B = (_xrec(_BY) % _Q, _BY % _Q)
def _ed(p, q):
    (x1, y1), (x2, y2) = p, q
    dd = _D * x1 * x2 * y1 * y2
    return ((x1*y2 + x2*y1) * _inv(1 + dd) % _Q,
            (y1*y2 + x1*x2) * _inv(1 - dd) % _Q)
def _sm(p, e):
    q = (0, 1)
    while e:
        if e & 1: q = _ed(q, p)
        p = _ed(p, p); e >>= 1
    return q
def _enc(p):
    x, y = p
    b = bytearray(y.to_bytes(32, "little")); b[31] |= (x & 1) << 7
    return bytes(b)
def _hint(m): return int.from_bytes(hashlib.sha512(m).digest(), "little")
def _scalar(sk):
    h = bytearray(hashlib.sha512(sk).digest()[:32])
    h[0] &= 248; h[31] &= 127; h[31] |= 64
    return int.from_bytes(h, "little"), hashlib.sha512(sk).digest()
def pubkey(sk):
    a, _ = _scalar(sk)
    return _enc(_sm(_B, a))
def sign(m, sk, pk):
    a, h = _scalar(sk)
    r = _hint(h[32:] + m)
    R = _sm(_B, r)
    S = (r + _hint(_enc(R) + pk + m) * a) % _L
    return _enc(R) + S.to_bytes(32, "little")

def sha256d(b): return hashlib.sha256(hashlib.sha256(b).digest()).digest()

def solve(seed_hex, difficulty):
    if seed_hex.startswith("0x"): seed_hex = seed_hex[2:]
    seed = bytes.fromhex(seed_hex)
    target = ((1 << 256) - 1) // difficulty
    nonce = random.getrandbits(64)
    while True:
        if int.from_bytes(sha256d(seed + struct.pack(">Q", nonce)), "big") <= target:
            return nonce
        nonce = (nonce + 1) & 0xFFFFFFFFFFFFFFFF

class Worker:
    def __init__(self, a):
        self.a = a
        self.state = {}
        try: self.state = json.load(open(a.state))
        except Exception: pass
    def log(self, m): print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {self.a.name}: {m}", flush=True)
    def save(self):
        tmp = self.a.state + ".tmp"
        open(tmp, "w").write(json.dumps(self.state, indent=2)); os.replace(tmp, self.a.state)
    def http(self, method, path, body=None):
        req = urllib.request.Request(self.a.endpoint.rstrip("/") + path,
            data=json.dumps(body).encode() if body is not None else None,
            headers={"Content-Type": "application/json"}, method=method)
        return json.loads(urllib.request.urlopen(req, timeout=30).read())
    def ensure_agent(self):
        if self.state.get("agent_id"): return True
        body = {"label": self.a.name}
        if self.a.referrer: body["referrer"] = self.a.referrer
        r = self.http("POST", "/rpc/agent/register", body)
        k = r["ephemeralKey"]
        self.state.update(agent_id=r["agentId"], agent_key=k["privateKey"], agent_pub=k["publicKey"])
        self.save()
        self.log(f"agent {r['agentId']} -> {r.get('address','?')}"
                 + (" (referral active)" if r.get("referralActive") else ""))
        return True
    def round(self):
        if not self.ensure_agent(): return
        aid, sk, pk = (self.state["agent_id"],
                       bytes.fromhex(self.state["agent_key"]),
                       bytes.fromhex(self.state["agent_pub"]))
        n = random.randint(3, 8)
        try:
            tasks = self.http("GET", f"/rpc/tasks/available?agentId={aid}&limit={n}")["tasks"]
        except Exception as e:
            self.state.pop("agent_id", None); self.save()
            self.log(f"task fetch failed ({e}); will re-register next round")
            return
        done = 0
        for t in tasks:
            nonce = solve(t["seed"], t["difficulty"])
            time.sleep(random.uniform(1, 8))
            sig = sign(t["taskId"].encode() + aid.encode() + struct.pack(">Q", nonce), sk, pk)
            try:
                self.http("POST", "/rpc/tasks/submit",
                          {"taskId": t["taskId"], "agentId": aid, "nonce": nonce, "signature": sig.hex()})
                done += 1
            except Exception as e:
                self.log(f"submit rejected ({e})")
        self.log(f"round complete: {done}/{len(tasks)} tasks accepted")
    def run(self):
        self.log(f"compute worker online -> {self.a.endpoint}")
        while True:
            try: self.round()
            except Exception as e: self.log(f"round error ({e})")
            time.sleep(random.uniform(self.a.idle_min, self.a.idle_max))

if __name__ == "__main__":
    p = argparse.ArgumentParser()
    p.add_argument("--endpoint", default="https://rpc.zanos.tech:7777")
    p.add_argument("--name", default=f"worker-{random.getrandbits(24):06x}",
                   help="omitted name is auto-generated, so bare `python3 agent_worker.py` just works")
    p.add_argument("--referrer", default="")
    p.add_argument("--state", default="zanos_worker.json")
    p.add_argument("--idle-min", type=float, default=120)
    p.add_argument("--idle-max", type=float, default=600)
    Worker(p.parse_args()).run()
