#!/usr/bin/env python3
"""
Historical Odds Backfill from The Odds API

Backfills opening and closing lines from The Odds API historical endpoint.
Data available from June 6, 2020 with 5-minute intervals (post Sept 2022).

Usage:
    python historical_backfill_theoddsapi.py --date 2026-01-15          # Single date
    python historical_backfill_theoddsapi.py --from 2025-10-01 --to 2025-12-31  # Date range
    python historical_backfill_theoddsapi.py --missing                   # Fill missing games

Cost: 10 credits per region per market per snapshot (30 credits per snapshot with h2h,spreads,totals)
"""
import argparse
import requests
import psycopg2
import json
import os
from datetime import datetime, timezone, timedelta
from typing import Optional, Dict, List, Tuple

BASE_URL = 'https://api.the-odds-api.com/v4'

SPORTS = {
    'nba': 'basketball_nba',
    'nfl': 'americanfootball_nfl',
    'nhl': 'icehockey_nhl',
    'mlb': 'baseball_mlb',
    'ncaab': 'basketball_ncaab',
    'ncaaf': 'americanfootball_ncaaf',
}

CORE_MARKETS = ['h2h', 'spreads', 'totals']

# Team name mapping: API full names -> normalized key
TEAM_NORMALIZE = {
    # NBA
    'Atlanta Hawks': 'ATL', 'Hawks': 'ATL', 'ATL': 'ATL',
    'Boston Celtics': 'BOS', 'Celtics': 'BOS', 'BOS': 'BOS',
    'Brooklyn Nets': 'BKN', 'Nets': 'BKN', 'BKN': 'BKN', 'BRK': 'BKN',
    'Charlotte Hornets': 'CHA', 'Hornets': 'CHA', 'CHA': 'CHA', 'CHO': 'CHA',
    'Chicago Bulls': 'CHI', 'Bulls': 'CHI', 'CHI': 'CHI',
    'Cleveland Cavaliers': 'CLE', 'Cavaliers': 'CLE', 'CLE': 'CLE', 'CAVALIERS': 'CLE',
    'Dallas Mavericks': 'DAL', 'Mavericks': 'DAL', 'DAL': 'DAL', 'MAVERICKS': 'DAL',
    'Denver Nuggets': 'DEN', 'Nuggets': 'DEN', 'DEN': 'DEN',
    'Detroit Pistons': 'DET', 'Pistons': 'DET', 'DET': 'DET', 'PISTONS': 'DET',
    'Golden State Warriors': 'GSW', 'Warriors': 'GSW', 'GSW': 'GSW', 'GS': 'GSW',
    'Houston Rockets': 'HOU', 'Rockets': 'HOU', 'HOU': 'HOU',
    'Indiana Pacers': 'IND', 'Pacers': 'IND', 'IND': 'IND', 'PACERS': 'IND',
    'Los Angeles Clippers': 'LAC', 'LA Clippers': 'LAC', 'Clippers': 'LAC', 'LAC': 'LAC',
    'Los Angeles Lakers': 'LAL', 'LA Lakers': 'LAL', 'Lakers': 'LAL', 'LAL': 'LAL',
    'Memphis Grizzlies': 'MEM', 'Grizzlies': 'MEM', 'MEM': 'MEM', 'GRIZZLIES': 'MEM',
    'Miami Heat': 'MIA', 'Heat': 'MIA', 'MIA': 'MIA',
    'Milwaukee Bucks': 'MIL', 'Bucks': 'MIL', 'MIL': 'MIL', 'BUCKS': 'MIL',
    'Minnesota Timberwolves': 'MIN', 'Timberwolves': 'MIN', 'MIN': 'MIN',
    'New Orleans Pelicans': 'NOP', 'Pelicans': 'NOP', 'NOP': 'NOP', 'NO': 'NOP',
    'New York Knicks': 'NYK', 'Knicks': 'NYK', 'NYK': 'NYK', 'NY': 'NYK', 'KNICKS': 'NYK',
    'Oklahoma City Thunder': 'OKC', 'Thunder': 'OKC', 'OKC': 'OKC', 'THUNDER': 'OKC',
    'Orlando Magic': 'ORL', 'Magic': 'ORL', 'ORL': 'ORL',
    'Philadelphia 76ers': 'PHI', '76ers': 'PHI', 'PHI': 'PHI',
    'Phoenix Suns': 'PHX', 'Suns': 'PHX', 'PHX': 'PHX', 'PHO': 'PHX',
    'Portland Trail Blazers': 'POR', 'Trail Blazers': 'POR', 'Blazers': 'POR', 'POR': 'POR',
    'Sacramento Kings': 'SAC', 'Kings': 'SAC', 'SAC': 'SAC',
    'San Antonio Spurs': 'SAS', 'Spurs': 'SAS', 'SAS': 'SAS', 'SA': 'SAS',
    'Toronto Raptors': 'TOR', 'Raptors': 'TOR', 'TOR': 'TOR', 'RAPTORS': 'TOR',
    'Utah Jazz': 'UTA', 'Jazz': 'UTA', 'UTA': 'UTA',
    'Washington Wizards': 'WAS', 'Wizards': 'WAS', 'WAS': 'WAS', 'WSH': 'WAS',
    # NFL
    'Arizona Cardinals': 'ARI', 'Cardinals': 'ARI', 'ARI': 'ARI',
    'Atlanta Falcons': 'ATL', 'Falcons': 'ATL',
    'Baltimore Ravens': 'BAL', 'Ravens': 'BAL', 'BAL': 'BAL',
    'Buffalo Bills': 'BUF', 'Bills': 'BUF', 'BUF': 'BUF',
    'Carolina Panthers': 'CAR', 'Panthers': 'CAR', 'CAR': 'CAR',
    'Chicago Bears': 'CHI', 'Bears': 'CHI',
    'Cincinnati Bengals': 'CIN', 'Bengals': 'CIN', 'CIN': 'CIN',
    'Cleveland Browns': 'CLE', 'Browns': 'CLE',
    'Dallas Cowboys': 'DAL', 'Cowboys': 'DAL',
    'Denver Broncos': 'DEN', 'Broncos': 'DEN',
    'Detroit Lions': 'DET', 'Lions': 'DET',
    'Green Bay Packers': 'GB', 'Packers': 'GB', 'GB': 'GB',
    'Houston Texans': 'HOU', 'Texans': 'HOU',
    'Indianapolis Colts': 'IND', 'Colts': 'IND',
    'Jacksonville Jaguars': 'JAX', 'Jaguars': 'JAX', 'JAX': 'JAX', 'JAC': 'JAX',
    'Kansas City Chiefs': 'KC', 'Chiefs': 'KC', 'KC': 'KC',
    'Las Vegas Raiders': 'LV', 'Raiders': 'LV', 'LV': 'LV', 'LVR': 'LV',
    'Los Angeles Chargers': 'LAC', 'Chargers': 'LAC',
    'Los Angeles Rams': 'LAR', 'Rams': 'LAR', 'LAR': 'LAR', 'LA': 'LAR',
    'Miami Dolphins': 'MIA', 'Dolphins': 'MIA',
    'Minnesota Vikings': 'MIN', 'Vikings': 'MIN',
    'New England Patriots': 'NE', 'Patriots': 'NE', 'NE': 'NE',
    'New Orleans Saints': 'NO', 'Saints': 'NO',
    'New York Giants': 'NYG', 'Giants': 'NYG', 'NYG': 'NYG',
    'New York Jets': 'NYJ', 'Jets': 'NYJ', 'NYJ': 'NYJ',
    'Philadelphia Eagles': 'PHI', 'Eagles': 'PHI',
    'Pittsburgh Steelers': 'PIT', 'Steelers': 'PIT', 'PIT': 'PIT',
    'San Francisco 49ers': 'SF', '49ers': 'SF', 'SF': 'SF',
    'Seattle Seahawks': 'SEA', 'Seahawks': 'SEA', 'SEA': 'SEA',
    'Tampa Bay Buccaneers': 'TB', 'Buccaneers': 'TB', 'TB': 'TB',
    'Tennessee Titans': 'TEN', 'Titans': 'TEN', 'TEN': 'TEN',
    'Washington Commanders': 'WSH', 'Commanders': 'WSH',
    # NHL
    'Anaheim Ducks': 'ANA', 'Ducks': 'ANA', 'ANA': 'ANA',
    'Arizona Coyotes': 'ARI', 'Coyotes': 'ARI',
    'Boston Bruins': 'BOS', 'Bruins': 'BOS',
    'Buffalo Sabres': 'BUF', 'Sabres': 'BUF',
    'Calgary Flames': 'CGY', 'Flames': 'CGY', 'CGY': 'CGY',
    'Carolina Hurricanes': 'CAR', 'Hurricanes': 'CAR',
    'Chicago Blackhawks': 'CHI', 'Blackhawks': 'CHI',
    'Colorado Avalanche': 'COL', 'Avalanche': 'COL', 'COL': 'COL',
    'Columbus Blue Jackets': 'CBJ', 'Blue Jackets': 'CBJ', 'CBJ': 'CBJ',
    'Dallas Stars': 'DAL', 'Stars': 'DAL',
    'Detroit Red Wings': 'DET', 'Red Wings': 'DET',
    'Edmonton Oilers': 'EDM', 'Oilers': 'EDM', 'EDM': 'EDM',
    'Florida Panthers': 'FLA', 'Panthers': 'FLA', 'FLA': 'FLA',
    'Los Angeles Kings': 'LAK', 'Kings': 'LAK', 'LAK': 'LAK',
    'Minnesota Wild': 'MIN', 'Wild': 'MIN',
    'Montreal Canadiens': 'MTL', 'Canadiens': 'MTL', 'MTL': 'MTL',
    'Nashville Predators': 'NSH', 'Predators': 'NSH', 'NSH': 'NSH',
    'New Jersey Devils': 'NJD', 'Devils': 'NJD', 'NJD': 'NJD', 'NJ': 'NJD',
    'New York Islanders': 'NYI', 'Islanders': 'NYI', 'NYI': 'NYI',
    'New York Rangers': 'NYR', 'Rangers': 'NYR', 'NYR': 'NYR',
    'Ottawa Senators': 'OTT', 'Senators': 'OTT', 'OTT': 'OTT',
    'Philadelphia Flyers': 'PHI', 'Flyers': 'PHI',
    'Pittsburgh Penguins': 'PIT', 'Penguins': 'PIT',
    'San Jose Sharks': 'SJS', 'Sharks': 'SJS', 'SJS': 'SJS', 'SJ': 'SJS',
    'Seattle Kraken': 'SEA', 'Kraken': 'SEA',
    'St Louis Blues': 'STL', 'St. Louis Blues': 'STL', 'Blues': 'STL', 'STL': 'STL',
    'Tampa Bay Lightning': 'TBL', 'Lightning': 'TBL', 'TBL': 'TBL', 'TB': 'TBL',
    'Toronto Maple Leafs': 'TOR', 'Maple Leafs': 'TOR',
    'Utah Hockey Club': 'UTA', 'Utah': 'UTA',
    'Vancouver Canucks': 'VAN', 'Canucks': 'VAN', 'VAN': 'VAN',
    'Vegas Golden Knights': 'VGK', 'Golden Knights': 'VGK', 'VGK': 'VGK',
    'Washington Capitals': 'WSH', 'Capitals': 'WSH',
    'Winnipeg Jets': 'WPG', 'Jets': 'WPG', 'WPG': 'WPG',
    # MLB
    'Arizona Diamondbacks': 'ARI', 'Diamondbacks': 'ARI', 'D-backs': 'ARI',
    'Atlanta Braves': 'ATL', 'Braves': 'ATL',
    'Baltimore Orioles': 'BAL', 'Orioles': 'BAL',
    'Boston Red Sox': 'BOS', 'Red Sox': 'BOS',
    'Chicago Cubs': 'CHC', 'Cubs': 'CHC', 'CHC': 'CHC',
    'Chicago White Sox': 'CWS', 'White Sox': 'CWS', 'CWS': 'CWS', 'CHW': 'CWS',
    'Cincinnati Reds': 'CIN', 'Reds': 'CIN',
    'Cleveland Guardians': 'CLE', 'Guardians': 'CLE',
    'Colorado Rockies': 'COL', 'Rockies': 'COL',
    'Detroit Tigers': 'DET', 'Tigers': 'DET',
    'Houston Astros': 'HOU', 'Astros': 'HOU',
    'Kansas City Royals': 'KC', 'Royals': 'KC',
    'Los Angeles Angels': 'LAA', 'Angels': 'LAA', 'LAA': 'LAA',
    'Los Angeles Dodgers': 'LAD', 'Dodgers': 'LAD', 'LAD': 'LAD',
    'Miami Marlins': 'MIA', 'Marlins': 'MIA',
    'Milwaukee Brewers': 'MIL', 'Brewers': 'MIL',
    'Minnesota Twins': 'MIN', 'Twins': 'MIN',
    'New York Mets': 'NYM', 'Mets': 'NYM', 'NYM': 'NYM',
    'New York Yankees': 'NYY', 'Yankees': 'NYY', 'NYY': 'NYY',
    'Oakland Athletics': 'OAK', 'Athletics': 'OAK', 'OAK': 'OAK', "A's": 'OAK',
    'Philadelphia Phillies': 'PHI', 'Phillies': 'PHI',
    'Pittsburgh Pirates': 'PIT', 'Pirates': 'PIT',
    'San Diego Padres': 'SD', 'Padres': 'SD', 'SD': 'SD',
    'San Francisco Giants': 'SF', 'Giants': 'SF',
    'Seattle Mariners': 'SEA', 'Mariners': 'SEA',
    'St Louis Cardinals': 'STL', 'St. Louis Cardinals': 'STL', 'Cardinals': 'STL',
    'Tampa Bay Rays': 'TB', 'Rays': 'TB',
    'Texas Rangers': 'TEX', 'Rangers': 'TEX', 'TEX': 'TEX',
    'Toronto Blue Jays': 'TOR', 'Blue Jays': 'TOR',
    'Washington Nationals': 'WSH', 'Nationals': 'WSH',
}


