"""
1xBet Nigeria scraper.

Endpoints discovered via Playwright network interception on 1xbet.ng/en/line/football.

League list:  GET /service-api/LineFeed/GetSportsShortZip?sports=1&lng=en&country=132&partner=159
Events:       GET /service-api/LineFeed/Get1x2_VZip?sports=1&champs=<LI>&count=500&lng=en&mode=4&country=132&partner=159&getEmpty=true&virtualSports=true

Football: fetches top MAX_LEAGUES leagues sorted by event count using a thread pool.
  Per-league requests require JS-set anti-bot cookies — seeded via Playwright on first run.
  Playwright visits /en/line/football, extracts cookies into the requests session, then all
  per-league calls go through requests concurrently (fast). Falls back to global endpoint
  (~50 events) if Playwright seed fails or all per-league calls still fail.
Basketball/Tennis: single call (league listing not available via this endpoint for these sports).

Response field mapping:
  O1 / O2  → home / away team name
  I        → event ID
  S        → start time (Unix seconds, NOT milliseconds)
  L        → league name
  E[]      → main odds array
    G=1,  T=1/2/3       → 1X2: Home / Draw / Away
    G=2,  T=7/8, P=None → Draw No Bet: Home / Away (no handicap line)
    G=2,  T=7/8, P=val  → Asian Handicap: T=7 home side (P=home handicap),
                           T=8 away side (P=away handicap); P(T7) == -P(T8)
    G=17, T=9/10        → Over/Under (P = line value, e.g. 2.5)
    G=101, T=401/402    → Basketball Winner: Team1 / Team2
"""
import re
import os
import json
import time
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from typing import List, Tuple

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

logger = logging.getLogger(__name__)

# Fetch top N leagues by event count. Concurrent requests bypass sequential rate limiting.
# Top 200 leagues gives ~1600 events; all 427 gives ~2022 events.
MAX_LEAGUES = 200

BASE_URL    = "https://1xbet.ng/service-api/LineFeed"
LEAGUES_URL = f"{BASE_URL}/GetSportsShortZip"
EVENTS_URL  = f"{BASE_URL}/Get1x2_VZip"

# 1xBet internal sport IDs (not Sportradar IDs)
SPORT_IDS = {
    'football':   '1',
    'basketball': '3',
    'tennis':     '4',
}

# Market group G → (market name, outcome map {T: label})
# Only the markets we care about for arb
MARKETS = {
    1:   ('1X2',            {1: 'Home', 2: 'Draw', 3: 'Away'}),
    5:   ('HT 1X2',         {1: 'Home', 2: 'Draw', 3: 'Away'}),  # football only
    8:   ('Double Chance',  {4: '1X', 5: '12', 6: 'X2'}),        # football only
    17:  ('Over/Under 2.5', {9: 'Over', 10: 'Under'}),            # P=2.5 filtered below
    101: ('Home/Away',      {401: 'Home', 402: 'Away'}),          # basketball
    19:  ('BTTS',           {1170: 'Yes', 1171: 'No'}),           # football only
}
# G=15 and G=62 exist in the main event but are unreliable: the global Get1x2_VZip
# endpoint mixes main events and sub-events, so G=15/62 map to different markets
# depending on the event type. Cross-verification against SportyBet confirms these
# odds don't correspond to HT/2H O/U — removed to prevent false arb signals.
# G=2 is handled separately: P=None → DNB, P≠None → AH (T=7=Home, T=8=Away)
AH_DNB_G = 2
AH_T_MAP  = {7: 'Home', 8: 'Away'}

# Correct Score — only available via GetGameZip (per-event detail endpoint).
# G=136: T=731 with P=home_goals + away_goals/1000 (P=None → 0:0),
#         T=3786 with P=None → "Any Other Score".
CS_G        = 136
_CS_SCORE_T = 731   # specific score; decode P → home-away
_CS_OTHER_T = 3786  # catch-all "Any Other Score"
CS_MAX_EVENTS = 150  # GetGameZip requests per cycle (≈15s at 10 threads)

