"""
Bet9ja Nigeria scraper.

API (no auth required):
  Bootstrap:   GET https://sports.bet9ja.com/desktop/feapi/JsObjectAjax/Desktop
  Prematch:    GET https://sports.bet9ja.com/desktop/feapi/PalimpsestAjax/GetSports?DISP=1000
  Live:        GET https://sports.bet9ja.com/desktop/feapi/PalimpsestAjax/GetSports?DISP=0
               (falls back to prematch PAL filtered for recently-started events if DISP=0 is empty)
  Odds:        GET https://sports.bet9ja.com/desktop/feapi/PalimpsestAjax/GetEvent?EVENTID={id}

GetSports response structure:
  D.PAL[sport_id].SG[sg_id].G[group_id].E[event_id] = {N, D (datetime str), C}

GetEvent response:
  D.DS   — "Home - Away" name
  D.STARTDATE — "YYYY-MM-DD HH:MM:SS" UTC
  D.O    — {key: decimal_odds} where keys follow pattern:
             Soccer:     S_1X2_1 / S_1X2_X / S_1X2_2
                         S_OU@2.5_O / S_OU@2.5_U
                         S_DNB_1 / S_DNB_2 (Draw No Bet: Home / Away)
                         S_AH@{line}_1 / S_AH@{line}_2 (Asian Handicap, e.g. S_AH@-1.5_1)
             Basketball: B_12_1 / B_12_2
             Tennis:     T_12_1 / T_12_2

Sport IDs (in GetSports response): Soccer=1, Basketball=2, Tennis=5
"""
import logging
import re
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta
from typing import List, Optional

from scrapers.base import BaseScraper
from core.models import Event, Outcome
import config

logger = logging.getLogger(__name__)

BASE            = 'https://sports.bet9ja.com/desktop/feapi/PalimpsestAjax'
SPORTS_URL      = f'{BASE}/GetSports?DISP=1000'
LIVE_SPORTS_URL = f'{BASE}/GetSports?DISP=0'   # DISP=0 likely returns in-play events
EVENT_URL       = f'{BASE}/GetEvent'

SPORT_IDS = {
    'football':   '1',
    'basketball': '2',
    'tennis':     '5',
}

# (market_name, odds_keys {label: key_suffix}, prefix)
MARKETS = {
    'football': [
        ('1X2',            {'Home': 'S_1X2_1', 'Draw': 'S_1X2_X', 'Away': 'S_1X2_2'}),
        ('Double Chance',  {'1X': 'S_DC_1X', 'X2': 'S_DC_X2', '12': 'S_DC_12'}),
        ('Over/Under 0.5', {'Over': 'S_OU@0.5_O', 'Under': 'S_OU@0.5_U'}),
        ('Over/Under 1.5', {'Over': 'S_OU@1.5_O', 'Under': 'S_OU@1.5_U'}),
        ('Over/Under 2.5', {'Over': 'S_OU@2.5_O', 'Under': 'S_OU@2.5_U'}),
        ('Over/Under 3.5', {'Over': 'S_OU@3.5_O', 'Under': 'S_OU@3.5_U'}),
        ('Over/Under 4.5', {'Over': 'S_OU@4.5_O', 'Under': 'S_OU@4.5_U'}),
        ('BTTS',           {'Yes': 'S_GGNG_Y', 'No': 'S_GGNG_N'}),
        ('Draw No Bet',    {'Home': 'S_DNB_1',  'Away': 'S_DNB_2'}),
    ],
    'basketball': [
        ('Home/Away', {'Home': 'B_12_1', 'Away': 'B_12_2'}),
    ],
    'tennis': [
        ('Home/Away', {'Home': 'T_12_1', 'Away': 'T_12_2'}),
    ],
}

MAX_WORKERS = 8   # parallel GetEvent requests — keep low to avoid rate limiting