def normalize_team(name: str) -> str:
    """Normalize team name to standard abbreviation"""
    if not name:
        return ''
    return TEAM_NORMALIZE.get(name, TEAM_NORMALIZE.get(name.upper(), name.upper()))


def load_config() -> Tuple[str, str]:
    """Load database URL and API key from .env"""
    env_path = '/var/www/html/eventheodds/.env'
    db_url = None
    api_key = None

    try:
        with open(env_path, 'r') as f:
            for line in f:
                if line.startswith('SPORTS_DATABASE_URL='):
                    db_url = line.split('=', 1)[1].strip().strip('"').split('?')[0]
                elif line.startswith('THE_ODDS_API_KEY='):
                    api_key = line.split('=', 1)[1].strip().strip('"')
    except FileNotFoundError:
        pass

    db_url = db_url or os.environ.get('SPORTS_DATABASE_URL', '').split('?')[0]
    api_key = api_key or os.environ.get('THE_ODDS_API_KEY')

    return db_url, api_key


class HistoricalFetcher:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.session = requests.Session()
        self.quota_used = 0
        self.quota_remaining = None

    def _request(self, endpoint: str, params: dict = None) -> Optional[dict]:
        """Make API request and track quota"""
        if params is None:
            params = {}
        params['apiKey'] = self.api_key

        url = f"{BASE_URL}{endpoint}"

        try:
            resp = self.session.get(url, params=params, timeout=60)

            self.quota_used = int(resp.headers.get('x-requests-used', 0))
            self.quota_remaining = int(resp.headers.get('x-requests-remaining', 0))

            if resp.status_code == 401:
                print(f"ERROR: Invalid API key")
                return None
            if resp.status_code == 422:
                return None  # No data for this timestamp
            if resp.status_code == 429:
                print(f"ERROR: Quota exceeded")
                return None
            if resp.status_code != 200:
                print(f"ERROR: API returned {resp.status_code}")
                return None

            return resp.json()

        except Exception as e:
            print(f"ERROR: Request failed: {e}")
            return None

    def get_historical_odds(self, sport_key: str, date_iso: str) -> Optional[dict]:
        """Get historical odds at a specific timestamp"""
        params = {
            'date': date_iso,
            'regions': 'us',
            'markets': ','.join(CORE_MARKETS),
            'oddsFormat': 'american'
        }
        return self._request(f'/historical/sports/{sport_key}/odds', params)

    def get_historical_events(self, sport_key: str, date_iso: str) -> Optional[dict]:
        """Get historical events at a specific timestamp (1 credit)"""
        params = {
            'date': date_iso,
        }
        return self._request(f'/historical/sports/{sport_key}/events', params)


