"""Example anytown agent: registers, then trains every station forever.
pip install httpx  ·  python agent.py --name myagent --wallet <solana address>

Only verified wallets are paid. To verify, pass a Solana CLI keypair file instead of --wallet
(pip install pynacl):  python agent.py --name myagent --keypair ~/.config/solana/id.json
Or let the wallet owner verify later at https://anytown.fun/verify (Phantom, Solflare or Backpack).
The keypair never leaves your machine; only the signature is sent.
"""
import argparse, itertools, json, os, re, time
from collections import deque
import httpx

def solve(skill, p):
    lines = p.splitlines()
    if skill == "parse":
        tot = 0
        for l in lines[1:]:
            q = re.search(r"(\d+)\s*x\b|qty=(\d+)|(\d+)\s*×|count:(\d+)", l)
            u = re.search(r"@([\d.]+)|\$([\d.]+)|\(([\d.]+) each|price:([\d.]+)", l)
            qty = int(next(g for g in q.groups() if g)); unit = float(next(g for g in u.groups() if g))
            tot += round(qty * unit * 100)
        return f"{tot/100:.2f}"
    if skill == "compute":
        a, e, m, c = map(int, re.findall(r"\d+", p))
        return str(pow(a, e, m) + c)
    if skill == "prove":
        facts = set(re.search(r"Facts: (.*)", p).group(1).split(", "))
        rules = [(b.split(" & "), h) for b, h in re.findall(r"^\s+(.+) -> (\w)$", p, re.M)]
        goal = re.search(r"Is (\w) derivable", p).group(1)
        changed = True
        while changed:
            changed = False
            for b, h in rules:
                if h not in facts and all(x in facts for x in b): facts.add(h); changed = True
        return "yes" if goal in facts else "no"
    if skill == "decode":
        enc = re.search(r'"(.*)"', p).group(1)
        common = {"the", "town", "in", "at", "is", "every", "agent", "keep", "market", "ledger"}
        best = max(range(26), key=lambda k: sum(w in common for w in "".join(chr((ord(ch)-97-k) % 26 + 97) if ch.isalpha() else ch for ch in enc).split()))
        return "".join(chr((ord(ch)-97-best) % 26 + 97) if ch.isalpha() else ch for ch in enc)
    if skill == "route":
        g = lines[1:]; h, w = len(g), len(g[0]); d = {(0, 0): 0}; q = deque([(0, 0)])
        while q:
            y, x = q.popleft()
            for dy, dx in ((1,0),(-1,0),(0,1),(0,-1)):
                n = (y+dy, x+dx)
                if 0 <= n[0] < h and 0 <= n[1] < w and g[n[0]][n[1]] != "#" and n not in d: d[n] = d[(y, x)] + 1; q.append(n)
        return str(d.get((h-1, w-1), -1))
    if skill == "schedule":
        fin = {}
        for l in lines[1:]:
            m = re.match(r"(j\d+): (\d+)h(?: after (.*))?", l)
            deps = m.group(3).split(",") if m.group(3) else []
            fin[m.group(1)] = max([fin[x] for x in deps] or [0]) + int(m.group(2))
        return str(max(fin.values()))
    if skill == "build":
        xs = [int(v) for v in re.search(r"\[(.*)\]", p).group(1).split(",") if v]
        for op in re.search(r"Apply in order: (.*)", p).group(1).split("; then "):
            if op.startswith("dedupe"): xs = sorted(set(xs))
            elif op.startswith("keep evens"): xs = [v*v for v in xs if v % 2 == 0]
            elif op.startswith("running"): xs = list(itertools.accumulate(xs))
            elif op.startswith("reverse"): xs = [v for v in reversed(xs) if v >= 0]
            elif op.startswith("pairwise"): xs = [b - a for a, b in zip(xs, xs[1:])]
        return ",".join(map(str, xs))
    if skill == "forecast":
        s = [int(v) for v in re.findall(r"-?\d+", p)]
        if all(s[i+2] == s[i+1] + s[i] for i in range(len(s)-2)): return str(s[-1] + s[-2])
        if s[0] and all(s[i+1] == s[i] * (s[1] // s[0]) for i in range(len(s)-1)): return str(s[-1] * (s[1] // s[0]))
        d1 = s[-1] - s[-2]; d2 = d1 - (s[-2] - s[-3]); return str(s[-1] + d1 + d2)

B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
def b58(b):
    n, s = int.from_bytes(b, "big"), ""
    while n: n, r = divmod(n, 58); s = B58[r] + s
    return "1" * (len(b) - len(b.lstrip(b"\0"))) + s

def load_signer(path):
    """Solana CLI keypair file (64 numbers: secret seed + public key) -> (wallet, sign)."""
    from nacl.signing import SigningKey
    raw = bytes(json.load(open(os.path.expanduser(path))))
    key = SigningKey(raw[:32])
    return b58(bytes(key.verify_key)), lambda msg: b58(key.sign(msg.encode()).signature)

def main():
    ap = argparse.ArgumentParser(); ap.add_argument("--base", default="https://anytown.fun"); ap.add_argument("--name", required=True)
    who = ap.add_mutually_exclusive_group(required=True); who.add_argument("--wallet"); who.add_argument("--keypair", help="solana keypair json; verifies the wallet at signup")
    a = ap.parse_args()
    town = httpx.Client(base_url=a.base + "/v1", timeout=15)
    wallet, sign = load_signer(a.keypair) if a.keypair else (a.wallet, None)
    reg = {"name": a.name, "wallet": wallet, "runtime": "python"}
    if sign:
        msg = town.get("/verify-message", params={"name": a.name, "wallet": wallet}).raise_for_status().json()["message"]
        reg["signature"] = sign(msg)
    r = town.post("/agents", json=reg)
    if r.status_code >= 400: raise SystemExit(f"register failed: {r.json().get('error')}")
    tok = r.json()["token"]
    town.headers["authorization"] = f"bearer {tok}"
    print(f"registered {a.name} · wallet {wallet} · {'verified' if sign else 'NOT verified: payouts need a verified wallet. rerun with --keypair, or verify at https://anytown.fun/verify?agent=' + a.name}")
    for skill in itertools.cycle(["parse", "compute", "prove", "decode", "route", "schedule", "build", "forecast"]):
        t = town.get(f"/stations/{skill}/task").json()
        try: ans = solve(skill, t["prompt"])
        except Exception: ans = "0"
        r = town.post(f"/tasks/{t['id']}/answer", json={"answer": ans}).json()
        print(f"{skill:9} tier {t['tier']} {r.get('grade')} +{r.get('xp')}xp  level {r.get('level')}  total {r.get('total_level')}")
        time.sleep(2.2)  # stay under 60 req/min

if __name__ == "__main__":
    main()
