"""
Sports Betting Module for Luke.

Bill's Distilled Betting Strategy — "Predictable Tempo" Philosophy
==================================================================
We bet on predictable tempo and structure, NOT outcomes.
Find the most controlled game environments and bet the scoring
range that matches the most common scorelines.

Core Process:
  1. Eliminate chaotic games (high-variance leagues, high-offense matchups)
  2. Find controlled games (defensive teams, low-creation matchups)
  3. Predict the scoring range (goal bands, not exact scores)
  4. Build matching bets (totals, spreads, correlated parlays)

Bill's Profile:
  - Platform: Bet365
  - Sports: NHL, NFL, EPL, Serie A, Ligue 1, La Liga, Liga Portugal, MLS
  - Style: Low-risk parlays, correlated boosts, profit boost optimization
  - Sweet spot: +120 to +220 boost range

Uses The Odds API (free tier: 500 requests/month).
Get a free key at: https://the-odds-api.com
"""

import os
import json
import logging
import requests
from datetime import datetime, date, timedelta
from config import Config

logger = logging.getLogger("sports-betting")

# The Odds API — free tier: 500 requests/month
# Use Config to ensure .env is loaded via python-dotenv first
ODDS_API_KEY = Config.ODDS_API_KEY or os.getenv("ODDS_API_KEY", "")
ODDS_BASE_URL = "https://api.the-odds-api.com/v4"

# Sport keys for The Odds API
SPORT_KEYS = {
    "nhl": "icehockey_nhl",
    "nfl": "americanfootball_nfl",
    "epl": "soccer_epl",
    "ncaaf": "americanfootball_ncaaf",
    "nba": "basketball_nba",
    "mlb": "baseball_mlb",
    "mls": "soccer_usa_mls",
    "cfl": "americanfootball_cfl",
    # European soccer leagues
    "seriea": "soccer_italy_serie_a",
    "serie_a": "soccer_italy_serie_a",
    "ligue1": "soccer_france_ligue_one",
    "ligue_1": "soccer_france_ligue_one",
    "laliga": "soccer_spain_la_liga",
    "la_liga": "soccer_spain_la_liga",
    "bundesliga": "soccer_germany_bundesliga",
    "liga_portugal": "soccer_portugal_primeira_liga",
    "eredivisie": "soccer_netherlands_eredivisie",
    # Cups
    "ucl": "soccer_uefa_champs_league",
    "champions_league": "soccer_uefa_champs_league",
    "europa": "soccer_uefa_europa_league",
    "europa_league": "soccer_uefa_europa_league",
    "conference": "soccer_uefa_europa_conference_league",
    "conference_league": "soccer_uefa_europa_conference_league",
    "fa_cup": "soccer_fa_cup",
    "copa_del_rey": "soccer_spain_copa_del_rey",
}

# ── BILL'S STRATEGY: League & Team Classifications ──────────────

# Soccer leagues ranked by predictability for unders/controlled tempo
SOCCER_LEAGUE_TIERS = {
    # TIER 1 — Best for controlled-tempo bets (low-scoring, tactical)
    "seriea": {"tier": 1, "label": "Serie A", "avg_goals": 2.5, "under_bias": True,
               "note": "Most tactically disciplined. Best league for unders."},
    "ligue1": {"tier": 1, "label": "Ligue 1", "avg_goals": 2.5, "under_bias": True,
               "note": "Low-creation outside PSG. Treat PSG games as outliers."},
    "liga_portugal": {"tier": 1, "label": "Liga Portugal", "avg_goals": 2.4, "under_bias": True,
                      "note": "Very predictable. Strong for Under 2.5 in mid-table clashes."},
    # TIER 2 — Usable with filtering
    "laliga": {"tier": 2, "label": "La Liga", "avg_goals": 2.6, "under_bias": False,
               "note": "Top 3 inflate goals. Mid-table matchups are solid."},
    "epl": {"tier": 2, "label": "EPL", "avg_goals": 2.8, "under_bias": False,
            "note": "Most variance. Only target 'boring' matchups (e.g., Everton/Palace/Wolves)."},
    "mls": {"tier": 2, "label": "MLS", "avg_goals": 3.0, "under_bias": False,
            "note": "Use Under 3.0 or 3.5 only. No Under 2.5 in MLS."},
    # TIER 3 — AVOID for unders
    "bundesliga": {"tier": 3, "label": "Bundesliga", "avg_goals": 3.2, "under_bias": False,
                   "note": "AVOID unders. High-pressing league, goals come in bunches."},
    "eredivisie": {"tier": 3, "label": "Eredivisie", "avg_goals": 3.3, "under_bias": False,
                   "note": "AVOID. Open, end-to-end play. Not suited to Bill's style."},
}