def get_games_missing_lines(conn, league: str, start_date: str, end_date: str) -> List[dict]:
    """Find games missing opening or closing lines"""
    cur = conn.cursor()

    # Opening/closing lines are stored directly on SportsGame
    cur.execute('''
        SELECT
            id,
            "gameDate",
            "homeTeam",
            "awayTeam",
            "openingSpreadHome",
            "closingSpreadHome"
        FROM "SportsGame"
        WHERE league = %s
          AND "gameDate" BETWEEN %s AND %s
          AND LOWER(status) = 'final'
          AND ("openingSpreadHome" IS NULL OR "closingSpreadHome" IS NULL)
        ORDER BY "gameDate"
        LIMIT 500
    ''', (league, start_date, end_date))

    games = []
    for row in cur.fetchall():
        games.append({
            'id': row[0],
            'gameDate': row[1],
            'homeTeam': row[2],
            'awayTeam': row[3],
            'hasOpening': row[4] is not None,
            'hasClosing': row[5] is not None,
        })

    cur.close()
    return games


def find_opening_snapshot(fetcher: HistoricalFetcher, sport_key: str,
                          game_date: datetime, home_team: str, away_team: str) -> Optional[dict]:
    """Find the opening line snapshot for a game"""
    # Normalize DB team names for comparison
    home_norm = normalize_team(home_team)
    away_norm = normalize_team(away_team)

    # Try to find lines from 24-48 hours before game
    search_times = [
        game_date - timedelta(hours=36),
        game_date - timedelta(hours=24),
        game_date - timedelta(hours=18),
    ]

    for search_time in search_times:
        date_iso = search_time.strftime('%Y-%m-%dT%H:%M:%SZ')
        data = fetcher.get_historical_odds(sport_key, date_iso)

        if not data or not data.get('data'):
            continue

        # Find matching game using normalized team names
        for event in data['data']:
            api_home = normalize_team(event.get('home_team', ''))
            api_away = normalize_team(event.get('away_team', ''))
            if api_home == home_norm and api_away == away_norm:
                return {
                    'timestamp': data.get('timestamp'),
                    'event': event
                }

    return None


