#!/usr/bin/env python3
"""
A minimal ArtWars bot: read the canvas, sign in with a Solana wallet, paint a pixel.

This exists because the READ half of the API was well documented and the WRITE half was
not: placing a pixel from a script means implementing a challenge/sign/verify handshake,
and prose describing a handshake is not the same as forty lines that run.

    # read-only, no dependencies at all beyond the standard library
    python3 artwars_bot.py show 512 512

    # sign in and paint (needs: pip install pynacl base58)
    python3 artwars_bot.py paint 512 512 5 --key ~/my_wallet.json

--key takes the 64-byte secret key array a Solana CLI keypair file contains.

⚠ Signing is NOT a payment and moves nothing. It proves you hold the address. Read the
message before signing it anywhere, including here: this script prints it first.
"""

import argparse
import json
import sys
import urllib.error
import urllib.request

BASE = "https://artwars.io/api"
CANVAS = 1024
UA = "artwars-example-bot/1.0 (+https://artwars.io/api.html)"


def request(path, payload=None, token=None, raw=False):
    """One HTTP call. Returns parsed JSON, or raw bytes when raw=True."""
    url = f"{BASE}/{path}"
    data = json.dumps(payload).encode() if payload is not None else None
    headers = {"User-Agent": UA}
    if data:
        headers["Content-Type"] = "application/json"
    if token:
        # The header is the form to prefer. A `token` body field and ?token= also work.
        headers["Authorization"] = f"Bearer {token}"

    req = urllib.request.Request(url, data=data, headers=headers)
    try:
        with urllib.request.urlopen(req, timeout=30) as res:
            body = res.read()
            return body if raw else json.loads(body)
    except urllib.error.HTTPError as e:
        body = e.read()
        # ⚠ Every failure is JSON with a stable `error` string. Branch on that string,
        # never on the human-readable prose, which is free to change.
        try:
            err = json.loads(body)
        except ValueError:
            err = {"error": "http_%d" % e.code, "detail": body[:200].decode("utf8", "replace")}
        raise SystemExit(f"{e.code} {err.get('error')}: {err.get('detail', '')}")


def read_canvas():
    """The whole canvas as one bytes object. offset = y * 1024 + x, value = palette index.

    One request, ~232KB gzipped, no parsing. The JSON route is ~23MB over 16 requests.
    A long-running bot should send If-None-Match with the ETag and expect mostly 304s.
    """
    blob = request("canvas_blob.php", raw=True)
    if len(blob) != CANVAS * CANVAS:
        raise SystemExit(f"expected {CANVAS * CANVAS} bytes, got {len(blob)}")
    return blob


def sign_in(keypair_path):
    """challenge -> sign -> verify, returning a bearer token."""
    try:
        import base58
        from nacl.signing import SigningKey
    except ImportError:
        raise SystemExit("painting needs: pip install pynacl base58")

    secret = bytes(json.load(open(keypair_path)))
    key = SigningKey(secret[:32])
    wallet = base58.b58encode(bytes(key.verify_key)).decode()

    challenge = request("wallet_auth.php", {"action": "challenge", "wallet": wallet})
    message = challenge["message"]

    # Printed on purpose. Never sign a message a tool would not show you.
    print("--- message to sign ---", message, "-----------------------", sep="\n")

    # ⚠ Sign the message EXACTLY as returned. The server verifies against its own stored
    # copy, so a re-encoded or reformatted string will not match.
    signature = base58.b58encode(key.sign(message.encode()).signature).decode()

    # ⚠ The nonce is required here too, not just the signature: it is what ties this
    # signature to this one challenge, and it is consumed atomically so it cannot be
    # replayed or ground against.
    verified = request("wallet_auth.php", {
        "action": "verify",
        "wallet": wallet,
        "nonce": challenge["nonce"],
        "signature": signature,
    })
    return verified["token"], wallet


def main():
    ap = argparse.ArgumentParser(description="Minimal ArtWars API client.")
    sub = ap.add_subparsers(dest="cmd", required=True)

    s = sub.add_parser("show", help="print the palette index at one pixel")
    s.add_argument("x", type=int)
    s.add_argument("y", type=int)

    p = sub.add_parser("paint", help="sign in and place one pixel")
    p.add_argument("x", type=int)
    p.add_argument("y", type=int)
    p.add_argument("color", type=int, help="palette index, 0-15")
    p.add_argument("--key", required=True, help="Solana keypair JSON (64-byte array)")

    sub.add_parser("info", help="print the API catalogue summary")
    args = ap.parse_args()

    if args.cmd == "info":
        cat = request("index.php")
        print(f"{cat['name']}  api_version={cat.get('api_version')}  "
              f"auth_mode={cat.get('auth_mode')}  canvas={cat['canvas_size']}")
        print(f"cooldown {cat['cooldown_seconds']}s   docs: {cat.get('docs')}")
        return

    if args.cmd == "show":
        blob = read_canvas()
        idx = blob[args.y * CANVAS + args.x]
        palette = request("canvas.php?meta=1")["palette"]
        print(f"({args.x},{args.y}) = index {idx} = {palette[idx]}")
        return

    token, wallet = sign_in(args.key)
    print(f"signed in as {wallet}")
    result = request("place.php",
                     {"x": args.x, "y": args.y, "color_index": args.color},
                     token=token)
    print("placed:", json.dumps(result))


if __name__ == "__main__":
    sys.exit(main())