# NHL team classifications for tempo-based betting
NHL_TEAM_PROFILES = {
    # ── LOW-EVENT teams — best targets for Under bets ──
    "Carolina Hurricanes": {"tempo": "low", "label": "elite defense", "under_target": True},
    "Minnesota Wild": {"tempo": "low", "label": "grind team", "under_target": True},
    "Nashville Predators": {"tempo": "low", "label": "defensive structure", "under_target": True},
    "Dallas Stars": {"tempo": "low", "label": "low-event hockey", "under_target": True},
    "Winnipeg Jets": {"tempo": "low", "label": "structured play", "under_target": True},
    "New York Islanders": {"tempo": "low", "label": "defensive identity", "under_target": True},
    "Columbus Blue Jackets": {"tempo": "low", "label": "low-scoring", "under_target": True},
    "Los Angeles Kings": {"tempo": "low", "label": "structured west", "under_target": True},
    "Boston Bruins": {"tempo": "low", "label": "defensive system", "under_target": True},
    "New Jersey Devils": {"tempo": "low", "label": "tight structure", "under_target": True},
    "Vegas Golden Knights": {"tempo": "low", "label": "controlled pace", "under_target": True},
    "St Louis Blues": {"tempo": "low", "label": "grinder identity", "under_target": True},
    "Seattle Kraken": {"tempo": "low", "label": "low-event expansion", "under_target": True},
    "Vancouver Canucks": {"tempo": "low", "label": "defensive build", "under_target": True},
    "Anaheim Ducks": {"tempo": "low", "label": "rebuild/low-event", "under_target": True},
    # ── HIGH-OFFENSE teams — AVOID for unders ──
    "Toronto Maple Leafs": {"tempo": "high", "label": "high-offense", "under_target": False},
    "Edmonton Oilers": {"tempo": "high", "label": "high-firepower", "under_target": False},
    "Colorado Avalanche": {"tempo": "high", "label": "explosive offense", "under_target": False},
    "Florida Panthers": {"tempo": "high", "label": "high-event", "under_target": False},
    "Tampa Bay Lightning": {"tempo": "high", "label": "offensive upside", "under_target": False},
    "Ottawa Senators": {"tempo": "high", "label": "young/chaotic", "under_target": False},
    "Buffalo Sabres": {"tempo": "high", "label": "chaotic/rebuilding", "under_target": False},
    "Detroit Red Wings": {"tempo": "high", "label": "inconsistent/leaky", "under_target": False},
    "Pittsburgh Penguins": {"tempo": "high", "label": "skill-driven offense", "under_target": False},
    "New York Rangers": {"tempo": "high", "label": "transition offense", "under_target": False},
    "Washington Capitals": {"tempo": "high", "label": "offensive veterans", "under_target": False},
    "Calgary Flames": {"tempo": "high", "label": "aggressive forecheck", "under_target": False},
    "Philadelphia Flyers": {"tempo": "high", "label": "chaotic/rebuilding", "under_target": False},
    "Chicago Blackhawks": {"tempo": "high", "label": "rebuild/leaky", "under_target": False},
    "Montreal Canadiens": {"tempo": "high", "label": "young/fast", "under_target": False},
    "San Jose Sharks": {"tempo": "high", "label": "rebuild/high-event", "under_target": False},
    "Utah Hockey Club": {"tempo": "high", "label": "transition team", "under_target": False},
}

# Goal band strategy
GOAL_BANDS = {
    "soccer_under": {
        "primary": 2.5,   # Under 2.5 for Tier 1 leagues
        "mls": 3.0,       # MLS uses 3.0 minimum
        "safe": 3.5,      # When less confident, use 3.5
    },
    "nhl_under": {
        "primary": 6.5,   # NHL sweet spot: 5-6 goal band → Under 6.5
        "tight": 5.5,     # For two low-event teams head-to-head
    },
}

# Profit boost strategy
BOOST_STRATEGY = {
    "min_odds": 120,   # Minimum boost value worth taking (+120)
    "max_odds": 220,   # Maximum boost value (above this = too risky)
    "note": "Sweet spot +120 to +220. Only on legs you'd bet anyway.",
}

# Team name aliases for fuzzy matching
TEAM_ALIASES = {
    # NHL
    "habs": "Montreal Canadiens", "canadiens": "Montreal Canadiens", "montreal": "Montreal Canadiens",
    "sens": "Ottawa Senators", "senators": "Ottawa Senators", "ottawa": "Ottawa Senators",
    "leafs": "Toronto Maple Leafs", "maple leafs": "Toronto Maple Leafs", "toronto": "Toronto Maple Leafs",
    "bruins": "Boston Bruins", "boston": "Boston Bruins",
    "rangers": "New York Rangers",
    "penguins": "Pittsburgh Penguins", "pens": "Pittsburgh Penguins",
    "oilers": "Edmonton Oilers", "edmonton": "Edmonton Oilers",
    "flames": "Calgary Flames", "calgary": "Calgary Flames",
    "jets": "Winnipeg Jets", "winnipeg": "Winnipeg Jets",
    "canucks": "Vancouver Canucks", "vancouver": "Vancouver Canucks",
    "lightning": "Tampa Bay Lightning", "tampa": "Tampa Bay Lightning",
    "panthers": "Florida Panthers", "florida": "Florida Panthers",
    "capitals": "Washington Capitals", "caps": "Washington Capitals",
    "hurricanes": "Carolina Hurricanes", "carolina": "Carolina Hurricanes",
    "devils": "New Jersey Devils",
    "islanders": "New York Islanders",
    "wild": "Minnesota Wild", "minnesota": "Minnesota Wild",
    "avalanche": "Colorado Avalanche", "avs": "Colorado Avalanche",
    "stars": "Dallas Stars", "dallas": "Dallas Stars",
    "blues": "St. Louis Blues",
    "predators": "Nashville Predators", "preds": "Nashville Predators",
    "blackhawks": "Chicago Blackhawks",
    "red wings": "Detroit Red Wings", "wings": "Detroit Red Wings",
    "sabres": "Buffalo Sabres", "buffalo": "Buffalo Sabres",
    "kraken": "Seattle Kraken", "seattle": "Seattle Kraken",
    "coyotes": "Utah Hockey Club", "utah": "Utah Hockey Club",
    "ducks": "Anaheim Ducks",
    "sharks": "San Jose Sharks",
    "kings": "Los Angeles Kings",
    "golden knights": "Vegas Golden Knights", "vegas": "Vegas Golden Knights",
    "blue jackets": "Columbus Blue Jackets",
    # NFL
    "chiefs": "Kansas City Chiefs",
    "bills": "Buffalo Bills",
    "eagles": "Philadelphia Eagles", "philly": "Philadelphia Eagles",
    "cowboys": "Dallas Cowboys",
    "49ers": "San Francisco 49ers", "niners": "San Francisco 49ers",
    "ravens": "Baltimore Ravens",
    "bengals": "Cincinnati Bengals",
    "dolphins": "Miami Dolphins", "miami": "Miami Dolphins",
    "lions": "Detroit Lions", "detroit": "Detroit Lions",
    "packers": "Green Bay Packers",
    "bears": "Chicago Bears",
    "vikings": "Minnesota Vikings",
    "steelers": "Pittsburgh Steelers", "pittsburgh": "Pittsburgh Steelers",
    "chargers": "Los Angeles Chargers",
    "broncos": "Denver Broncos",
    "raiders": "Las Vegas Raiders",
    "texans": "Houston Texans",
    "jaguars": "Jacksonville Jaguars",
    "colts": "Indianapolis Colts",
    "titans": "Tennessee Titans",
    "commanders": "Washington Commanders",
    "giants": "New York Giants",
    "saints": "New Orleans Saints",
    "falcons": "Atlanta Falcons",
    "buccaneers": "Tampa Bay Buccaneers", "bucs": "Tampa Bay Buccaneers",
    "seahawks": "Seattle Seahawks",
    "cardinals": "Arizona Cardinals",
    "rams": "Los Angeles Rams",
    # EPL
    "arsenal": "Arsenal", "gunners": "Arsenal",
    "liverpool": "Liverpool", "reds": "Liverpool",
    "man city": "Manchester City", "city": "Manchester City",
    "man united": "Manchester United", "united": "Manchester United",
    "chelsea": "Chelsea",
    "tottenham": "Tottenham Hotspur", "spurs": "Tottenham Hotspur",
    "newcastle": "Newcastle United", "magpies": "Newcastle United",
    "aston villa": "Aston Villa", "villa": "Aston Villa",
    "brighton": "Brighton and Hove Albion",
    "west ham": "West Ham United", "hammers": "West Ham United",
    "crystal palace": "Crystal Palace", "palace": "Crystal Palace",
    "brentford": "Brentford",
    "fulham": "Fulham",
    "wolves": "Wolverhampton Wanderers", "wolverhampton": "Wolverhampton Wanderers",
    "bournemouth": "AFC Bournemouth",
    "nottingham": "Nottingham Forest", "forest": "Nottingham Forest",
    "everton": "Everton",
    "leicester": "Leicester City",
    "ipswich": "Ipswich Town",
    "southampton": "Southampton",
}