def find_closing_snapshot(fetcher: HistoricalFetcher, sport_key: str,
                          game_date: datetime, home_team: str, away_team: str) -> Optional[dict]:
    """Find the closing line snapshot for a game"""
    # Normalize DB team names for comparison
    home_norm = normalize_team(home_team)
    away_norm = normalize_team(away_team)

    # Try to find lines from close to game time (5-30 min before)
    search_times = [
        game_date - timedelta(minutes=10),
        game_date - timedelta(minutes=30),
        game_date - timedelta(hours=1),
    ]

    for search_time in search_times:
        date_iso = search_time.strftime('%Y-%m-%dT%H:%M:%SZ')
        data = fetcher.get_historical_odds(sport_key, date_iso)

        if not data or not data.get('data'):
            continue

        # Find matching game using normalized team names
        for event in data['data']:
            api_home = normalize_team(event.get('home_team', ''))
            api_away = normalize_team(event.get('away_team', ''))
            if api_home == home_norm and api_away == away_norm:
                return {
                    'timestamp': data.get('timestamp'),
                    'event': event
                }

    return None


def extract_consensus_line(event: dict) -> dict:
    """Extract consensus line from bookmakers (average of top books)"""
    spreads = []
    totals = []
    ml_home = []
    ml_away = []

    preferred_books = ['fanduel', 'draftkings', 'betmgm', 'caesars']

    for book in event.get('bookmakers', []):
        book_key = book.get('key', '')

        # Prioritize preferred books
        weight = 2 if book_key in preferred_books else 1

        for market in book.get('markets', []):
            market_key = market.get('key')

            if market_key == 'spreads':
                for o in market.get('outcomes', []):
                    if o.get('name') == event.get('home_team'):
                        if o.get('point') is not None:
                            spreads.extend([o.get('point')] * weight)

            elif market_key == 'totals':
                for o in market.get('outcomes', []):
                    if o.get('name') == 'Over' and o.get('point') is not None:
                        totals.extend([o.get('point')] * weight)

            elif market_key == 'h2h':
                for o in market.get('outcomes', []):
                    if o.get('name') == event.get('home_team'):
                        if o.get('price') is not None:
                            ml_home.extend([o.get('price')] * weight)
                    elif o.get('name') == event.get('away_team'):
                        if o.get('price') is not None:
                            ml_away.extend([o.get('price')] * weight)

    result = {}

    if spreads:
        result['spread_home'] = round(sum(spreads) / len(spreads) * 2) / 2  # Round to 0.5
    if totals:
        result['total'] = round(sum(totals) / len(totals) * 2) / 2
    if ml_home:
        result['ml_home'] = int(sum(ml_home) / len(ml_home))
    if ml_away:
        result['ml_away'] = int(sum(ml_away) / len(ml_away))

    return result


