"""Transparent NBA points baseline, not picks or a profitability claim. Python 3 standard library."""
import argparse
import csv
import json
import math
import os
import time
from datetime import datetime, timezone
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from zoneinfo import ZoneInfo

API = "https://prizepicksapi.com/api/v1"
TASK = "Build an NBA points baseline with earlier-game features and chronological MAE"


def pages(path, params):
    headers = {"Authorization": "Bearer " + os.environ["PRIZEPICKS_API_KEY"], "X-User-Request": TASK}
    rows, metadata, seen = [], [], set()
    params = dict(params)
    while True:
        with urlopen(Request(API + path + "?" + urlencode(params), headers=headers), timeout=30) as response:
            page = json.load(response)
        rows.extend(page["data"])
        metadata.append(page["meta"])
        cursor = page["meta"].get("next_cursor")
        if page["meta"].get("plan_limit") or not cursor:
            return rows, metadata
        if cursor in seen:
            raise RuntimeError("Repeated pagination cursor")
        seen.add(cursor)
        params["cursor"] = cursor
        time.sleep(1.1)


def clean_logs(rows):
    unique = {}
    for row in rows:
        if row.get("season_type") != "regular" or row.get("did_not_play"):
            continue
        value = row.get("stats", {}).get("points")
        if value is None:  # Missing is not zero; a real zero-point game stays in.
            continue
        points = float(value)
        if not math.isfinite(points) or points < 0:
            continue
        unique[row["stats_event_id"]] = {
            "game_id": row["stats_event_id"], "date": row["game_date"],
            "season": row["season"], "points": points,
        }
    return sorted(unique.values(), key=lambda row: (row["date"], row["game_id"]))


def features(history, season, before_date, window=5):
    earlier = [row for row in history if row["season"] == season and row["date"] < before_date]
    if len(earlier) < window:
        return None
    recent = earlier[-window:]
    return {
        "rolling_points": sum(row["points"] for row in recent) / window,
        "season_average": sum(row["points"] for row in earlier) / len(earlier),
        "prior_games": len(earlier), "last_feature_date": earlier[-1]["date"],
    }


def walk_forward(logs, window=5):
    predictions = []
    for game in logs:
        x = features(logs, game["season"], game["date"], window)
        if x is not None:
            predictions.append({"game_id": game["game_id"], "game_date": game["date"],
                                "actual_points": game["points"], **x})
    return predictions


def evaluate(predictions):
    if not predictions:
        return {"evaluated_games": 0, "note": "Not enough earlier same-season games; no score invented."}
    n = len(predictions)
    if n < 20:
        return {"evaluated_games": n, "note": "Saved worked forecasts, but fewer than 20 are too small for this tutorial to report MAE. This guard is not a statistical sample-size guarantee."}
    return {
        "evaluated_games": n,
        "rolling_mae": sum(abs(row["rolling_points"] - row["actual_points"]) for row in predictions) / n,
        "season_average_mae": sum(abs(row["season_average"] - row["actual_points"]) for row in predictions) / n,
        "date_window": [predictions[0]["game_date"], predictions[-1]["game_date"]],
        "note": "Descriptive retrospective walk-forward errors, not profits or proof of predictive reliability.",
    }


def compare_live(logs, board, player, window=5):
    result = []
    eastern = ZoneInfo("America/New_York")
    today = datetime.now(eastern).date().isoformat()
    for offer in board:
        name = (offer.get("player") or {}).get("name", "")
        market, event = offer.get("market") or {}, offer.get("event") or {}
        standard_available = any(side.get("tier") == "standard" and side.get("available")
                                 for side in offer.get("selections", []))
        if (name.casefold() != player.casefold() or offer.get("league") != "NBA"
                or market.get("stat") != "Points" or market.get("period") != "Full game"
                or not standard_available or offer.get("status") != "active"
                or not event.get("starts_at")):
            continue
        start = datetime.fromisoformat(event["starts_at"].replace("Z", "+00:00"))
        observed = offer.get("observed_at")
        if not observed or (datetime.now(timezone.utc) - datetime.fromisoformat(observed.replace("Z", "+00:00"))).total_seconds() > 600:
            continue
        if start <= datetime.now(timezone.utc):
            continue  # A full-game pregame baseline cannot price an in-game line.
        # Conservative date cutoff: never include today's games when completion time is unavailable.
        season = f"{start.year if start.month >= 10 else start.year-1}-{str((start.year+1) if start.month >= 10 else start.year)[-2:]}"
        cutoff = min(today, start.astimezone(eastern).date().isoformat())
        x = features(logs, season, cutoff, window)
        if x is None or offer.get("line") is None:
            continue
        result.append({"projection_id": offer["id"], "player": name,
                       "predicted_points": x["rolling_points"], "line": offer["line"],
                       "difference": x["rolling_points"] - float(offer["line"]),
                       "observed_at": offer.get("observed_at"),
                       "meaning": "Stat-forecast difference only, not an over probability or betting edge."})
    return result


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--player", default="Jayson Tatum")
    parser.add_argument("--player-id", help="Stable pl_ player ID or sp_ stats-player ID, instead of a name")
    parser.add_argument("--season", help="Optional loaded season, e.g. 2025-26; Free may restrict older games")
    args = parser.parse_args()
    params = {"league": "NBA", "player": args.player, "season_type": "regular", "view": "full", "limit": 200}
    if args.player_id:
        params.pop("player")
        params["player_id"] = args.player_id
    if args.season:
        params["season"] = args.season
    raw, meta = pages("/game-logs", params)
    player_name = raw[0]["player"]["name"] if raw else args.player
    logs = clean_logs(raw)
    predictions = walk_forward(logs)
    report = evaluate(predictions)
    report["loaded_games"] = len(logs)
    report["plan_limits"] = [m["plan_limit"] for m in meta if m.get("plan_limit")]
    report["coverage_warnings"] = [warning for m in meta for warning in m.get("warnings", [])]
    board, board_meta = pages("/projections", {"league": "NBA", "stat": "Points", "search": player_name,
                                              "tier": "standard", "view": "full", "limit": 100})
    report["live_comparisons"] = compare_live(logs, board, player_name) if board_meta and all(m.get("source_status") == "fresh" for m in board_meta) else []
    with open("nba-model-game-logs.json", "w") as file:
        json.dump(raw, file, indent=2)
    with open("nba-model-metadata.json", "w") as file:
        json.dump({"exported_at": datetime.now(timezone.utc).isoformat(), "game_log_meta": meta,
                   "board_meta": board_meta, "report": report}, file, indent=2)
    with open("nba-model-walk-forward.csv", "w", newline="") as file:
        fields = ["game_id", "game_date", "actual_points", "rolling_points", "season_average", "prior_games", "last_feature_date"]
        writer = csv.DictWriter(file, fieldnames=fields)
        writer.writeheader()
        writer.writerows(predictions)
    print(json.dumps(report, indent=2))  # Never prints the API key or request headers.


if __name__ == "__main__":
    main()