class SportsBettingAnalyst:
    """Luke's sports betting brain — built on Bill's Predictable Tempo strategy."""

    def __init__(self):
        self._available = None

    @property
    def available(self):
        """Check if the Odds API is configured."""
        if self._available is None:
            self._available = bool(ODDS_API_KEY)
            if self._available:
                logger.info("Sports betting module: ACTIVE")
            else:
                logger.info("Sports betting module: INACTIVE (set ODDS_API_KEY in .env)")
        return self._available

    # ── Strategy Engine ──────────────────────────────────────

    def classify_nhl_matchup(self, home, away):
        """
        Classify an NHL matchup by tempo.
        Returns: 'controlled', 'chaotic', or 'mixed'
        """
        home_profile = NHL_TEAM_PROFILES.get(home, {})
        away_profile = NHL_TEAM_PROFILES.get(away, {})
        home_tempo = home_profile.get("tempo", "medium")
        away_tempo = away_profile.get("tempo", "medium")

        if home_tempo == "low" and away_tempo == "low":
            return "controlled"
        elif home_tempo == "high" and away_tempo == "high":
            return "chaotic"
        elif home_tempo == "low" or away_tempo == "low":
            return "mixed"
        else:
            return "mixed"

    def classify_soccer_matchup(self, game):
        """
        Classify a soccer matchup by league tier and expected tempo.
        Returns dict with tier, recommendation, and notes.
        """
        sport_key = game.get("sport", "")
        league_info = SOCCER_LEAGUE_TIERS.get(sport_key, None)

        if not league_info:
            # Try to detect league from sport key
            for key, info in SOCCER_LEAGUE_TIERS.items():
                if key in sport_key:
                    league_info = info
                    break

        # European cup competitions — treat as Tier 1 (knockout rounds are cagey)
        if not league_info:
            cup_labels = {
                "europa": "Europa League",
                "europa_league": "Europa League",
                "conference": "Conference League",
                "conference_league": "Conference League",
                "ucl": "Champions League",
                "champions_league": "Champions League",
            }
            if sport_key in cup_labels:
                return {
                    "tier": 1,
                    "label": cup_labels[sport_key],
                    "avg_goals": 2.4,
                    "under_bias": True,
                    "is_cup": True,
                    "note": "CUP KNOCKOUT: Teams protect leads, first legs are cagey. Prime under territory.",
                }

        if not league_info:
            return {"tier": 2, "label": "Unknown League", "recommendation": "caution",
                    "note": "Unknown league — apply Tier 2 caution."}

        result = dict(league_info)

        # Cup game detection (domestic cups)
        if any(cup in sport_key for cup in ["cup", "copa"]):
            result["is_cup"] = True
            result["note"] = (
                "CUP GAME: Unders hit more in knockout rounds (teams protect leads). "
                "Group stages are less predictable. Check the stage."
            )

        return result

    def get_nhl_team_label(self, team_name):
        """Get the tempo label for an NHL team."""
        profile = NHL_TEAM_PROFILES.get(team_name, {})
        return profile.get("label", "unlabeled")

    def is_boost_worthy(self, odds_value):
        """Check if a profit boost falls in Bill's sweet spot (+120 to +220)."""
        return BOOST_STRATEGY["min_odds"] <= odds_value <= BOOST_STRATEGY["max_odds"]

    # API response cache: {endpoint: {"data": ..., "timestamp": datetime}}
    _api_cache = {}
    _api_cache_ttl = 900  # 15 minutes — avoids hammering the API
    _last_api_call = 0     # rate limiter
    # Sports that returned 401 — don't retry until restart
    _unavailable_sports = set()

    def _api_get(self, endpoint, params=None):
        """Make a request to The Odds API with caching and rate limiting."""
        if not self.available:
            return None
        if params is None:
            params = {}

        # Skip sports we already know aren't on our plan
        if endpoint in self._unavailable_sports:
            return None

        # Check cache first
        cache_key = endpoint
        cached = self._api_cache.get(cache_key)
        if cached and (datetime.utcnow() - cached["timestamp"]).total_seconds() < self._api_cache_ttl:
            logger.debug(f"Odds API cache hit: {endpoint}")
            return cached["data"]

        # Rate limit: minimum 2 seconds between API calls
        import time
        elapsed = time.time() - self._last_api_call
        if elapsed < 2:
            time.sleep(2 - elapsed)
        self._last_api_call = time.time()

        params["apiKey"] = ODDS_API_KEY
        try:
            url = f"{ODDS_BASE_URL}{endpoint}"
            logger.info(f"Odds API calling: {endpoint}")
            resp = requests.get(url, params=params, timeout=15)
            if resp.status_code == 200:
                remaining = resp.headers.get("x-requests-remaining", "?")
                data = resp.json()
                logger.info(f"Odds API OK — {len(data)} results, {remaining} requests remaining")
                # Only cache non-empty responses
                if data:
                    self._api_cache[cache_key] = {"data": data, "timestamp": datetime.utcnow()}
                return data
            elif resp.status_code == 401:
                # Sport not on our plan — remember it so we don't retry
                self._unavailable_sports.add(endpoint)
                logger.info(f"Odds API: {endpoint} not available on current plan — skipping")
                return None
            elif resp.status_code == 422:
                logger.warning(f"Odds API invalid request (422) — {resp.text[:200]}")
                return None
            elif resp.status_code == 429:
                logger.warning(f"Odds API rate limited — will use cache next cycle")
                return None
            else:
                logger.warning(f"Odds API error: {resp.status_code} — {resp.text[:200]}")
                return None
        except Exception as e:
            logger.warning(f"Odds API request failed: {e}")
            return None

    def resolve_team(self, query):
        """Resolve a team name from a casual query."""
        q = query.lower().strip()
        # Direct alias match
        if q in TEAM_ALIASES:
            return TEAM_ALIASES[q]
        # Partial match
        for alias, full_name in TEAM_ALIASES.items():
            if alias in q or q in alias:
                return full_name
        return None

    def detect_sport(self, query):
        """Detect which sport a query is about."""
        q = query.lower()
        # Explicit sport mentions
        if any(w in q for w in ["nhl", "hockey", "ice"]):
            return "nhl"
        if any(w in q for w in ["nfl", "football", "touchdown"]):
            return "nfl"
        if any(w in q for w in ["epl", "premier league", "soccer", "football"]):
            return "epl"
        if any(w in q for w in ["nba", "basketball"]):
            return "nba"

        # Try to detect by team name
        team = self.resolve_team(q)
        if team:
            # Check which sport this team belongs to
            nhl_teams = [v for k, v in TEAM_ALIASES.items() if any(
                t in v for t in ["Canadiens", "Senators", "Maple Leafs", "Bruins", "Rangers",
                                  "Penguins", "Oilers", "Flames", "Jets", "Canucks", "Lightning",
                                  "Panthers", "Capitals", "Hurricanes", "Devils", "Islanders",
                                  "Wild", "Avalanche", "Stars", "Blues", "Predators", "Blackhawks",
                                  "Red Wings", "Sabres", "Kraken", "Hockey Club", "Ducks",
                                  "Sharks", "Kings", "Golden Knights", "Blue Jackets"]
            )]
            if team in nhl_teams:
                return "nhl"

            nfl_teams = [v for k, v in TEAM_ALIASES.items() if any(
                t in v for t in ["Chiefs", "Bills", "Eagles", "Cowboys", "49ers", "Ravens",
                                  "Bengals", "Dolphins", "Lions", "Packers", "Bears", "Vikings",
                                  "Steelers", "Chargers", "Broncos", "Raiders", "Texans",
                                  "Jaguars", "Colts", "Titans", "Commanders", "Giants",
                                  "Saints", "Falcons", "Buccaneers", "Seahawks", "Cardinals", "Rams"]
            )]
            if team in nfl_teams:
                return "nfl"

            return "epl"  # Default for unmatched

        return None

    def get_upcoming_games(self, sport="nhl", days_ahead=2):
        """Get upcoming games with odds for a sport."""
        sport_key = SPORT_KEYS.get(sport)
        if not sport_key:
            logger.warning(f"Unknown sport key: {sport}")
            return []

        data = self._api_get(f"/sports/{sport_key}/odds/", params={
            "regions": "us,us2,uk,eu",
            "markets": "h2h,spreads,totals",
            "oddsFormat": "american",
            "dateFormat": "iso",
        })

        if not data:
            logger.info(f"No data returned for {sport} ({sport_key})")
            return []

        # Filter to upcoming games within the window
        now = datetime.utcnow()
        cutoff = now + timedelta(days=days_ahead)
        games = []
        skipped = 0
        for game in data:
            try:
                game_time = datetime.fromisoformat(game["commence_time"].replace("Z", "+00:00")).replace(tzinfo=None)
                if game_time < now:
                    skipped += 1
                    continue  # Already started — skip
                if game_time <= cutoff:
                    games.append(self._parse_game(game, sport))
                else:
                    skipped += 1
            except (KeyError, ValueError) as e:
                logger.debug(f"Skipped game parse error: {e}")
                continue

        logger.info(f"{sport}: {len(games)} upcoming games within window, {skipped} skipped (started or beyond cutoff)")
        return games

    def get_game_for_team(self, team_query, sport=None):
        """Find an upcoming game for a specific team."""
        team_name = self.resolve_team(team_query)
        if not team_name:
            return None, f"I don't recognize '{team_query}' as a team. Try the full name?"

        if not sport:
            sport = self.detect_sport(team_query)
        if not sport:
            # Try all Bill's sports
            for s in ["nhl", "nfl", "epl"]:
                games = self.get_upcoming_games(s, days_ahead=7)
                for g in games:
                    if team_name.lower() in g["home_team"].lower() or team_name.lower() in g["away_team"].lower():
                        return g, None
            return None, f"No upcoming game found for {team_name}."

        games = self.get_upcoming_games(sport, days_ahead=7)
        for g in games:
            if team_name.lower() in g["home_team"].lower() or team_name.lower() in g["away_team"].lower():
                return g, None

        return None, f"No upcoming {sport.upper()} game found for {team_name} in the next 7 days."

    def _parse_game(self, raw, sport):
        """Parse raw API game data into a clean structure."""
        game = {
            "id": raw.get("id"),
            "sport": sport,
            "home_team": raw.get("home_team", ""),
            "away_team": raw.get("away_team", ""),
            "commence_time": raw.get("commence_time", ""),
            "bookmakers": [],
        }

        # Parse commence time for display
        try:
            ct = datetime.fromisoformat(raw["commence_time"].replace("Z", "+00:00"))
            # Convert to Eastern (UTC-5)
            eastern = ct - timedelta(hours=5)
            game["game_date"] = eastern.strftime("%A, %B %-d")
            game["game_time"] = eastern.strftime("%-I:%M %p ET")
        except (ValueError, KeyError):
            game["game_date"] = ""
            game["game_time"] = ""

        # Parse bookmaker odds — prefer Bet365, fallback to DraftKings/FanDuel
        preferred_books = ["bet365", "draftkings", "fanduel", "bovada", "betmgm"]
        for book_name in preferred_books:
            for bm in raw.get("bookmakers", []):
                if bm["key"] == book_name:
                    book_data = {"name": bm["title"], "markets": {}}
                    for market in bm.get("markets", []):
                        book_data["markets"][market["key"]] = market["outcomes"]
                    game["bookmakers"].append(book_data)
                    break

        return game

    def generate_analysis(self, team_query):
        """
        Generate a strategy-driven pre-game analysis using Bill's
        Predictable Tempo philosophy.

        No odds displayed — just game environment classification,
        pre-game chatter, and recommended legs that fit the matchup.
        """
        if not self.available:
            return "Sports betting not configured. Add ODDS_API_KEY to your .env file. Get a free key at the-odds-api.com — Luke"

        game, error = self.get_game_for_team(team_query)
        if error:
            return error

        sport = game["sport"]
        lines = [f"{game['away_team']} @ {game['home_team']}"]
        lines.append(f"{game['game_date']} • {game['game_time']}")

        # ── Tempo Classification ──
        if sport == "nhl":
            tempo = self.classify_nhl_matchup(game["home_team"], game["away_team"])
            home_label = self.get_nhl_team_label(game["home_team"])
            away_label = self.get_nhl_team_label(game["away_team"])
            lines.append(f"Tempo: {tempo.upper()}")
            if home_label != "unlabeled":
                lines.append(f"  {game['home_team']}: {home_label}")
            if away_label != "unlabeled":
                lines.append(f"  {game['away_team']}: {away_label}")
        elif sport in SOCCER_LEAGUE_TIERS or any(sport.startswith(k) for k in ["epl", "serie", "ligue", "laliga", "liga_"]):
            league_class = self.classify_soccer_matchup(game)
            lines.append(f"League: {league_class.get('label', sport.upper())} (Tier {league_class['tier']})")
            if league_class.get("is_cup"):
                lines.append(f"  CUP GAME — knockout round unders hit more often")
            if league_class["tier"] == 3:
                lines.append(f"  CAUTION: {league_class['note']}")
        lines.append("")

        # ── Pre-Game Chatter ──
        try:
            from pregame_chatter import get_pregame_chatter, format_chatter_for_whatsapp
            chatter = get_pregame_chatter(game["home_team"], game["away_team"], sport)
            chatter_text = format_chatter_for_whatsapp(chatter, game["home_team"], game["away_team"])
            if chatter_text:
                lines.append(chatter_text)
                lines.append("")
        except Exception as e:
            logger.debug(f"Chatter fetch failed (non-critical): {e}")

        # ── Suggested Legs ──
        lines.append("SUGGESTED LEGS:")
        lines.append("")

        suggestions = self._generate_suggestions(game)
        for s in suggestions:
            lines.append(f"  {s}")

        # ── Pregame Validation ──
        try:
            from pregame_validator import validate_pick, format_validation_for_whatsapp
            validation = validate_pick(game["home_team"], game["away_team"], sport)
            pick_with_validation = {"validation": validation}
            validation_text = format_validation_for_whatsapp(pick_with_validation)
            if validation_text:
                lines.append("")
                lines.append(validation_text)
        except ImportError:
            logger.debug("pregame_validator not available — skipping validation")
        except Exception as e:
            logger.debug(f"Pregame validation failed (non-critical): {e}")

        lines.append("")
        lines.append("— Luke")

        return "\n".join(lines)

    def _generate_suggestions(self, game):
        """
        Generate leg recommendations based on Bill's Predictable Tempo strategy.

        No odds or prices — just the legs that fit the game environment.
        Uses the API data internally to detect the favorite and total line,
        then recommends legs purely on matchup structure.
        """
        suggestions = []
        sport = game["sport"]

        # Extract structural info (favorite, total line) — used internally only
        favorite_name = None
        total_line = 0
        if game["bookmakers"]:
            book = game["bookmakers"][0]
            h2h = book["markets"].get("h2h", [])
            totals = book["markets"].get("totals", [])
            if h2h:
                fav = min(h2h, key=lambda x: x["price"])
                favorite_name = fav["name"]
            over = next((t for t in totals if t["name"] == "Over"), None) if totals else None
            total_line = over.get("point", 0) if over else 0

        # ── NHL Strategy ──────────────────────────────────
        if sport == "nhl":
            tempo = self.classify_nhl_matchup(game["home_team"], game["away_team"])

            if tempo == "controlled":
                suggestions.append("CONTROLLED GAME — prime target")
                suggestions.append("Two low-event teams = predictable tempo.")
                suggestions.append("")
                suggestions.append("Legs that fit:")
                suggestions.append("  Under 6.5 goals")
                suggestions.append("  5-6 goal band is most likely scoreline range")
                if favorite_name:
                    suggestions.append(f"  {favorite_name} ML (tight win expected)")
                    suggestions.append("")
                    suggestions.append("PARLAY IDEA:")
                    suggestions.append(f"  {favorite_name} ML + Under 6.5")
                    suggestions.append("  Favorite wins a tight, low-scoring game.")
                    suggestions.append("  Good candidate for a profit boost.")

            elif tempo == "chaotic":
                suggestions.append("CHAOTIC MATCHUP — skip or go light")
                suggestions.append("Both teams can score. Hard to predict the range.")
                suggestions.append("")
                suggestions.append("If you must play:")
                suggestions.append("  Over 6.5 goals (goals are coming)")
                suggestions.append("  Avoid unders in this matchup.")

            else:  # mixed
                home_p = NHL_TEAM_PROFILES.get(game["home_team"], {})
                away_p = NHL_TEAM_PROFILES.get(game["away_team"], {})
                anchor = game["home_team"] if home_p.get("tempo") == "low" else game["away_team"]
                suggestions.append("MIXED TEMPO — one team anchors the pace")
                suggestions.append(f"{anchor} controls the tempo here.")
                suggestions.append("")
                suggestions.append("Legs that fit:")
                suggestions.append("  Under 6.5 goals (defensive team dictates pace)")
                if favorite_name:
                    suggestions.append(f"  {favorite_name} ML")
                    suggestions.append("")
                    suggestions.append("PARLAY IDEA:")
                    suggestions.append(f"  {favorite_name} ML + Under 6.5")
                    suggestions.append("  The defensive team keeps it tight.")

        # ── Soccer Strategy ───────────────────────────────
        elif sport in ("epl", "seriea", "serie_a", "ligue1", "ligue_1", "laliga", "la_liga",
                       "liga_portugal", "mls", "bundesliga", "eredivisie") or "soccer" in str(game.get("id", "")):

            league_class = self.classify_soccer_matchup(game)
            tier = league_class.get("tier", 2)

            if tier == 1:
                suggestions.append(f"{league_class['label']} — TIER 1 (best for unders)")
                suggestions.append(league_class.get("note", ""))
                suggestions.append("")
                suggestions.append("Legs that fit:")
                suggestions.append("  Under 2.5 goals")
                suggestions.append("  Most common scoreline: 1-0 or 1-1")
                if favorite_name:
                    suggestions.append(f"  {favorite_name} to win")
                    suggestions.append("")
                    suggestions.append("PARLAY IDEA:")
                    suggestions.append(f"  {favorite_name} ML + Under 2.5 goals")
                    suggestions.append("  Favorite wins 1-0 or 2-1, controls possession.")

            elif tier == 2:
                suggestions.append(f"{league_class['label']} — TIER 2 (filter needed)")
                suggestions.append(league_class.get("note", ""))
                suggestions.append("")
                if sport == "mls":
                    suggestions.append("Legs that fit:")
                    suggestions.append("  Under 3.0 or Under 3.5 goals (no Under 2.5 in MLS)")
                    if favorite_name:
                        suggestions.append(f"  {favorite_name} to win")
                elif sport == "epl":
                    suggestions.append("Only play if both teams are mid/low-table:")
                    suggestions.append("  Under 2.5 goals")
                    if favorite_name:
                        suggestions.append(f"  {favorite_name} to win")
                    suggestions.append("  Skip if top-6 teams are involved.")
                else:
                    suggestions.append("Legs that fit (mid-table matchups only):")
                    suggestions.append("  Under 2.5 goals")
                    if favorite_name:
                        suggestions.append(f"  {favorite_name} to win")

                if favorite_name:
                    suggestions.append("")
                    suggestions.append("PARLAY IDEA:")
                    suggestions.append(f"  {favorite_name} ML + Under 2.5 goals")

            elif tier == 3:
                suggestions.append(f"{league_class['label']} — TIER 3 (AVOID)")
                suggestions.append(league_class.get("note", ""))
                suggestions.append("")
                suggestions.append("Strategy says skip unders in this league.")
                if favorite_name:
                    suggestions.append(f"If you must play: {favorite_name} ML only.")
                    suggestions.append("No totals — too unpredictable.")

            # Cup game overlay
            if league_class.get("is_cup"):
                suggestions.append("")
                suggestions.append("CUP PATTERN:")
                suggestions.append("  Knockout rounds favor unders (teams protect leads).")
                suggestions.append("  First legs especially — both teams cautious.")
                suggestions.append("  Under 2.5 is playable even in higher-scoring leagues.")

        # ── NFL Strategy ──────────────────────────────────
        elif sport == "nfl":
            suggestions.append("")
            if total_line >= 48:
                suggestions.append("HIGH TOTAL — contrarian under territory")
                suggestions.append("Legs that fit:")
                suggestions.append(f"  Under {total_line} (shootout lines tend to overshoot)")
            elif total_line <= 41:
                suggestions.append("LOW TOTAL — defensive game expected")
                suggestions.append("Legs that fit:")
                suggestions.append(f"  Under {total_line} (both defenses are legit)")
            else:
                suggestions.append("STANDARD TOTAL — need to dig into the matchup")
                suggestions.append("Legs that fit:")
                suggestions.append(f"  Lean Under {total_line} if defenses travel well")

            if favorite_name:
                suggestions.append(f"  {favorite_name} ML or spread")
                suggestions.append("")
                suggestions.append("PARLAY IDEA:")
                suggestions.append(f"  {favorite_name} ML + Under {total_line}")

        # ── Fallback ──────────────────────────────────────
        else:
            if favorite_name:
                suggestions.append(f"  {favorite_name} to win")
            suggestions.append("  No specific strategy profile for this sport yet.")
            suggestions.append("  Check Bet365 for what looks right.")

        # ── Boost Reminder ────────────────────────────────
        if favorite_name and len(suggestions) > 2:
            suggestions.append("")
            suggestions.append("Check Bet365 for profit boosts in the +120 to +220 range.")
            suggestions.append("Only use boosts on legs you'd already play.")

        if not suggestions:
            suggestions.append("Not enough data to suggest legs. Check Bet365 directly.")

        return suggestions

    def get_upcoming_events(self, sport="nhl", days_ahead=1):
        """
        Fallback: get upcoming events (games) without requiring odds.
        The Odds API /events endpoint returns scheduled games even if
        no bookmaker has posted odds yet.
        """
        sport_key = SPORT_KEYS.get(sport)
        if not sport_key:
            return []

        data = self._api_get(f"/sports/{sport_key}/events/", params={
            "dateFormat": "iso",
        })

        if not data:
            return []

        now = datetime.utcnow()
        cutoff = now + timedelta(days=days_ahead)
        games = []
        for event in data:
            try:
                game_time = datetime.fromisoformat(event["commence_time"].replace("Z", "+00:00")).replace(tzinfo=None)
                if game_time < now:
                    continue  # Already started — skip
                if game_time <= cutoff:
                    game = {
                        "id": event.get("id"),
                        "sport": sport,
                        "home_team": event.get("home_team", ""),
                        "away_team": event.get("away_team", ""),
                        "commence_time": event.get("commence_time", ""),
                        "bookmakers": [],
                    }
                    # Parse time for display
                    eastern = game_time - timedelta(hours=5)
                    game["game_date"] = eastern.strftime("%A, %B %-d")
                    game["game_time"] = eastern.strftime("%-I:%M %p ET")
                    games.append(game)
            except (KeyError, ValueError):
                continue

        logger.info(f"{sport} events fallback: {len(games)} games found")
        return games

    def get_todays_slate(self, sport=None):
        """
        Get today's full slate with tempo classification.
        Games are tagged as CONTROLLED / MIXED / CHAOTIC to help
        Bill quickly identify the best betting targets.
        """
        sports = [sport] if sport else ["nhl", "epl", "seriea", "ligue1", "laliga", "liga_portugal", "europa", "conference", "ucl", "mls"]
        all_games = []

        for s in sports:
            games = self.get_upcoming_games(s, days_ahead=1)
            if not games:
                # Fallback: try events endpoint (no odds required)
                games = self.get_upcoming_events(s, days_ahead=1)
            for g in games:
                all_games.append(g)

        if not all_games:
            if not self.available:
                return "Sports betting not configured. Add ODDS_API_KEY to your .env file. Get a free key at the-odds-api.com — Luke"
            return "Odds API returned no games right now. Could be early — bookmakers may not have posted yet. Check back in a bit or try 'what's good tonight'. — Luke"

        lines = ["TODAY'S SLATE — Luke", ""]

        # Group by sport
        by_sport = {}
        for g in all_games:
            s = g["sport"]
            if s not in by_sport:
                by_sport[s] = []
            by_sport[s].append(g)

        for sport_key, games in by_sport.items():
            sport_label = sport_key.upper()
            if sport_key in SOCCER_LEAGUE_TIERS:
                sport_label = SOCCER_LEAGUE_TIERS[sport_key]["label"]
            lines.append(f"── {sport_label} ──")

            for g in games:
                tempo_tag = ""

                # Tempo classification
                if sport_key == "nhl":
                    tempo = self.classify_nhl_matchup(g["home_team"], g["away_team"])
                    if tempo == "controlled":
                        tempo_tag = " ★ CONTROLLED"
                    elif tempo == "chaotic":
                        tempo_tag = " ⚡ CHAOTIC"
                    else:
                        tempo_tag = " ~ MIXED"
                elif sport_key in SOCCER_LEAGUE_TIERS:
                    tier = SOCCER_LEAGUE_TIERS[sport_key]["tier"]
                    if tier == 1:
                        tempo_tag = " ★ TIER 1"
                    elif tier == 3:
                        tempo_tag = " ⚠ AVOID"

                lines.append(f"{g['away_team']} @ {g['home_team']}")
                lines.append(f"  {g['game_time']}{tempo_tag}")
                lines.append("")

        # ── Top Headlines ──
        try:
            from pregame_chatter import get_general_sport_headlines
            # Pull headlines from the first sport in the slate
            primary_sport = list(by_sport.keys())[0] if by_sport else "nhl"
            headlines = get_general_sport_headlines(primary_sport, max_items=3)
            if headlines:
                lines.append("TOP HEADLINES:")
                for h in headlines:
                    lines.append(f"  {h['title']}")
                    lines.append(f"    — {h['source']}")
                lines.append("")
        except Exception as e:
            logger.debug(f"Headlines fetch failed (non-critical): {e}")

        lines.append("★ = best targets for your strategy")
        lines.append("Reply 'odds [team]' for a full breakdown.")
        return "\n".join(lines)

    def get_best_targets(self):
        """
        'What's good today/tonight' — only returns games that pass
        Bill's strategy filter. No noise, just the plays.

        Filter rules:
          NHL: controlled or mixed tempo only (skip chaotic)
          Soccer: Tier 1 leagues only + Tier 2 with caution note
          NFL: games with total <= 44 (defensive matchups)
          Everything else: skip
        """
        if not self.available:
            return "Sports betting not configured. Add ODDS_API_KEY to your .env file. Get a free key at the-odds-api.com — Luke"

        sports = ["nhl", "epl", "seriea", "ligue1", "laliga", "liga_portugal", "europa", "conference", "ucl", "mls"]
        targets = []

        for s in sports:
            games = self.get_upcoming_games(s, days_ahead=1)
            if not games:
                games = self.get_upcoming_events(s, days_ahead=1)
            for g in games:
                if s == "nhl":
                    tempo = self.classify_nhl_matchup(g["home_team"], g["away_team"])
                    if tempo == "controlled":
                        targets.append((g, s, "★ CONTROLLED — prime target"))
                    elif tempo == "mixed":
                        # Find the anchor
                        home_p = NHL_TEAM_PROFILES.get(g["home_team"], {})
                        away_p = NHL_TEAM_PROFILES.get(g["away_team"], {})
                        anchor = g["home_team"] if home_p.get("tempo") == "low" else g["away_team"]
                        targets.append((g, s, f"MIXED — {anchor} anchors the pace"))
                    # chaotic = skip entirely
                else:
                    league_class = self.classify_soccer_matchup(g)
                    tier = league_class.get("tier", 2)
                    if tier == 1:
                        targets.append((g, s, f"★ {league_class['label']} (Tier 1)"))
                    elif tier == 2 and s not in ("bundesliga", "eredivisie"):
                        targets.append((g, s, f"{league_class['label']} (Tier 2 — filter)"))

        if not targets:
            return "Nothing jumps out today. Strategy says sit tight. — Luke"

        lines = ["WHAT'S GOOD TODAY — Luke", ""]
        lines.append(f"{len(targets)} game{'s' if len(targets) != 1 else ''} passed the filter:")
        lines.append("")

        for g, sport, tag in targets:
            # Get favorite name internally
            favorite_name = None
            if g["bookmakers"]:
                h2h = g["bookmakers"][0]["markets"].get("h2h", [])
                if h2h:
                    fav = min(h2h, key=lambda x: x["price"])
                    favorite_name = fav["name"]

            lines.append(f"{g['away_team']} @ {g['home_team']}")
            lines.append(f"  {g['game_time']} — {tag}")

            # Quick leg suggestion (from Bill's bet_legs.csv hit rates)
            if sport == "nhl":
                if "CONTROLLED" in tag:
                    if favorite_name:
                        lines.append(f"  Legs: U6.5 + {favorite_name} +1.5 PL + opponent U2.5 TT")
                    else:
                        lines.append(f"  Legs: U6.5 + U3.5 first two periods + 1P U1.5")
                elif "MIXED" in tag:
                    lines.append(f"  Legs: U6.5 + non-anchor U3.5 TT + fav +1.5 PL")
            else:
                league_class = self.classify_soccer_matchup(g)
                tier = league_class.get("tier", 2)
                is_cup = league_class.get("is_cup", False)
                avg_goals = league_class.get("avg_goals", 2.6)
                if tier == 1:
                    if is_cup:
                        if favorite_name:
                            legs = f"{favorite_name} DC + Range 1-4 + {favorite_name} O3 corners"
                        else:
                            legs = "U2.5 + Range 0-2 + Corners U9.5"
                    elif avg_goals <= 2.3:
                        if favorite_name:
                            legs = f"{favorite_name} DC + opponent Not 2-3 range + {favorite_name} O2 corners"
                        else:
                            legs = "Range 1-4 + U2.5 + Corners O5"
                    else:
                        if favorite_name:
                            legs = f"{favorite_name} DC + O1 goals + {favorite_name} O3 corners"
                        else:
                            legs = "Range 1-5 + O1 goals + Corners O6"
                    lines.append(f"  Legs: {legs}")
                elif tier == 2:
                    if favorite_name:
                        legs = f"{favorite_name} DC + Range 1-4 + {favorite_name} O3 corners"
                    else:
                        legs = "U2.5 + Range 0-3 + Corners U9.5"
                    lines.append(f"  Legs: {legs} (caution — Tier 2)")

            # Pregame validation tag
            try:
                from pregame_validator import validate_pick
                v = validate_pick(g["home_team"], g["away_team"], s)
                verdict = v.get("verdict", "")
                reason = v.get("reason", "")
                if verdict == "VALIDATED":
                    lines.append(f"  ★ PREGAME: VALIDATED — {reason[:60]}")
                elif verdict == "CAUTION":
                    lines.append(f"  ⚠️ PREGAME: CAUTION — {reason[:60]}")
                elif verdict == "DROPPED":
                    lines.append(f"  ❌ PREGAME: DROPPED — {reason[:60]}")
            except Exception:
                pass  # Non-critical — skip silently

            lines.append("")

        # Pre-game chatter for the top target
        try:
            from pregame_chatter import get_pregame_chatter, format_chatter_for_whatsapp
            top_game, top_sport, _ = targets[0]
            chatter = get_pregame_chatter(top_game["home_team"], top_game["away_team"], top_sport)
            chatter_text = format_chatter_for_whatsapp(chatter, top_game["home_team"], top_game["away_team"])
            if chatter_text:
                lines.append(chatter_text)
                lines.append("")
        except Exception:
            pass

        lines.append("Check Bet365 for profit boosts in the +120 to +220 range.")
        lines.append("Reply 'odds [team]' to dive deeper on any game.")
        return "\n".join(lines)


# Module-level singleton
_analyst = None


def get_analyst():
    """Get the singleton SportsBettingAnalyst."""
    global _analyst
    if _analyst is None:
        _analyst = SportsBettingAnalyst()
    return _analyst