def update_game_odds(conn, game_id: int, opening: dict = None, closing: dict = None):
    """Update SportsGame with opening/closing lines"""
    cur = conn.cursor()

    updates = []
    values = []

    if opening:
        if opening.get('spread_home') is not None:
            updates.append('"openingSpreadHome" = %s')
            values.append(opening['spread_home'])
            updates.append('"openingSpreadAway" = %s')
            values.append(-opening['spread_home'])
        if opening.get('total') is not None:
            updates.append('"openingTotal" = %s')
            values.append(opening['total'])
        if opening.get('ml_home') is not None:
            updates.append('"openingMoneylineHome" = %s')
            values.append(opening['ml_home'])
        if opening.get('ml_away') is not None:
            updates.append('"openingMoneylineAway" = %s')
            values.append(opening['ml_away'])
        updates.append('"openingCapturedAt" = NOW()')

    if closing:
        if closing.get('spread_home') is not None:
            updates.append('"closingSpreadHome" = %s')
            values.append(closing['spread_home'])
            updates.append('"closingSpreadAway" = %s')
            values.append(-closing['spread_home'])
        if closing.get('total') is not None:
            updates.append('"closingTotal" = %s')
            values.append(closing['total'])
        if closing.get('ml_home') is not None:
            updates.append('"closingMoneylineHome" = %s')
            values.append(closing['ml_home'])
        if closing.get('ml_away') is not None:
            updates.append('"closingMoneylineAway" = %s')
            values.append(closing['ml_away'])
        updates.append('"closingCapturedAt" = NOW()')

    if updates:
        values.append(game_id)
        cur.execute(f'''
            UPDATE "SportsGame"
            SET {', '.join(updates)}
            WHERE id = %s
        ''', values)

    conn.commit()
    cur.close()