class Bet9jaScraper(BaseScraper):

    def __init__(self):
        super().__init__('Bet9ja')
        self.session.headers.update({
            'Referer':          'https://sports.bet9ja.com/',
            'X-Requested-With': 'XMLHttpRequest',
        })
        self._pal_cache: Optional[dict] = None
        self._pal_fetched_at: float = 0.0
        self._live_pal_cache: Optional[dict] = None
        self._live_pal_fetched_at: float = 0.0

    # ── Public ────────────────────────────────────────────────────────────────

    def get_all_events(self) -> List[Event]:
        """Override: warm PAL cache once, then fetch all sports in one thread pool."""
        try:
            self._get_pal()
        except Exception as ex:
            logger.error(f'[Bet9ja] GetSports failed: {ex}')
            return []
        all_stubs: list = []
        for sport, sport_id in SPORT_IDS.items():
            try:
                stubs = self._fetch_stubs(sport_id)
                for s in stubs:
                    s['sport'] = sport
                all_stubs.extend(stubs)
                logger.info(f'[Bet9ja] {sport}: {len(stubs)} stubs')
            except Exception as ex:
                logger.error(f'[Bet9ja] GetSports {sport} failed: {ex}')

        all_events: List[Event] = []
        with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
            futures = {pool.submit(self._fetch_event_odds, s): s for s in all_stubs}
            for fut in as_completed(futures):
                raw = fut.result()
                if raw:
                    stub = raw['_stub']
                    all_events.extend(self._parse(raw, stub['sport']))

        return all_events

    def get_events(self, sport: str) -> List[Event]:
        sport_id = SPORT_IDS.get(sport)
        if not sport_id:
            return []

        try:
            event_stubs = self._fetch_stubs(sport_id)
        except Exception as ex:
            logger.error(f'[Bet9ja] GetSports failed: {ex}')
            return []

        if not event_stubs:
            return []

        return self._fetch_odds_parallel(event_stubs, sport)

    def get_live_events(self, sport: str) -> List[Event]:
        sport_id = SPORT_IDS.get(sport)
        if not sport_id:
            return []

        try:
            stubs = self._fetch_live_stubs(sport_id)
            if not stubs:
                return []
            return self._fetch_odds_parallel(stubs, sport)
        except Exception as ex:
            logger.error(f'[Bet9ja] live {sport} error: {ex}')
            return []

    # ── Fetching ──────────────────────────────────────────────────────────────

    def _seed_session(self):
        """Visit homepage to receive the ftv cookie required by the sportsbook API."""
        if 'ftv' in self.session.cookies:
            return
        r = self.session.get(
            'https://sports.bet9ja.com/',
            headers={'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'},
            timeout=15,
        )
        r.raise_for_status()
        if 'ftv' not in self.session.cookies:
            raise RuntimeError('Bet9ja seed failed: ftv cookie not received')

    def _get_pal(self) -> dict:
        """Fetch and cache the full PAL dict (all sports) for one refresh cycle."""
        if self._pal_cache and (time.time() - self._pal_fetched_at) < 55:
            return self._pal_cache
        self._seed_session()
        r = self.session.get(SPORTS_URL, timeout=20)
        if r.status_code == 403:
            # IP may be rate-limited; clear ftv cookie so next call re-seeds
            self.session.cookies.clear()
            self._pal_cache = None
        r.raise_for_status()
        self._pal_cache = r.json().get('D', {}).get('PAL', {})
        self._pal_fetched_at = time.time()
        return self._pal_cache

    def _fetch_stubs(self, sport_id: str) -> list:
        """Return list of {event_id, name, starts_at, league} filtered to HOURS_AHEAD."""
        pal = self._get_pal()
        sport_data = pal.get(sport_id, {})
        now    = datetime.utcnow()
        cutoff = now + timedelta(hours=config.HOURS_AHEAD)

        stubs = []
        for sg in sport_data.get('SG', {}).values():
            for group in sg.get('G', {}).values():
                league = group.get('N', '')
                for eid, ev in group.get('E', {}).items():
                    starts_at = _parse_dt(ev.get('D', ''))
                    if starts_at and now < starts_at <= cutoff:
                        stubs.append({
                            'event_id': eid,
                            'name':     ev.get('N', ''),
                            'starts_at': starts_at,
                            'league':   league,
                        })
        return stubs

    def _get_live_pal(self) -> dict:
        """Fetch PAL from DISP=0 (in-play events). Cached for 8 seconds."""
        if self._live_pal_cache and (time.time() - self._live_pal_fetched_at) < 8:
            return self._live_pal_cache
        self._seed_session()
        r = self.session.get(LIVE_SPORTS_URL, timeout=20)
        if r.status_code == 403:
            self.session.cookies.clear()
            self._live_pal_cache = None
        r.raise_for_status()
        self._live_pal_cache = r.json().get('D', {}).get('PAL', {})
        self._live_pal_fetched_at = time.time()
        return self._live_pal_cache

    def _fetch_live_stubs(self, sport_id: str) -> list:
        """Return stubs for currently in-play matches.

        Strategy: fetch DISP=0 (in-play endpoint). If that returns nothing,
        fall back to scanning the prematch PAL for recently-started events
        (started within the last 3 hours) in case the same endpoint serves both.
        """
        now          = datetime.utcnow()
        lookback     = now - timedelta(hours=3)

        def _stubs_from_pal(pal: dict) -> list:
            sport_data = pal.get(sport_id, {})
            stubs = []
            for sg in sport_data.get('SG', {}).values():
                for group in sg.get('G', {}).values():
                    league = group.get('N', '')
                    for eid, ev in group.get('E', {}).items():
                        starts_at = _parse_dt(ev.get('D', ''))
                        if starts_at and lookback <= starts_at <= now:
                            stubs.append({
                                'event_id':  eid,
                                'name':      ev.get('N', ''),
                                'starts_at': starts_at,
                                'league':    league,
                            })
            return stubs

        # Try dedicated live endpoint first
        try:
            live_pal = self._get_live_pal()
            stubs = _stubs_from_pal(live_pal)
            if stubs:
                logger.debug(f'[Bet9ja] live stubs from DISP=0: {len(stubs)}')
                return stubs
        except Exception as ex:
            logger.debug(f'[Bet9ja] DISP=0 failed: {ex}')

        # Fallback: check prematch PAL for recently-started events
        try:
            prematch_pal = self._get_pal()
            stubs = _stubs_from_pal(prematch_pal)
            if stubs:
                logger.debug(f'[Bet9ja] live stubs from prematch PAL fallback: {len(stubs)}')
            return stubs
        except Exception as ex:
            logger.debug(f'[Bet9ja] live fallback failed: {ex}')
            return []

    def _fetch_event_odds(self, stub: dict) -> Optional[dict]:
        """Fetch odds for a single event. Returns the raw D dict or None."""
        try:
            time.sleep(0.05)  # gentle throttle across workers
            r = self.session.get(EVENT_URL, params={'EVENTID': stub['event_id']}, timeout=10)
            r.raise_for_status()
            d = r.json().get('D', {})
            if not d:
                return None
            d['_stub'] = stub
            return d
        except Exception as ex:
            logger.debug(f'[Bet9ja] event {stub["event_id"]} failed: {ex}')
            return None

    def _fetch_odds_parallel(self, stubs: list, sport: str) -> List[Event]:
        events: List[Event] = []
        with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
            futures = {pool.submit(self._fetch_event_odds, s): s for s in stubs}
            for fut in as_completed(futures):
                raw = fut.result()
                if raw:
                    events.extend(self._parse(raw, sport))
        return events

    # ── Parser ────────────────────────────────────────────────────────────────

    def _parse(self, raw: dict, sport: str) -> List[Event]:
        stub = raw.get('_stub', {})
        name = raw.get('DS', stub.get('name', ''))
        parts = name.split(' - ', 1)
        if len(parts) != 2:
            return []
        home, away = parts[0].strip(), parts[1].strip()
        if not home or not away:
            return []

        starts_at = stub.get('starts_at') or _parse_dt(raw.get('STARTDATE', ''))
        league    = stub.get('league', '')
        event_id  = str(stub.get('event_id', raw.get('ID', '')))
        odds_dict = raw.get('O', {})
        if not isinstance(odds_dict, dict):
            return []

        event_url = f'https://sports.bet9ja.com/event/{event_id}' if event_id else None

        result: List[Event] = []
        for market_name, key_map in MARKETS.get(sport, []):
            outcomes = []
            for label, key in key_map.items():
                val = odds_dict.get(key)
                if val is None:
                    break
                try:
                    odds = float(val)
                except (TypeError, ValueError):
                    break
                if odds > 1.0:
                    outcomes.append(Outcome(name=label, odds=odds, bookmaker='Bet9ja', event_url=event_url))
            else:
                # All outcomes present
                if len(outcomes) == len(key_map):
                    result.append(Event(
                        event_id  = f'b9j_{event_id}_{market_name.replace("/","_").replace(" ","")}',
                        bookmaker = 'Bet9ja',
                        sport     = sport,
                        home_team = home,
                        away_team = away,
                        market    = market_name,
                        outcomes  = outcomes,
                        starts_at = starts_at,
                        league    = league,
                    ))

        # Asian Handicap — football only, dynamic key scan: S_AH@{line}_1 / S_AH@{line}_2
        if sport == 'football':
            ah_home: dict = {}  # line_float -> odds
            ah_away: dict = {}
            for key, val in odds_dict.items():
                m = re.match(r'^S_AH@([+-]?\d+(?:\.\d+)?)_(1|2)$', key)
                if not m:
                    continue
                try:
                    line_val = float(m.group(1))
                    odds     = float(val)
                except (TypeError, ValueError):
                    continue
                if odds <= 1.0:
                    continue
                if abs(line_val * 4) % 2 != 0:  # reject quarter-ball lines
                    continue
                if m.group(2) == '1':
                    ah_home[line_val] = odds
                else:
                    ah_away[line_val] = odds

            for home_p, home_cf in ah_home.items():
                # Bet9ja uses the same line value for both sides of the same market
                # (S_AH@-0.5_1 and S_AH@-0.5_2 are both the AH -0.5 market).
                # Do NOT negate: -home_p would pick the Away side of the opposite market.
                away_cf = ah_away.get(home_p)
                if away_cf is None:
                    away_cf = next((v for k, v in ah_away.items() if abs(k - home_p) < 1e-9), None)
                if away_cf is None:
                    continue
                line = f'+{home_p:g}' if home_p > 0 else f'{home_p:g}'
                result.append(Event(
                    event_id  = f'b9j_{event_id}_ah{line}',
                    bookmaker = 'Bet9ja',
                    sport     = sport,
                    home_team = home,
                    away_team = away,
                    market    = f'Asian Handicap {line}',
                    outcomes  = [
                        Outcome(name='Home', odds=home_cf, bookmaker='Bet9ja', event_url=event_url),
                        Outcome(name='Away', odds=away_cf, bookmaker='Bet9ja', event_url=event_url),
                    ],
                    starts_at = starts_at,
                    league    = league,
                ))

        return result


def _parse_dt(s: str) -> Optional[datetime]:
    if not s:
        return None
    try:
        return datetime.strptime(s, '%Y-%m-%d %H:%M:%S')
    except ValueError:
        return None
