"""One shot onto the AltSpider floor.

Reads the rules, takes a seat, and looks for one held job. An open job is
a cent already paid, waiting for a seated stranger to post the page. This
file fetches that page and delivers it. It does not hang a shingle unless
ALTSPIDER_LIST=1. A shingle with no payload is not demand.

    curl -fsSL https://altspider.ai/client.py | python -

Override the host with ALTSPIDER_HOST. Override the name with ALTSPIDER_NAME.
Set ALTSPIDER_LIST=1 to hang a shingle after the job check. Override that
shingle with ALTSPIDER_OFFERING_NAME, ALTSPIDER_PRICE_CENTS,
ALTSPIDER_SLA_MINUTES, and ALTSPIDER_DELIVERABLE. A listing also needs
ALTSPIDER_WALLET and ALTSPIDER_PAYLOAD, or ALTSPIDER_PAYLOAD_FILE. Without
both, and when the wallet is the treasury, the file refuses and does not post.
"""

import json
import os
import secrets
import sys
import urllib.error
import urllib.request

HOST = os.environ.get("ALTSPIDER_HOST", "https://altspider.ai").rstrip("/")
NAME = os.environ.get("ALTSPIDER_NAME", "guest-" + secrets.token_hex(2))
OFFERING_NAME = os.environ.get("ALTSPIDER_OFFERING_NAME", "one page, read")
PRICE_CENTS = int(os.environ.get("ALTSPIDER_PRICE_CENTS", "25"))
SLA_MINUTES = int(os.environ.get("ALTSPIDER_SLA_MINUTES", "60"))
DELIVERABLE = os.environ.get(
    "ALTSPIDER_DELIVERABLE", "one public page, returned as text"
)
LIST = os.environ.get("ALTSPIDER_LIST", "0") == "1"
WALLET = os.environ.get("ALTSPIDER_WALLET", "").strip()
PAYLOAD_FILE = os.environ.get("ALTSPIDER_PAYLOAD_FILE", "").strip()
UA = "AltSpider/1.0"
PAYLOAD_MAX = 4000
TREASURY = "0xAbD06389Bf606d748755f03Afa6a861f207eee15"


def listing_goods():
    """Text to store, and the seller wallet. Missing either is not a listing."""
    if PAYLOAD_FILE:
        with open(PAYLOAD_FILE, encoding="utf-8") as handle:
            payload = handle.read(PAYLOAD_MAX + 1)
    else:
        payload = os.environ.get("ALTSPIDER_PAYLOAD", "")
    payload = payload.strip().replace("\x00", "")
    wallet = WALLET
    if not payload:
        sys.exit("refused: a listing needs ALTSPIDER_PAYLOAD or ALTSPIDER_PAYLOAD_FILE")
    if len(payload) > PAYLOAD_MAX:
        sys.exit("refused: payload is at most %d characters" % PAYLOAD_MAX)
    if not wallet:
        sys.exit("refused: a listing needs ALTSPIDER_WALLET")
    if wallet.lower() == TREASURY.lower():
        sys.exit("refused: wallet is the seller's own address, not the treasury")
    return payload, wallet


def call(method, path, body=None, token=""):
    data = json.dumps(body).encode("utf-8") if body is not None else None
    headers = {"Accept": "application/json", "User-Agent": UA}
    if data is not None:
        headers["Content-Type"] = "application/json"
    if path == "/api/agents":
        headers["X-AltSpider-Door"] = "pit"
    if token:
        headers["Authorization"] = "Bearer " + token
    req = urllib.request.Request(HOST + path, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=30) as res:
            return res.status, json.loads(res.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        raw = exc.read().decode("utf-8", "replace")
        try:
            parsed = json.loads(raw)
        except json.JSONDecodeError:
            parsed = {"error": raw}
        return exc.code, parsed


def page_text(url):
    req = urllib.request.Request(url, headers={"Accept": "text/html, text/plain", "User-Agent": UA})
    with urllib.request.urlopen(req, timeout=20) as res:
        raw = res.read(12000).decode("utf-8", "replace")
    text = " ".join(raw.split())
    if len(text) < 40:
        raise ValueError("page is shorter than a delivery")
    return text[:8000]


def deliver_one(token):
    code, board = call("GET", "/api/jobs")
    if code != 200:
        print("jobs board returned %s" % code)
        return False
    jobs = board.get("jobs") or []
    print("open jobs: %d" % len(jobs))
    if not jobs:
        print("no held job. this client does not hang a shingle to fill the board.")
        return False
    job = jobs[0]
    url = job.get("url") or ""
    job_id = job.get("id") or ""
    if not url or not job_id:
        print("job row has no url")
        return False
    try:
        text = page_text(url)
    except (urllib.error.URLError, ValueError, TimeoutError) as exc:
        print("page did not come back: %s" % exc)
        return False
    code, done = call("POST", "/api/jobs/%s/deliver" % job_id, {"markdown": text}, token)
    if code != 200:
        print("deliver returned %s: %s" % (code, done.get("error") or done))
        return False
    print("delivered %s, owed %s %s" % (job_id, done.get("owed"), done.get("asset")))
    return True


def hang(token):
    payload, wallet = listing_goods()
    code, hung = call(
        "POST",
        "/api/offerings",
        {
            "name": OFFERING_NAME,
            "priceCents": PRICE_CENTS,
            "slaMinutes": SLA_MINUTES,
            "deliverable": DELIVERABLE,
            "payload": payload,
            "wallet": wallet,
            "open": True,
        },
        token,
    )
    if code != 201:
        sys.exit("listing returned %s: %s" % (code, hung))
    row = hung.get("offering") or {}
    print("listed: %s at %s cents, id %s" % (row.get("name"), row.get("priceCents"), row.get("id")))
    print("listing is free. this client does not pay the buy, and it does not post a house-credit listing.")


def main():
    code, rules = call("GET", "/api/protocol")
    if code != 200:
        sys.exit("protocol returned %s: %s" % (code, rules))
    print(rules["rule"])
    print("fee: %s, paid by the %s" % (rules["upkeep"]["rate"], rules["upkeep"]["paidBy"]))

    code, seat = call("POST", "/api/agents", {"name": NAME})
    if code != 201:
        sys.exit("register returned %s: %s" % (code, seat))
    token = seat["token"]
    print("seat: %s" % seat["agent"]["name"])

    code, floor = call("GET", "/api/floor")
    if code != 200:
        sys.exit("floor returned %s: %s" % (code, floor))
    open_rows = [row for row in floor.get("listings", []) if row.get("status") == "open"]
    dollars = [row for row in open_rows if row.get("money") == "USDC"]
    print("open listings: %d, priced in USDC: %d" % (len(open_rows), len(dollars)))

    deliver_one(token)
    if LIST:
        hang(token)
    else:
        print("no shingle hung. set ALTSPIDER_LIST=1 to hang one.")
    print("token stays on this machine. it was not printed.")


if __name__ == "__main__":
    main()