def backfill_missing_lines(fetcher: HistoricalFetcher, conn, league: str,
                           start_date: str, end_date: str, max_games: int = 50):
    """Backfill missing opening/closing lines for a date range"""
    sport_key = SPORTS.get(league)
    if not sport_key:
        print(f"Unknown league: {league}")
        return

    print(f"\n{'='*60}")
    print(f"HISTORICAL BACKFILL: {league.upper()}")
    print(f"Date range: {start_date} to {end_date}")
    print(f"{'='*60}")

    games = get_games_missing_lines(conn, league, start_date, end_date)
    print(f"Found {len(games)} games missing lines")

    if not games:
        return

    games = games[:max_games]
    print(f"Processing {len(games)} games...")

    updated_opening = 0
    updated_closing = 0
    credits_start = fetcher.quota_used

    for i, game in enumerate(games):
        game_date = game['gameDate']
        if game_date.tzinfo is None:
            game_date = game_date.replace(tzinfo=timezone.utc)

        print(f"\n[{i+1}/{len(games)}] {game['awayTeam']} @ {game['homeTeam']} ({game_date.date()})")

        opening = None
        closing = None

        # Fetch opening if missing
        if not game['hasOpening']:
            snapshot = find_opening_snapshot(
                fetcher, sport_key, game_date,
                game['homeTeam'], game['awayTeam']
            )
            if snapshot:
                opening = extract_consensus_line(snapshot['event'])
                if opening:
                    print(f"  Opening: spread={opening.get('spread_home')}, total={opening.get('total')}")
                    updated_opening += 1

        # Fetch closing if missing
        if not game['hasClosing']:
            snapshot = find_closing_snapshot(
                fetcher, sport_key, game_date,
                game['homeTeam'], game['awayTeam']
            )
            if snapshot:
                closing = extract_consensus_line(snapshot['event'])
                if closing:
                    print(f"  Closing: spread={closing.get('spread_home')}, total={closing.get('total')}")
                    updated_closing += 1

        # Update database
        if opening or closing:
            update_game_odds(conn, game['id'], opening, closing)

        # Check quota
        if fetcher.quota_remaining and fetcher.quota_remaining < 100:
            print("\nWARNING: Low quota, stopping early")
            break

    credits_used = fetcher.quota_used - credits_start

    print(f"\n{'='*60}")
    print(f"BACKFILL COMPLETE")
    print(f"  Opening lines updated: {updated_opening}")
    print(f"  Closing lines updated: {updated_closing}")
    print(f"  Credits used: {credits_used}")
    print(f"  Quota remaining: {fetcher.quota_remaining:,}" if fetcher.quota_remaining else "  Quota remaining: N/A")
    print(f"{'='*60}")


