#!/usr/bin/env python3
"""Python 3.10+. Standard library only. Run server-side; no credentials in URLs or output."""
import argparse
import json
import os
import random
import sys
import time
from datetime import datetime, timezone
from urllib.error import HTTPError
from urllib.parse import urlencode, urlsplit
from urllib.request import Request, HTTPRedirectHandler, build_opener

ORIGIN = "https://prizepicksapi.com"
PURPOSE = "Maintain an NBA NFL NHL sportsbook comparison dashboard"

class RejectRedirects(HTTPRedirectHandler):
    def redirect_request(self, request, response, code, message, headers, newurl):
        raise RuntimeError("API redirect refused; credentials were not forwarded.")

open_without_redirects = build_opener(RejectRedirects()).open

def read_page(url, key, cache, opener=open_without_redirects, sleep=time.sleep):
    if not key:
        raise ValueError("Set PRIZEPICKS_API_KEY in your server environment.")
    if urlsplit(url).scheme != "https" or urlsplit(url).netloc != "prizepicksapi.com":
        raise ValueError("Refusing to send credentials to another origin.")
    for attempt in range(3):
        saved = cache.get(url)
        headers = {"Authorization": "Bearer " + key, "X-User-Request": PURPOSE}
        if saved and saved.get("etag"):
            headers["If-None-Match"] = saved["etag"]
        try:
            response = opener(Request(url, headers=headers), timeout=15)
        except HTTPError as error:
            response = error
        with response:
            status = response.code
            if status == 304:
                if not saved:
                    raise RuntimeError("Unchanged response without a saved body.")
                return dict(saved, unchanged=True)
            try:
                body = json.load(response)
            except (ValueError, UnicodeError):
                raise RuntimeError(f"Non-JSON response ({status}); no data saved.") from None
            if status >= 400:
                code = body.get("error", {}).get("code", "UNKNOWN")
                transient = status == 503 or (status == 429 and code == "RATE_LIMIT_EXCEEDED")
                try:
                    retry = float(response.headers.get("Retry-After", 2 ** attempt))
                except ValueError:
                    retry = float("inf")
                if not transient or attempt == 2 or not (0 <= retry <= 10):
                    raise RuntimeError(f"Request stopped: HTTP {status} {code}. Check account access and allowance at {ORIGIN}/dashboard/billing; do not loop on quota errors.")
                sleep(max(retry, 2 ** attempt) + random.random() / 5)
                continue
            if not isinstance(body.get("data"), list):
                raise RuntimeError("Unexpected response shape; no data saved.")
            entry = dict(body=body, etag=response.headers.get("ETag"), next_update_seconds=float(response.headers.get("X-Next-Update-Seconds", 0)), usage_unit=response.headers.get("X-Usage-Unit"), credits_remaining=response.headers.get("X-Credits-Remaining"))
            cache[url] = entry
            return dict(entry, unchanged=False)
    raise RuntimeError("Request retry budget exhausted.")

def read_snapshot(key, max_pages=1, cache=None, opener=open_without_redirects, sleep=time.sleep):
    if not isinstance(max_pages, int) or not 1 <= max_pages <= 20:
        raise ValueError("pages must be an integer from 1 to 20.")
    if cache is None:
        cache = {}
    params = dict(league="NBA,NFL,NHL", view="compact", priced_only="true", alt_lines="false", limit="100")
    rows, cursor, pages, unchanged_pages, next_update = {}, None, 0, 0, 0
    while True:
        query = dict(params)
        if cursor:
            query["cursor"] = cursor
        page = read_page(ORIGIN + "/api/v1/compare?" + urlencode(query), key, cache, opener, sleep)
        for row in page["body"]["data"]:
            rows[row["id"]] = row
        cursor = page["body"].get("meta", {}).get("next_cursor")
        pages += 1
        unchanged_pages += int(page["unchanged"])
        next_update = max(next_update, page["next_update_seconds"])
        if not cursor or pages >= max_pages:
            break
    return dict(data=list(rows.values()), meta=dict(fetched_at=datetime.now(timezone.utc).isoformat(), pages=pages, unchanged_pages=unchanged_pages, partial=cursor is not None, next_cursor=cursor, next_update_seconds=next_update, usage_unit=page["usage_unit"], credits_remaining=page["credits_remaining"], notice="Rows retain source observation times. A paginated live read is not a frozen historical snapshot."))

def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--pages", type=int, default=1)
    parser.add_argument("--cycles", type=int, default=1)
    parser.add_argument("--poll-seconds", type=int, default=60)
    args = parser.parse_args()
    if not 1 <= args.cycles <= 20 or args.poll_seconds < 60:
        parser.error("cycles: 1–20; poll-seconds: at least 60")
    cache = {}
    for cycle in range(args.cycles):
        snapshot = read_snapshot(os.environ.get("PRIZEPICKS_API_KEY"), args.pages, cache)
        print(json.dumps(snapshot))
        if cycle + 1 < args.cycles:
            time.sleep(max(args.poll_seconds, snapshot["meta"]["next_update_seconds"]))

if __name__ == "__main__":
    try:
        main()
    except Exception as error:
        print(str(error), file=sys.stderr)
        sys.exit(1)