# For tennis 1X2 market has no draw — only T=1 and T=3
TENNIS_1X2 = ('Home/Away', {1: 'Home', 3: 'Away'})

SPORT_SLUGS = {
    'football':   'football',
    'basketball': 'basketball',
    'tennis':     'tennis',
}

# Cookies are persisted here so a successful seed survives app restarts.
# Max age in seconds before we re-seed regardless of whether calls succeed.
_COOKIE_FILE    = os.path.join(os.path.dirname(__file__), '..', 'data', '1xbet_cookies.json')
_COOKIE_MAX_AGE = 4 * 3600   # 4 hours


def _slugify(text: str) -> str:
    text = text.lower()
    text = re.sub(r'[^a-z0-9 ]', '', text)
    return re.sub(r'\s+', '-', text.strip())


def _decode_cs_p(p) -> str:
    """Decode a LineFeed CS score from P encoding: home_goals + away_goals/1000.
    P=None represents 0-0 (stored as null because 0 is falsy in the API).
    """
    if p is None:
        return '0-0'
    home_g = int(p)
    away_g = round(p * 1000) - home_g * 1000
    return f'{home_g}-{away_g}'


class OneXBetScraper(BaseScraper):

    def __init__(self):
        super().__init__('1xBet')
        self.session.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
            'Accept': 'application/json, text/plain, */*',
            'Referer': 'https://1xbet.ng/en/line/football',
            'Origin': 'https://1xbet.ng',
        }
        self._session_seeded = False
        # Load persisted cookies on startup so we don't need a browser on every restart
        self._load_cached_cookies()

    # ── Cookie persistence ────────────────────────────────────────────────────

    def _load_cached_cookies(self) -> bool:
        """Load cookies from disk if they are still fresh. Returns True if loaded."""
        try:
            with open(_COOKIE_FILE) as f:
                data = json.load(f)
            age = time.time() - data.get('saved_at', 0)
            if age > _COOKIE_MAX_AGE:
                logger.info('[1xBet] cached cookies too old (%.0fh), will re-seed', age / 3600)
                return False
            for c in data.get('cookies', []):
                self.session.cookies.set(c['name'], c['value'],
                                         domain=c.get('domain', '1xbet.ng'))
            self._session_seeded = True
            logger.info('[1xBet] loaded %d cached cookies (%.0fm old)',
                        len(data['cookies']), age / 60)
            return True
        except FileNotFoundError:
            return False
        except Exception as ex:
            logger.warning('[1xBet] cookie cache load failed: %s', ex)
            return False

    def _save_cached_cookies(self, playwright_cookies: list) -> None:
        """Persist Playwright cookies to disk for reuse across restarts."""
        try:
            os.makedirs(os.path.dirname(_COOKIE_FILE), exist_ok=True)
            with open(_COOKIE_FILE, 'w') as f:
                json.dump({'saved_at': time.time(), 'cookies': playwright_cookies}, f)
        except Exception as ex:
            logger.warning('[1xBet] cookie cache save failed: %s', ex)

    # ── Session seeding ───────────────────────────────────────────────────────

    def _seed_session(self) -> bool:
        """Launch a stealthy Playwright browser, visit the football page, extract
        the JS-set anti-bot cookies into the requests session, and persist them to disk."""
        try:
            from playwright.sync_api import sync_playwright
            with sync_playwright() as pw:
                browser = pw.chromium.launch(
                    headless=True,
                    args=[
                        '--no-sandbox',
                        '--disable-blink-features=AutomationControlled',
                        '--disable-gpu',
                        '--disable-dev-shm-usage',
                    ],
                )
                ctx = browser.new_context(
                    locale='en-US',
                    user_agent=(
                        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                        'AppleWebKit/537.36 (KHTML, like Gecko) '
                        'Chrome/124.0.0.0 Safari/537.36'
                    ),
                    viewport={'width': 1920, 'height': 1080},
                )
                ctx.add_init_script(
                    "Object.defineProperty(navigator, 'webdriver', {get: () => undefined});"
                    "window.chrome = {runtime: {}};"
                )
                page = ctx.new_page()
                # Apply playwright-stealth if installed (masks more fingerprints)
                try:
                    from playwright_stealth import stealth_sync
                    stealth_sync(page)
                except ImportError:
                    pass
                page.goto('https://1xbet.ng/en/line/football',
                          wait_until='domcontentloaded', timeout=30_000)
                time.sleep(5)   # let JS finish setting anti-bot cookies
                cookies = ctx.cookies()
                browser.close()

            for c in cookies:
                self.session.cookies.set(c['name'], c['value'],
                                         domain=c.get('domain', '1xbet.ng'))
            self._save_cached_cookies(cookies)
            self._session_seeded = True
            logger.info('[1xBet] session seeded via Playwright (%d cookies)', len(cookies))
            return True
        except Exception as ex:
            logger.warning('[1xBet] Playwright seed failed: %s', ex)
            self._session_seeded = False
            return False

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

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

        if sport == 'football':
            return self._get_football_events()
        else:
            return self._get_events_single_call(sport, sport_id)

    # ── Football: per-league pagination ───────────────────────────────────────

    def _get_football_events(self) -> List[Event]:
        if not self._session_seeded:
            self._seed_session()

        try:
            leagues = self._fetch_leagues('1')
        except Exception as ex:
            logger.error(f"[1xBet] league list failed: {ex}")
            return []

        failures = []
        raw_pool: List[dict] = []  # collect raw events for the CS GetGameZip pass

        def _fetch_league(league_id: int, league_name: str) -> List[Event]:
            try:
                raw_events = self._fetch_events('1', champs=str(league_id))
                raw_pool.extend(raw_events)   # list.extend is GIL-safe
                result = []
                for raw in raw_events:
                    result.extend(self._parse(raw, 'football', league_id=league_id, league_name=league_name))
                return result
            except Exception as ex:
                failures.append(str(ex))
                return []

        events: List[Event] = []
        with ThreadPoolExecutor(max_workers=10) as pool:
            futures = {pool.submit(_fetch_league, lid, lname): (lid, lname) for lid, lname in leagues}
            for future in as_completed(futures):
                events.extend(future.result())

        if failures:
            blocked = sum(1 for f in failures if '406' in f)
            other   = len(failures) - blocked
            parts   = []
            if blocked:
                parts.append(f"{blocked} blocked (406)")
            if other:
                parts.append(f"{other} other errors")
            logger.warning(f"[1xBet] {len(leagues) - len(failures)}/{len(leagues)} leagues ok — {', '.join(parts)}")
            if blocked > len(leagues) / 2:
                self._session_seeded = False
                logger.warning('[1xBet] >50%% leagues blocked — will re-seed next cycle')

        if not events and leagues:
            self._session_seeded = False
            logger.warning("[1xBet] no per-league events, falling back to global endpoint")
            try:
                raw_events = self._fetch_events('1')
                raw_pool.extend(raw_events)
                for raw in raw_events:
                    events.extend(self._parse(raw, 'football'))
            except Exception as ex:
                logger.error(f"[1xBet] fallback also failed: {ex}")

        # Phase 2: Correct Score via GetGameZip (not in Get1x2_VZip)
        events.extend(self._fetch_football_cs(raw_pool))

        return events

    def _fetch_football_cs(self, raw_pool: List[dict]) -> List[Event]:
        """Fetch Correct Score markets via concurrent GetGameZip calls."""
        # Deduplicate by event ID, cap at CS_MAX_EVENTS
        seen: set = set()
        unique_raws: List[dict] = []
        for raw in raw_pool:
            eid = str(raw.get('I', ''))
            if eid and eid not in seen:
                seen.add(eid)
                unique_raws.append(raw)
                if len(unique_raws) >= CS_MAX_EVENTS:
                    break

        if not unique_raws:
            return []

        cs_events: List[Event] = []

        def _fetch_one_cs(raw: dict) -> List[Event]:
            eid = str(raw.get('I', ''))
            try:
                r = self.session.get(
                    f'{BASE_URL}/GetGameZip',
                    params={'id': eid, 'lng': 'en', 'isStatic': 'true',
                            'groupEvents': 'true', 'country': '132', 'partner': '159'},
                    timeout=15,
                )
                r.raise_for_status()
                return self._parse_cs(r.json().get('Value', {}), raw)
            except Exception as ex:
                logger.debug(f'[1xBet] GetGameZip {eid} CS: {ex}')
                return []

        with ThreadPoolExecutor(max_workers=10) as pool:
            for result in pool.map(_fetch_one_cs, unique_raws):
                cs_events.extend(result)

        logger.info(f'[1xBet] CS: {len(cs_events)} events from {len(unique_raws)} GetGameZip calls')
        return cs_events

    def _parse_cs(self, val: dict, raw: dict) -> List[Event]:
        """Parse Correct Score market from a GetGameZip response."""
        home = (val.get('O1') or raw.get('O1') or '').strip()
        away = (val.get('O2') or raw.get('O2') or '').strip()
        if not home or not away:
            return []

        eid    = str(val.get('I') or raw.get('I') or '')
        li     = val.get('LI') or raw.get('LI')
        league = (val.get('L') or raw.get('L') or '').strip()
        ts     = val.get('S') or raw.get('S')
        try:
            starts_at = datetime.utcfromtimestamp(ts) if ts else None
        except Exception:
            starts_at = None

        if li and eid:
            lg_slug = _slugify(league)
            ev_slug = _slugify(f'{home} {away}')
            event_url = f'https://1xbet.ng/en/line/football/{li}-{lg_slug}/{eid}-{ev_slug}'
        else:
            event_url = 'https://1xbet.ng/en/line/football'

        outcomes: List[Outcome] = []
        for e in val.get('E', []):
            if e.get('G') != CS_G or e.get('CE') == 1:
                continue
            t = e.get('T')
            p = e.get('P')
            try:
                odds = float(e.get('C', 0))
            except (TypeError, ValueError):
                continue
            if odds <= 1.0:
                continue
            if t == _CS_SCORE_T:
                name = _decode_cs_p(p)
            elif t == _CS_OTHER_T and p is None:
                name = 'Any Other Score'
            else:
                continue
            outcomes.append(Outcome(name=name, odds=odds, bookmaker='1xBet', event_url=event_url))

        if len(outcomes) < 2:
            return []

        return [Event(
            event_id  = f'1x_{eid}_cs',
            bookmaker = '1xBet',
            sport     = 'football',
            home_team = home,
            away_team = away,
            market    = 'Correct Score',
            outcomes  = outcomes,
            starts_at = starts_at,
            league    = league,
        )]

    def _fetch_leagues(self, sport_id: str) -> List[Tuple[int, str]]:
        r = self.session.get(
            LEAGUES_URL,
            params={
                'sports':        sport_id,
                'lng':           'en',
                'country':       '132',
                'partner':       '159',
                'virtualSports': 'true',
                'gr':            '412',
            },
            timeout=15,
        )
        r.raise_for_status()
        sport_data = r.json().get('Value', [])
        if not sport_data:
            return []
        leagues = sport_data[0].get('L', [])
        # Sort by event count descending, take top MAX_LEAGUES to avoid rate limiting
        leagues_sorted = sorted(leagues, key=lambda x: x.get('GC', 0), reverse=True)
        return [(lg['LI'], lg.get('L', '')) for lg in leagues_sorted[:MAX_LEAGUES] if 'LI' in lg]

    # ── Basketball / Tennis: single call ──────────────────────────────────────

    def _get_events_single_call(self, sport: str, sport_id: str) -> List[Event]:
        events: List[Event] = []
        try:
            raw_events = self._fetch_events(sport_id)
            for raw in raw_events:
                events.extend(self._parse(raw, sport))
        except Exception as ex:
            logger.error(f"[1xBet] {sport} fetch error: {ex}")
        return events

    # ── Shared fetch ──────────────────────────────────────────────────────────

    def get_live_events(self, sport: str) -> List[Event]:
        """Fetch in-play events via the LiveFeed endpoint (separate from prematch LineFeed)."""
        sport_id = SPORT_IDS.get(sport)
        if not sport_id:
            return []
        events: List[Event] = []
        try:
            raw_events = self._fetch_live_events(sport_id)
            for raw in raw_events:
                events.extend(self._parse(raw, sport, live=True))
        except Exception as ex:
            logger.warning(f'[1xBet] live {sport} fetch error: {ex}')
        return events

    def _fetch_live_events(self, sport_id: str) -> list:
        r = self.session.get(
            BASE_URL.replace('LineFeed', 'LiveFeed') + '/Get1x2_VZip',
            params={
                'sports':  sport_id,
                'count':   '500',
                'lng':     'en',
                'mode':    '1',
                'country': '132',
                'partner': '159',
            },
            timeout=15,
        )
        r.raise_for_status()
        value = r.json().get('Value', [])
        return value if isinstance(value, list) else []

    def _fetch_events(self, sport_id: str, champs: str = None) -> list:
        params = {
            'sports':        sport_id,
            'count':         '500',
            'lng':           'en',
            'mode':          '4',
            'country':       '132',
            'partner':       '159',
            'getEmpty':      'true',
            'virtualSports': 'true',
        }
        if champs:
            params['champs'] = champs

        r = self.session.get(EVENTS_URL, params=params, timeout=15)
        r.raise_for_status()
        value = r.json().get('Value', [])
        return value if isinstance(value, list) else []

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

    def _parse(self, raw: dict, sport: str, league_id: int = None, league_name: str = None, live: bool = False) -> List[Event]:
        home = (raw.get('O1') or '').strip()
        away = (raw.get('O2') or '').strip()
        if not home or not away:
            return []

        ts = raw.get('S')  # Unix seconds (not ms)
        try:
            starts_at = datetime.utcfromtimestamp(ts) if ts else None
        except Exception:
            starts_at = None

        event_id_base = str(raw.get('I', ''))
        league = league_name or raw.get('L', '') or raw.get('LE', '')

        # Build direct event URL using fields always present in the raw event dict.
        # LI = league ID, I = event ID (both appear in /line/ prematch URLs).
        li         = raw.get('LI')
        event_i    = raw.get('I')
        sport_slug = SPORT_SLUGS.get(sport, sport)
        if li and event_i:
            lg_slug = _slugify(league)
            ev_slug = _slugify(f'{home} {away}')
            if live:
                event_url: str = f'https://1xbet.ng/en/live/{sport_slug}/{event_i}-{ev_slug}'
            else:
                event_url = (
                    f'https://1xbet.ng/en/line/{sport_slug}'
                    f'/{li}-{lg_slug}/{event_i}-{ev_slug}'
                )
        else:
            event_url = f'https://1xbet.ng/en/{"live" if live else "line"}/{sport_slug}'

        # Group E[] entries by market G
        # CE=1 means the market entry is suspended/closed — skip in live mode
        markets_data: dict = {}
        for entry in raw.get('E', []):
            if live and entry.get('CE', 0) == 1:
                continue
            g = entry.get('G')
            if g not in MARKETS and not (g == AH_DNB_G and sport == 'football') and not (sport == 'tennis' and g == 1):
                continue
            markets_data.setdefault(g, []).append(entry)

        result: List[Event] = []

        # G=2: Draw No Bet (P=None) and Asian Handicap (P≠None) — football only
        if AH_DNB_G in markets_data and sport == 'football':
            g2_entries = markets_data.pop(AH_DNB_G)
            dnb_entries = [e for e in g2_entries if e.get('P') is None]
            ah_entries  = [e for e in g2_entries if e.get('P') is not None]

            # Draw No Bet
            dnb_outcomes = []
            for e in dnb_entries:
                label = AH_T_MAP.get(e.get('T'))
                if label is None:
                    continue
                try:
                    odds = float(e.get('C', 0))
                except (TypeError, ValueError):
                    continue
                if odds > 1.0:
                    dnb_outcomes.append(Outcome(name=label, odds=odds, bookmaker='1xBet', event_url=event_url))
            if len(dnb_outcomes) == 2:
                result.append(Event(
                    event_id  = f'1x_{event_id_base}_dnb',
                    bookmaker = '1xBet',
                    sport     = sport,
                    home_team = home,
                    away_team = away,
                    market    = 'Draw No Bet',
                    outcomes  = dnb_outcomes,
                    starts_at = starts_at,
                    league    = league,
                ))

            # Asian Handicap — group by home P value, pair with away at -P
            home_odds: dict = {}   # P_float -> odds
            away_odds: dict = {}
            for e in ah_entries:
                label = AH_T_MAP.get(e.get('T'))
                p = e.get('P')
                if label is None or p is None:
                    continue
                try:
                    p_val = float(p)
                    odds  = float(e.get('C', 0))
                except (TypeError, ValueError):
                    continue
                if odds <= 1.0:
                    continue
                if label == 'Home':
                    home_odds[p_val] = odds
                else:
                    away_odds[p_val] = odds

            for home_p, home_cf in home_odds.items():
                if abs(home_p * 4) % 2 != 0:   # reject quarter-ball
                    continue
                away_p = -home_p
                away_cf = away_odds.get(away_p)
                if away_cf is None:
                    away_cf = next((v for k, v in away_odds.items() if abs(k - away_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'1x_{event_id_base}_ah{line}',
                    bookmaker = '1xBet',
                    sport     = sport,
                    home_team = home,
                    away_team = away,
                    market    = f'Asian Handicap {line}',
                    outcomes  = [
                        Outcome(name='Home', odds=home_cf, bookmaker='1xBet', event_url=event_url),
                        Outcome(name='Away', odds=away_cf, bookmaker='1xBet', event_url=event_url),
                    ],
                    starts_at = starts_at,
                    league    = league,
                ))

        for g, entries in markets_data.items():
            # G=5 (HT 1X2), G=8 (DC), G=19 (BTTS) are football-only
            if g in (5, 8, 19) and sport != 'football':
                continue
            if sport == 'tennis' and g == 1:
                market_name, outcome_map = TENNIS_1X2
            else:
                market_name, outcome_map = MARKETS[g]

            # For Over/Under (G=17), emit a separate Event per line
            if g == 17:
                lines = (0.5, 1.5, 2.5, 3.5, 4.5)
                mkt_prefix = 'Over/Under'
                for lv in lines:
                    lv_entries = [e for e in entries if e.get('P') == lv]
                    ou_outcomes = []
                    for e in lv_entries:
                        label = outcome_map.get(e.get('T'))
                        if label is None:
                            continue
                        try:
                            odds = float(e.get('C', 0))
                        except (TypeError, ValueError):
                            continue
                        if odds > 1.0:
                            ou_outcomes.append(Outcome(name=label, odds=odds, bookmaker='1xBet', event_url=event_url))
                    if len(ou_outcomes) == len(outcome_map):
                        result.append(Event(
                            event_id=f"1x_{event_id_base}_{g}_{lv}",
                            bookmaker='1xBet',
                            sport=sport,
                            home_team=home,
                            away_team=away,
                            market=f'{mkt_prefix} {lv}',
                            outcomes=ou_outcomes,
                            starts_at=starts_at,
                            league=league,
                        ))
                continue

            outcomes = []
            for e in entries:
                t = e.get('T')
                label = outcome_map.get(t)
                if label is None:
                    continue
                try:
                    odds = float(e.get('C', 0))
                except (TypeError, ValueError):
                    continue
                if odds > 1.0:
                    outcomes.append(Outcome(name=label, odds=odds, bookmaker='1xBet', event_url=event_url))

            # Need exactly the right number of outcomes for the market
            expected = len(outcome_map)
            if len(outcomes) == expected:
                result.append(Event(
                    event_id=f"1x_{event_id_base}_{g}",
                    bookmaker='1xBet',
                    sport=sport,
                    home_team=home,
                    away_team=away,
                    market=market_name,
                    outcomes=outcomes,
                    starts_at=starts_at,
                    league=league,
                ))

        return result