def backfill_date_range(fetcher: HistoricalFetcher, conn, league: str,
                        start_date: str, end_date: str):
    """Backfill all games in a date range"""
    sport_key = SPORTS.get(league)
    if not sport_key:
        print(f"Unknown league: {league}")
        return

    print(f"\n{'='*60}")
    print(f"HISTORICAL BACKFILL: {league.upper()}")
    print(f"Full date range: {start_date} to {end_date}")
    print(f"{'='*60}")

    cur = conn.cursor()

    # Get all games in range
    cur.execute('''
        SELECT
            id, "gameDate", "homeTeam", "awayTeam"
        FROM "SportsGame"
        WHERE league = %s
          AND "gameDate" BETWEEN %s AND %s
          AND LOWER(status) = 'final'
        ORDER BY "gameDate"
    ''', (league, start_date, end_date))

    games = cur.fetchall()
    cur.close()

    print(f"Found {len(games)} games")

    updated = 0
    credits_start = fetcher.quota_used

    for i, (game_id, game_date, home, away) in enumerate(games):
        if game_date.tzinfo is None:
            game_date = game_date.replace(tzinfo=timezone.utc)

        if (i + 1) % 10 == 0:
            remaining = f"{fetcher.quota_remaining:,}" if fetcher.quota_remaining else "N/A"
            print(f"Progress: {i+1}/{len(games)} games, {remaining} credits remaining")

        # Get opening snapshot
        opening = None
        opening_snap = find_opening_snapshot(fetcher, sport_key, game_date, home, away)
        if opening_snap:
            opening = extract_consensus_line(opening_snap['event'])

        # Get closing snapshot
        closing = None
        closing_snap = find_closing_snapshot(fetcher, sport_key, game_date, home, away)
        if closing_snap:
            closing = extract_consensus_line(closing_snap['event'])

        if opening or closing:
            update_game_odds(conn, game_id, opening, closing)
            updated += 1

        # Check quota
        if fetcher.quota_remaining and fetcher.quota_remaining < 100:
            print("\nWARNING: Low quota, stopping early")
            break

    credits_used = fetcher.quota_used - credits_start

    print(f"\n{'='*60}")
    print(f"BACKFILL COMPLETE")
    print(f"  Games updated: {updated}/{len(games)}")
    print(f"  Credits used: {credits_used}")
    print(f"  Quota remaining: {fetcher.quota_remaining:,}" if fetcher.quota_remaining else "  Quota remaining: N/A")
    print(f"{'='*60}")


def main():
    parser = argparse.ArgumentParser(description='Historical Odds Backfill')
    parser.add_argument('--date', type=str, help='Single date to backfill (YYYY-MM-DD)')
    parser.add_argument('--from', dest='from_date', type=str, help='Start date (YYYY-MM-DD)')
    parser.add_argument('--to', dest='to_date', type=str, help='End date (YYYY-MM-DD)')
    parser.add_argument('--missing', action='store_true', help='Only fill missing lines')
    parser.add_argument('--league', type=str, default='nba', help='League (nba, nfl, nhl, mlb)')
    parser.add_argument('--max-games', type=int, default=50, help='Max games to process')

    args = parser.parse_args()

    db_url, api_key = load_config()

    if not api_key:
        print("ERROR: THE_ODDS_API_KEY not configured")
        return 1

    if not db_url:
        print("ERROR: SPORTS_DATABASE_URL not configured")
        return 1

    fetcher = HistoricalFetcher(api_key)
    conn = psycopg2.connect(db_url)

    # Determine date range
    if args.date:
        start_date = args.date
        end_date = args.date
    elif args.from_date and args.to_date:
        start_date = args.from_date
        end_date = args.to_date
    else:
        # Default to last 7 days
        end_date = datetime.now(timezone.utc).strftime('%Y-%m-%d')
        start_date = (datetime.now(timezone.utc) - timedelta(days=7)).strftime('%Y-%m-%d')

    if args.missing:
        backfill_missing_lines(fetcher, conn, args.league, start_date, end_date, args.max_games)
    else:
        backfill_date_range(fetcher, conn, args.league, start_date, end_date)

    conn.close()
    return 0


if __name__ == '__main__':
    exit(main())
