#!/usr/bin/env python3
"""Daily archive workflow: save bounded pages of each dataset to SQLite so your own history grows.

Independent service, not affiliated with or endorsed by DraftKings. No wagering.
Run: python3 daily-archive.py --db archive.sqlite --sports NBA,NFL,NHL --max-requests 60
Schedule once a day, for example with cron:
  15 6 * * * cd /path/to/archive && DRAFTKINGS_API_KEY=... python3 daily-archive.py --db archive.sqlite --sports NBA,NFL,NHL --max-requests 60
Re-running the same day is safe: rows are keyed by dataset, id and observed_at. The budget caps spend.
Retained upstream history began around 2026-11-03, so your archive's value comes from running it over time.
Keep archives to your own use unless you hold redistribution rights.
"""
import argparse
import hashlib
import json
import os
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
import sqlite3

API_KEY = os.environ.get("DRAFTKINGS_API_KEY")
BASE = os.environ.get("DRAFTKINGS_API_BASE", "https://draftkingsapi.com").rstrip("/")
CACHE_DIR = Path(os.environ.get("DRAFTKINGS_CACHE_DIR", ".draftkings-cache"))
# Collection targets five minutes. Polling faster spends quota without newer data.
CACHE_TTL_SECONDS = 300


class ApiError(Exception):
    def __init__(self, status, message):
        super().__init__(f"HTTP {status}: {message}")
        self.status = status


class Client:
    """Bounded client: a request budget, a timeout, a file cache and limited retries."""

    def __init__(self, max_requests, use_cache=True):
        if not API_KEY:
            sys.exit("Set DRAFTKINGS_API_KEY in your environment. Create a key at https://draftkingsapi.com/dashboard")
        self.max_requests = max_requests
        self.requests = 0
        self.use_cache = use_cache

    def get(self, path, **params):
        query = {k: v for k, v in params.items() if v is not None}
        url = f"{BASE}{path}?{urlencode(query)}" if query else f"{BASE}{path}"
        cached = self._read_cache(url)
        if cached is not None:
            return cached
        body = self._fetch(url)
        self._write_cache(url, body)
        return body

    def _fetch(self, url):
        for attempt in range(3):
            if self.requests >= self.max_requests:
                raise ApiError(0, f"Local budget of {self.max_requests} requests reached. Raise it deliberately.")
            self.requests += 1
            request = Request(url, headers={"Authorization": f"Bearer {API_KEY}", "Accept": "application/json"})
            try:
                with urlopen(request, timeout=20) as response:
                    return json.load(response)
            except HTTPError as error:
                message = _error_message(error)
                can_retry = self.requests < self.max_requests
                if error.code == 429 and attempt == 0 and can_retry:
                    # Minute limits reset at the next UTC minute. A second 429 usually means the monthly allowance.
                    time.sleep(61 - datetime.now(timezone.utc).second)
                    continue
                if error.code >= 500 and attempt < 2 and can_retry:
                    time.sleep(2 ** attempt)
                    continue
                raise ApiError(error.code, message) from None
            except (URLError, TimeoutError) as error:
                if attempt < 2 and self.requests < self.max_requests:
                    time.sleep(2 ** attempt)
                    continue
                raise ApiError(0, f"Network failure: {error}") from None
        raise ApiError(429, "Account request limit reached. Check the allowance in your dashboard.")

    def _cache_path(self, url):
        return CACHE_DIR / (hashlib.sha256(url.encode()).hexdigest() + ".json")

    def _read_cache(self, url):
        path = self._cache_path(url)
        if not self.use_cache or not path.exists() or time.time() - path.stat().st_mtime > CACHE_TTL_SECONDS:
            return None
        return json.loads(path.read_text())

    def _write_cache(self, url, body):
        if self.use_cache:
            CACHE_DIR.mkdir(exist_ok=True)
            self._cache_path(url).write_text(json.dumps(body))


def _error_message(error):
    try:
        return json.load(error)["error"]["message"]
    except Exception:
        return error.reason


