#!/usr/bin/env python3
"""Python 3.10+; stdlib only. Small, explicit NBA archive jobs. Never reconstructs unobserved lines."""
import argparse
import json
import os
import sqlite3
import sys
import time
from datetime import date, datetime, timedelta, timezone
from urllib.error import HTTPError
from urllib.parse import urlencode
from urllib.request import Request, HTTPRedirectHandler, build_opener
from zoneinfo import ZoneInfo

ORIGIN = 'https://prizepicksapi.com'
STATS = ('Points', 'Rebounds', 'Assists')
PURPOSE = 'Maintain a daily NBA points rebounds assists and observed graded-line archive'

class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, *args, **kwargs):
        raise RuntimeError('Redirect refused; credentials were not forwarded.')

class Client:
    def __init__(self, key, max_requests=100, opener=None, sleep=time.sleep):
        if not key:
            raise ValueError('Set PRIZEPICKS_API_KEY in your server environment.')
        self.key, self.remaining, self.sleep = key, max_requests, sleep
        self.opener = opener or build_opener(NoRedirect()).open

    def read(self, endpoint, params):
        for attempt in range(3):
            if self.remaining <= 0:
                raise RuntimeError('Request budget reached. Rerun a smaller date range; incomplete slices were not saved.')
            self.remaining -= 1
            request = Request(ORIGIN + endpoint + '?' + urlencode(params), headers={
                'Authorization': 'Bearer ' + self.key, 'X-User-Request': PURPOSE,
                'X-PPAPI-Client': 'daily-nba-archive'})
            try:
                response = self.opener(request, timeout=25)
            except HTTPError as error:
                response = error
            with response:
                status = response.code
                try:
                    body = json.load(response)
                except (ValueError, UnicodeError):
                    raise RuntimeError('Non-JSON response; archive slice not saved.') from None
                if status >= 400:
                    code = body.get('error', {}).get('code')
                    retryable = status in (502, 503, 504) or (status == 429 and code == 'RATE_LIMIT_EXCEEDED')
                    try:
                        delay = max(float(response.headers.get('Retry-After', 2 ** attempt)), 2 ** attempt)
                    except ValueError:
                        delay = float('inf')
                    if retryable and attempt < 2 and 0 <= delay <= 30:
                        self.sleep(delay)
                        continue
                    raise RuntimeError(f'HTTP {status}. Check account access/allowance; no retry loop on monthly quota or paid access.')
                if not isinstance(body, dict) or not isinstance(body.get('data'), list) or not isinstance(body.get('meta'), dict) or 'next_cursor' not in body['meta']:
                    raise RuntimeError('Unexpected page shape; archive slice not saved.')
                if body['meta'].get('plan_limit') or body['meta'].get('limit_reached'):
                    raise RuntimeError('Plan-limited sample cannot be labeled a complete archive.')
                self.sleep(0.5)
                return body
        raise RuntimeError('Retry budget exhausted.')

def read_slice(client, endpoint, day, stat=None):
    params = {'league': 'NBA', 'date': day, 'limit': 200}
    if stat:
        params['stat'] = stat
    # Full game-log rows retain confirmed and player identity; results retain every status.
    rows, seen = {}, set()
    while True:
        body = client.read(endpoint, params)
        for row in body['data']:
            if row.get('game_date') != day or row.get('league') != 'NBA':
                raise RuntimeError('Unexpected row scope; archive slice not saved.')
            if endpoint.endswith('/results'):
                key = row.get('projection_id')
                if stat and row.get('market', {}).get('stat') != stat:
                    raise RuntimeError('Unexpected stat scope; archive slice not saved.')
            else:
                event, player = row.get('stats_event_id'), row.get('player', {}).get('stats_player_id')
                key = f'{event}:{player}' if event and player else None
            if not key:
                raise RuntimeError('Missing stable row identity; archive slice not saved.')
            rows[key] = row
        cursor = body['meta'].get('next_cursor')
        if cursor is None:
            return rows, body['meta']
        if not isinstance(cursor, str) or not cursor or cursor in seen:
            raise RuntimeError('Repeated or invalid cursor; archive slice not saved.')
        seen.add(cursor)
        params['cursor'] = cursor