SCHEMA = """
CREATE TABLE IF NOT EXISTS rows(dataset TEXT, row_id TEXT, sport TEXT, observed_at TEXT, run_day TEXT, payload TEXT, PRIMARY KEY(dataset, row_id, observed_at));
CREATE TABLE IF NOT EXISTS cursors(name TEXT PRIMARY KEY, value TEXT);
CREATE TABLE IF NOT EXISTS runs(started_at TEXT, finished_at TEXT, requests INTEGER, rows_seen INTEGER, stopped_reason TEXT);
"""


class Archive:
    def __init__(self, db, client):
        self.db, self.client, self.rows_seen = db, client, 0
        self.run_day = datetime.now(timezone.utc).date().isoformat()

    def save(self, dataset, row_id, sport, observed_at, row):
        self.rows_seen += 1
        self.db.execute("INSERT OR IGNORE INTO rows VALUES(?,?,?,?,?,?)", (dataset, str(row_id), sport, observed_at, self.run_day, json.dumps(row, separators=(",", ":"))))

    def cursor(self, name, default):
        row = self.db.execute("SELECT value FROM cursors WHERE name=?", (name,)).fetchone()
        return row[0] if row else default

    def set_cursor(self, name, value):
        self.db.execute("INSERT OR REPLACE INTO cursors VALUES(?,?)", (name, str(value)))
        self.db.commit()

    def offset_pages(self, dataset, path, sport, row_id, pages=2, **params):
        offset = 0
        for _ in range(pages):
            page = self.client.get(path, sport=sport, limit=100, offset=offset, **params)
            for row in page["data"]:
                self.save(dataset, row_id(row), sport, row.get("observed_at"), row)
            self.db.commit()
            if not page["pagination"]["has_more"]:
                return
            offset += 100

    def pick6_history(self, sport, pages=3):
        name = f"pick6_history:{sport}"
        after = int(self.cursor(name, 0))
        for _ in range(pages):
            page = self.client.get("/api/v1/pick6/history", sport=sport, limit=100, after=after)
            for row in page["data"]:
                self.save("pick6_history", row["change_id"], sport, row.get("observed_at"), row)
            after = page["pagination"]["next_after"]
            self.set_cursor(name, after)
            if not page["pagination"]["has_more"]:
                return


def pick6_id(row):
    return ":".join(str(row.get(k)) for k in ("pick_group_id", "pickable_id", "pickable_market_id"))


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--db", default="archive.sqlite")
    parser.add_argument("--sports", default="NBA,NFL,NHL")
    parser.add_argument("--max-requests", type=int, default=60)
    args = parser.parse_args()
    started = datetime.now(timezone.utc).isoformat()
    db = sqlite3.connect(args.db)
    db.executescript(SCHEMA)
    client = Client(max_requests=args.max_requests, use_cache=False)
    archive = Archive(db, client)
    reason = "finished"
    try:
        for sport in [s.strip() for s in args.sports.split(",") if s.strip()]:
            archive.offset_pages("pick6_current", "/api/v1/pick6/projections", sport, pick6_id)
            archive.pick6_history(sport)
            archive.offset_pages("sportsbook_events", "/api/v1/events", sport, lambda r: r["id"])
            archive.offset_pages("sportsbook_odds", "/api/v1/odds", sport, lambda r: r["id"])
            archive.offset_pages("dfs_slates", "/api/v1/dfs/slates", sport, lambda r: r["id"], pages=1)
    except ApiError as error:
        # Keep what was saved. A later run resumes from the stored cursors.
        reason = str(error)
    db.execute("INSERT INTO runs VALUES(?,?,?,?,?)", (started, datetime.now(timezone.utc).isoformat(), client.requests, archive.rows_seen, reason))
    db.commit()
    stored = db.execute("SELECT dataset, count(*) FROM rows GROUP BY dataset").fetchall()
    print(json.dumps({"requests": client.requests, "rows_seen": archive.rows_seen, "stored": dict(stored), "stopped": reason}))


if __name__ == "__main__":
    main()