def initialize(db):
    db.executescript('''CREATE TABLE IF NOT EXISTS archive_rows(
      dataset TEXT NOT NULL,row_key TEXT NOT NULL,game_date TEXT NOT NULL,payload TEXT NOT NULL,captured_at TEXT NOT NULL,
      PRIMARY KEY(dataset,row_key));
      CREATE TABLE IF NOT EXISTS archive_reads(dataset TEXT,game_date TEXT,stat TEXT,captured_at TEXT,rows INTEGER,meta TEXT,
      PRIMARY KEY(dataset,game_date,stat));
      CREATE VIEW IF NOT EXISTS nba_points_rebounds_assists AS SELECT game_date,
      json_extract(payload,'$.player.stats_player_id') player_id,json_extract(payload,'$.player.name') player,
      json_extract(payload,'$.stats_event_id') game_id,json_extract(payload,'$.stats.points') points,
      json_extract(payload,'$.stats.rebounds') rebounds,json_extract(payload,'$.stats.assists') assists,
      json_extract(payload,'$.confirmed') confirmed,captured_at FROM archive_rows WHERE dataset='game-logs';
      CREATE VIEW IF NOT EXISTS graded_prizepicks AS SELECT game_date,row_key projection_id,
      json_extract(payload,'$.market.stat') stat,json_extract(payload,'$.line') closing_line,
      json_extract(payload,'$.actual_value') actual_value,json_extract(payload,'$.result') result,
      json_extract(payload,'$.revision') revision,captured_at,payload FROM archive_rows
      WHERE dataset='results' AND json_extract(payload,'$.status')='graded';''')

def save_slice(db, dataset, day, stat, rows, meta):
    captured = datetime.now(timezone.utc).isoformat()
    # No partial cursor checkpoints: replay each date from page one to catch revised older seq values.
    with db:
        for key, row in rows.items():
            db.execute('INSERT INTO archive_rows VALUES(?,?,?,?,?) ON CONFLICT(dataset,row_key) DO UPDATE SET game_date=excluded.game_date,payload=excluded.payload,captured_at=excluded.captured_at',
                       (dataset, key, day, json.dumps(row), captured))
        db.execute('INSERT INTO archive_reads VALUES(?,?,?,?,?,?) ON CONFLICT(dataset,game_date,stat) DO UPDATE SET captured_at=excluded.captured_at,rows=excluded.rows,meta=excluded.meta',
                   (dataset, day, stat or '', captured, len(rows), json.dumps(meta)))

def dates(start, end):
    first, last = date.fromisoformat(start), date.fromisoformat(end)
    if first > last or (last-first).days > 30:
        raise ValueError('Choose an ordered range of at most 31 days per invocation.')
    return [(first+timedelta(days=i)).isoformat() for i in range((last-first).days+1)]

def main():
    yesterday = datetime.now(ZoneInfo('America/New_York')).date()-timedelta(days=1)
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--start', default=(yesterday-timedelta(days=4)).isoformat())
    parser.add_argument('--end', default=yesterday.isoformat())
    parser.add_argument('--dataset', choices=('both','game-logs','results'), default='both')
    parser.add_argument('--all-markets', action='store_true', help='Archive every NBA result market instead of only Points/Rebounds/Assists.')
    parser.add_argument('--max-requests', type=int, default=100)
    parser.add_argument('--db', default='nba-archive.sqlite')
    args = parser.parse_args()
    days = dates(args.start,args.end)
    if not 1 <= args.max_requests <= 1000:
        parser.error('--max-requests must be between 1 and 1000')
    client = Client(os.environ.get('PRIZEPICKS_API_KEY'), args.max_requests)
    with sqlite3.connect(args.db) as db:
        initialize(db)
        for day in days:
            for dataset in ('game-logs','results') if args.dataset == 'both' else (args.dataset,):
                for stat in (STATS if dataset == 'results' and not args.all_markets else (None,)):
                    rows, meta = read_slice(client,'/api/v1/'+dataset,day,stat)
                    save_slice(db,dataset,day,stat,rows,meta)
                    print(json.dumps(dict(dataset=dataset,game_date=day,stat=stat,rows=len(rows),pagination_finished=True)))

if __name__ == '__main__':
    try:
        main()
    except (RuntimeError, ValueError, OSError) as error:
        print(str(error), file=sys.stderr)
        sys.exit(1)
