"""
Betjara Nigeria scraper.

Platform: Centrivo (cvio-digi.com / sportdigi.com) white-label sportsbook,
running as a cross-origin iframe at sport.betjara.ng.

Architecture:
  betjara.ng loads a React wrapper page.  The actual sportsbook UI is served
  from sport.betjara.ng inside an <iframe>.  Cloudflare protects BOTH origins.

  Strategy (same pattern as Betpawa):
    1. A singleton _BetjaraWorker daemon thread owns one Chrome browser session.
    2. On init: load betjara.ng homepage (gets CF clearance for betjara.ng),
       then click the SPORTS nav link — this is a client-side React Router
       navigation that does NOT trigger a full page reload, so Cloudflare does
       NOT re-challenge.  Once on /en/sport/ the iframe from sport.betjara.ng
       loads in the same CF session.
    3. Each scrape call uses iframe_frame.evaluate() to call the sportsbook's
       built-in $httpApi JavaScript object directly inside the iframe context,
       which inherits the CF cookies.

API (discovered by inspecting $httpApi.paths inside the iframe):
  $httpApi.getTree(sportIds, stakeTypeIds, startDate, endDate, langId, partnerId, countryCode)
    POST Prematch/GetTree — returns the full prematch event tree with odds.

  $httpApi.getPrematchEventsListWithStakeTypes(champId, timeFilter, stakeIds, ...)
    GET Prematch/GetEventsListWithStakeTypes — per-championship event list.

  $httpApi.championships(null, null, 0, sportId, ...)
    GET Prematch/Championships — list of championships for a sport.

Platform sport IDs (Betjara internal):
  Football=1, Basketball=4, Tennis=3, Virtual=118

Stake type IDs (football):
  1  = Result (1X2)   outcomes: Win1/X/Win2
  3  = Total (O/U)    outcomes: Over/Under, line in stake.A field
  26 = Both Teams to Score    Yes/No
  37 = Double Chance          1X/12/X2
  2  = Handicap (AH)  outcomes: Handicap 1/Handicap 2, line in stake.A
"""

import logging
import queue
import re
import threading
import time
from datetime import datetime
from typing import List, Optional, Dict

import psutil

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

logger = logging.getLogger(__name__)

BASE_URL = 'https://betjara.ng'
BOOKMAKER = 'Betjara'

# Platform sport IDs
SPORT_IDS = {
    'football':   1,
    'basketball': 4,
    'tennis':     3,
}

# stakeTypeId → (market_label_prefix, outcome_name_map)
# outcome_name_map: betjara outcome name → our canonical name
FOOTBALL_STAKE_TYPES = [
    (1,  '1X2',           {'Win1': 'Home', 'X': 'Draw', 'Win2': 'Away'}, None),
    (3,  'Total',         {'Over': 'Over', 'Under': 'Under'},            [1.5, 2.5, 3.5, 4.5]),
    (26, 'BTTS',          {'Yes': 'Yes', 'No': 'No'},                    None),
    (37, 'Double Chance', {'1X': '1X', '12': '12', 'X2': 'X2'},         None),
    (2,  'AH',            {'Handicap 1': 'Home', 'Handicap 2': 'Away'}, '__ah__'),
]

# Basketball: moneyline is also "Result" (stakeTypeId=1)
BASKETBALL_STAKE_TYPES = [
    (1,  'Home/Away', {'Win1': 'Home', 'Win2': 'Away'}, None),
    (3,  'Total',     {'Over': 'Over', 'Under': 'Under'}, '__ou__'),
]

TENNIS_STAKE_TYPES = [
    (1,  'Home/Away', {'Win1': 'Home', 'Win2': 'Away'}, None),
]

SPORT_STAKE_TYPES = {
    'football':   FOOTBALL_STAKE_TYPES,
    'basketball': BASKETBALL_STAKE_TYPES,
    'tennis':     TENNIS_STAKE_TYPES,
}

# Live markets differ from prematch: in-play only exposes result + total lines
LIVE_FOOTBALL_STAKE_TYPES = [
    (1,  '1X2',       {'Win1': 'Home', 'X': 'Draw', 'Win2': 'Away'}, None),
    (3,  'Total',     {'Over': 'Over', 'Under': 'Under'},            [1.5, 2.5, 3.5, 4.5]),
    (26, 'BTTS',      {'Yes': 'Yes', 'No': 'No'},                    None),
]

LIVE_BASKETBALL_STAKE_TYPES = [
    (1,  'Home/Away', {'Win1': 'Home', 'Win2': 'Away'}, None),
    (3,  'Total',     {'Over': 'Over', 'Under': 'Under'}, '__ou__'),
]

# Live tennis: stakeTypeId=1 (match winner) when available, stakeTypeId=3 for Total Games O/U
LIVE_TENNIS_STAKE_TYPES = [
    (1,  'Home/Away',    {'Win1': 'Home', 'Win2': 'Away'}, None),
    (3,  'Total Games',  {'Over': 'Over', 'Under': 'Under'}, '__ou__'),
]

LIVE_SPORT_STAKE_TYPES = {
    'football':   LIVE_FOOTBALL_STAKE_TYPES,
    'basketball': LIVE_BASKETBALL_STAKE_TYPES,
    'tennis':     LIVE_TENNIS_STAKE_TYPES,
}

# JS that runs inside the iframe context to fetch and transform prematch events
_FETCH_JS = """
async ({sportId, stakeTypeIds}) => {
    try {
        const r = await $httpApi.getTree([sportId], stakeTypeIds, null, null, 2, 3000136, 'NG');
        const events = [];
        for (const sport of (r || [])) {
            if (sport.Id !== sportId) continue;
            for (const champ of (sport.CSH || [])) {
                for (const ev of (champ.E || [])) {
                    if (!ev.IsA) continue;
                    const now = Date.now();
                    // Skip events that have already started
                    if (ev.TS && ev.TS < now) continue;
                    const ev_data = {
                        id: ev.Id,
                        cid: ev.CId,
                        rid: ev.RId,
                        sid: ev.SId,
                        home: ev.HT,
                        away: ev.AT,
                        date: ev.D,
                        league: ev.CN
                    };
                    const markets = [];
                    for (const st of (ev.StakeTypes || [])) {
                        const stakes = [];
                        for (const s of (st.Stakes || [])) {
                            if (s.SS) continue;
                            stakes.push({n: s.N, f: s.F, a: s.A});
                        }
                        if (stakes.length > 0) {
                            markets.push({id: st.Id, name: st.N, stakes});
                        }
                    }
                    if (markets.length > 0) {
                        ev_data.markets = markets;
                        events.push(ev_data);
                    }
                }
            }
        }
        return {ok: true, events};
    } catch(e) {
        return {ok: false, error: e.message};
    }
}
"""

# Same structure but uses getLiveTree — no IsA or time filters
_LIVE_FETCH_JS = """
async ({sportId, stakeTypeIds}) => {
    try {
        const r = await $httpApi.getLiveTree([sportId], stakeTypeIds, null, null, 2, 3000136, 'NG');
        const events = [];
        for (const sport of (r || [])) {
            if (sport.Id !== sportId) continue;
            for (const champ of (sport.CSH || [])) {
                for (const ev of (champ.E || [])) {
                    const ev_data = {
                        id: ev.Id,
                        cid: ev.CId,
                        rid: ev.RId,
                        sid: ev.SId,
                        home: ev.HT,
                        away: ev.AT,
                        date: ev.D,
                        league: ev.CN
                    };
                    const markets = [];
                    for (const st of (ev.StakeTypes || [])) {
                        const stakes = [];
                        for (const s of (st.Stakes || [])) {
                            if (s.SS) continue;
                            stakes.push({n: s.N, f: s.F, a: s.A});
                        }
                        if (stakes.length > 0) {
                            markets.push({id: st.Id, name: st.N, stakes});
                        }
                    }
                    if (markets.length > 0) {
                        ev_data.markets = markets;
                        events.push(ev_data);
                    }
                }
            }
        }
        return {ok: true, events};
    } catch(e) {
        return {ok: false, error: e.message};
    }
}
"""


class _BetjaraWorker(threading.Thread):
    """
    Singleton daemon thread that owns the Playwright browser session for Betjara.
    Keeps CF clearance alive by staying on the sport section page.
    """

    def __init__(self):
        super().__init__(daemon=True, name='betjara-playwright')
        self._q: queue.Queue = queue.Queue()
        self._ready = threading.Event()
        self._failed: Optional[Exception] = None
        self._pw = None
        self.start()
        if not self._ready.wait(timeout=90):
            raise RuntimeError('[Betjara] browser worker timed out during init')
        if self._failed:
            raise self._failed

    def run(self):
        try:
            from playwright.sync_api import sync_playwright
            import shutil

            chrome_bin = (
                shutil.which('google-chrome') or
                shutil.which('chromium') or
                shutil.which('chromium-browser')
            )

            pw = sync_playwright().start()
            self._pw = pw
            browser = pw.chromium.launch(
                executable_path=chrome_bin,
                headless=True,
                args=[
                    '--disable-blink-features=AutomationControlled',
                    '--no-sandbox',
                    '--disable-gpu',
                    '--disable-dev-shm-usage',
                    '--disable-extensions',
                    '--window-size=1280,800',
                ],
            )
            ctx = browser.new_context(
                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': 1280, 'height': 800},
                locale='en-US',
            )
            ctx.add_init_script(
                "Object.defineProperty(navigator, 'webdriver', {get: () => undefined});"
                "window.chrome = {runtime: {}};"
            )

            page = ctx.new_page()

            # Step 1: load homepage → CF clearance
            logger.info('[Betjara] loading homepage for CF clearance...')
            try:
                page.goto(BASE_URL, timeout=60000, wait_until='domcontentloaded')
                time.sleep(8)
            except Exception as ex:
                logger.warning(f'[Betjara] homepage load warning: {ex}')

            title = page.title()
            if 'betjara' not in title.lower():
                raise RuntimeError(f'[Betjara] unexpected homepage title: {title!r}')

            # Step 2: click SPORTS nav (SPA navigation, no CF re-check)
            try:
                page.locator('a:has-text("SPORTS")').first.click(timeout=10000)
                time.sleep(10)
            except Exception as ex:
                raise RuntimeError(f'[Betjara] could not click SPORTS: {ex}')

            # Step 3: find iframe
            iframe_el = (
                page.query_selector('#sport_div_iframe iframe') or
                page.query_selector('iframe[src*="sport.betjara"]')
            )
            if not iframe_el:
                raise RuntimeError('[Betjara] sport iframe not found')

            sport_frame = iframe_el.content_frame()
            if not sport_frame:
                raise RuntimeError('[Betjara] content_frame() returned None')

            logger.info('[Betjara] browser worker ready (sport frame accessible)')

        except Exception as ex:
            self._failed = ex
            self._ready.set()
            return

        self._page = page
        self._sport_frame = sport_frame
        self._ready.set()

        while True:
            item = self._q.get()
            if item is None:
                break
            fn, result_q = item
            try:
                result_q.put(('ok', fn(self._sport_frame)))
            except Exception as ex:
                result_q.put(('err', ex))

        try:
            browser.close()
        except Exception:
            pass
        try:
            pw.stop()
        except Exception:
            pass

    def _kill_processes(self):
        pw, self._pw = self._pw, None
        if pw is None:
            return
        try:
            node_pid = pw._impl_obj._connection._transport._proc.pid
            proc = psutil.Process(node_pid)
            for child in proc.children(recursive=True):
                try:
                    child.kill()
                except psutil.NoSuchProcess:
                    pass
            proc.kill()
        except Exception:
            pass

    def stop(self):
        self._kill_processes()
        while True:
            try:
                self._q.get_nowait()
            except queue.Empty:
                break
        self._q.put(None)

    def call(self, fn, timeout: float = 90):
        result_q: queue.Queue = queue.Queue()
        self._q.put((fn, result_q))
        try:
            status, val = result_q.get(timeout=timeout)
        except queue.Empty:
            raise TimeoutError('[Betjara] worker call timed out')
        if status == 'err':
            raise val
        return val


class BetjaraScraper(BaseScraper):

    _worker: Optional[_BetjaraWorker] = None
    _worker_lock = threading.Lock()

    def __init__(self):
        super().__init__(BOOKMAKER)

    # ── Lifecycle ─────────────────────────────────────────────────────────────

    def _ensure_worker(self) -> bool:
        with self.__class__._worker_lock:
            if self.__class__._worker is not None and self.__class__._worker.is_alive():
                return True
            try:
                self.__class__._worker = _BetjaraWorker()
                return True
            except Exception as ex:
                logger.error(f'[Betjara] browser init failed: {ex}')
                self.__class__._worker = None
                return False

    def _reset_worker(self):
        with self.__class__._worker_lock:
            if self.__class__._worker:
                try:
                    self.__class__._worker.stop()
                except Exception:
                    pass
                self.__class__._worker = None

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

    def get_all_events(self) -> List[Event]:
        """Fetch all sports, keeping the browser alive between refresh cycles."""
        if not self._ensure_worker():
            return []
        result: List[Event] = []
        for sport in ['football', 'basketball', 'tennis']:
            sport_id = SPORT_IDS.get(sport)
            if not sport_id:
                continue
            try:
                worker = self.__class__._worker
                events = worker.call(
                    lambda frame, s=sport, sid=sport_id: self._fetch_sport(frame, s, sid),
                    timeout=120,
                )
                result.extend(events)
                logger.info(f'[Betjara] {sport}: {len(events)} events')
            except Exception as ex:
                logger.error(f'[Betjara] {sport} error: {ex}')
                self._reset_worker()
                return result
        return result

    def get_events(self, sport: str) -> List[Event]:
        sport_id = SPORT_IDS.get(sport)
        if not sport_id:
            return []
        if not self._ensure_worker():
            return []
        try:
            worker = self.__class__._worker
            return worker.call(
                lambda frame: self._fetch_sport(frame, sport, sport_id),
                timeout=120,
            )
        except Exception as ex:
            logger.error(f'[Betjara] {sport} error: {ex}')
            self._reset_worker()
            return []

    def get_all_live_events(self) -> List[Event]:
        """Fetch all live sports in one browser session (keeps browser alive for next call)."""
        if not self._ensure_worker():
            return []
        result: List[Event] = []
        for sport in ['football', 'basketball', 'tennis']:
            sport_id = SPORT_IDS.get(sport)
            if not sport_id:
                continue
            try:
                worker = self.__class__._worker
                events = worker.call(
                    lambda frame, s=sport, sid=sport_id: self._fetch_live_sport(frame, s, sid),
                    timeout=60,
                )
                result.extend(events)
                if events:
                    logger.info(f'[Betjara] live {sport}: {len(events)} events')
            except Exception as ex:
                logger.error(f'[Betjara] live {sport} error: {ex}')
                self._reset_worker()
                return result
        return result

    def get_live_events(self, sport: str) -> List[Event]:
        sport_id = SPORT_IDS.get(sport)
        if not sport_id:
            return []
        if not self._ensure_worker():
            return []
        try:
            worker = self.__class__._worker
            return worker.call(
                lambda frame: self._fetch_live_sport(frame, sport, sport_id),
                timeout=60,
            )
        except Exception as ex:
            logger.error(f'[Betjara] live {sport} error: {ex}')
            self._reset_worker()
            return []

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

    def _fetch_sport(self, frame, sport: str, sport_id: int) -> List[Event]:
        stake_types = SPORT_STAKE_TYPES.get(sport, [])
        stake_ids = list(dict.fromkeys(st_id for st_id, _, _, _ in stake_types))

        result = frame.evaluate(_FETCH_JS, {'sportId': sport_id, 'stakeTypeIds': stake_ids})
        if not result.get('ok'):
            logger.error(f'[Betjara] fetch failed: {result.get("error")}')
            return []

        raw_events = result.get('events', [])
        logger.info(f'[Betjara] {sport}: {len(raw_events)} raw events from getTree')

        events: List[Event] = []
        for raw in raw_events:
            parsed = self._parse_event(raw, sport, stake_types)
            events.extend(parsed)
        return events

    def _fetch_live_sport(self, frame, sport: str, sport_id: int) -> List[Event]:
        stake_types = LIVE_SPORT_STAKE_TYPES.get(sport, [])
        stake_ids = list(dict.fromkeys(st_id for st_id, _, _, _ in stake_types))

        result = frame.evaluate(_LIVE_FETCH_JS, {'sportId': sport_id, 'stakeTypeIds': stake_ids})
        if not result.get('ok'):
            logger.error(f'[Betjara] live fetch failed: {result.get("error")}')
            return []

        raw_events = result.get('events', [])
        logger.debug(f'[Betjara] live {sport}: {len(raw_events)} raw events from getLiveTree')

        events: List[Event] = []
        for raw in raw_events:
            parsed = self._parse_event(raw, sport, stake_types, live=True)
            events.extend(parsed)
        return events

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

    def _parse_event(self, raw: dict, sport: str, stake_types: list, live: bool = False) -> List[Event]:
        home = (raw.get('home') or '').strip()
        away = (raw.get('away') or '').strip()
        if not home or not away:
            return []

        league    = raw.get('league', '')
        event_id  = str(raw.get('id', ''))
        id_prefix = 'bj_live' if live else 'bj'
        event_url = _build_event_url(event_id, raw.get('cid'), raw.get('rid'), raw.get('sid'), home, away)
        starts_at = _parse_date(raw.get('date'))

        # Build lookup: stakeTypeId → [stakes]
        mkt_lookup: Dict[int, List[dict]] = {}
        for mkt in raw.get('markets', []):
            mkt_id = mkt.get('id')
            if mkt_id is not None:
                mkt_lookup.setdefault(mkt_id, []).extend(mkt.get('stakes', []))

        result: List[Event] = []
        for st_id, market_label, outcome_map, line_filter in stake_types:
            raw_stakes = mkt_lookup.get(st_id, [])
            if not raw_stakes:
                continue

            # Asian Handicap: group by line, emit one Event per line
            if line_filter == '__ah__':
                home_by_line: Dict[float, float] = {}
                away_by_line: Dict[float, float] = {}
                for s in raw_stakes:
                    a_val = s.get('a')
                    odds  = s.get('f')
                    name  = s.get('n', '')
                    if a_val is None or not odds or odds <= 1.0:
                        continue
                    try:
                        a_val = float(a_val)
                    except (TypeError, ValueError):
                        continue
                    if abs(a_val * 4) % 2 != 0:
                        continue  # quarter-ball
                    if name == 'Handicap 1':
                        home_by_line[a_val] = odds
                    elif name == 'Handicap 2':
                        away_by_line[a_val] = odds

                for h_line, h_odds in home_by_line.items():
                    a_line = -h_line
                    a_odds = away_by_line.get(a_line)
                    if a_odds is None:
                        continue
                    line_str = f'+{h_line:g}' if h_line > 0 else f'{h_line:g}'
                    result.append(Event(
                        event_id  = f'{id_prefix}_{event_id}_ah_{line_str}',
                        bookmaker = BOOKMAKER,
                        sport     = sport,
                        home_team = home,
                        away_team = away,
                        market    = f'Asian Handicap {line_str}',
                        outcomes  = [
                            Outcome(name='Home', odds=h_odds, bookmaker=BOOKMAKER, event_url=event_url),
                            Outcome(name='Away', odds=a_odds, bookmaker=BOOKMAKER, event_url=event_url),
                        ],
                        starts_at = starts_at,
                        league    = league,
                    ))
                continue

            # Dynamic O/U (basketball): accept all total lines
            if line_filter == '__ou__':
                by_line: Dict[float, Dict[str, float]] = {}
                for s in raw_stakes:
                    a_val = s.get('a')
                    odds  = s.get('f')
                    name  = s.get('n', '')
                    label = outcome_map.get(name)
                    if a_val is None or label is None or not odds or odds <= 1.0:
                        continue
                    try:
                        a_val = float(a_val)
                    except (TypeError, ValueError):
                        continue
                    by_line.setdefault(a_val, {})[label] = odds
                for line_val, side_odds in by_line.items():
                    if len(side_odds) < 2:
                        continue
                    line_str = f'{line_val:g}'
                    result.append(Event(
                        event_id  = f'{id_prefix}_{event_id}_ou_{line_str}',
                        bookmaker = BOOKMAKER,
                        sport     = sport,
                        home_team = home,
                        away_team = away,
                        market    = f'Over/Under {line_str}',
                        outcomes  = [
                            Outcome(name=lbl, odds=od, bookmaker=BOOKMAKER, event_url=event_url)
                            for lbl, od in side_odds.items()
                        ],
                        starts_at = starts_at,
                        league    = league,
                    ))
                continue

            # O/U lines: group by .a value, filter to specific lines
            if isinstance(line_filter, list):
                by_line: Dict[float, Dict[str, float]] = {}
                for s in raw_stakes:
                    a_val = s.get('a')
                    odds  = s.get('f')
                    name  = s.get('n', '')
                    label = outcome_map.get(name)
                    if a_val is None or label is None or not odds or odds <= 1.0:
                        continue
                    try:
                        a_val = float(a_val)
                    except (TypeError, ValueError):
                        continue
                    by_line.setdefault(a_val, {})[label] = odds

                for line_val, side_odds in by_line.items():
                    if line_val not in line_filter:
                        continue
                    if len(side_odds) < 2:
                        continue
                    line_str = f'{line_val:g}'
                    result.append(Event(
                        event_id  = f'{id_prefix}_{event_id}_ou_{line_str}',
                        bookmaker = BOOKMAKER,
                        sport     = sport,
                        home_team = home,
                        away_team = away,
                        market    = f'Over/Under {line_str}',
                        outcomes  = [
                            Outcome(name=lbl, odds=od, bookmaker=BOOKMAKER, event_url=event_url)
                            for lbl, od in side_odds.items()
                        ],
                        starts_at = starts_at,
                        league    = league,
                    ))
                continue

            # Simple markets (1X2, BTTS, Double Chance, Home/Away)
            # If any active (non-suspended) stake is not in the outcome_map, the
            # platform returned a different market variant (e.g. basketball
            # "Result OT not including" is a 3-way Win1/X/Win2 market that shares
            # stakeTypeId=1 with the standard 2-way Home/Away). Skip it entirely
            # so it doesn't get matched against incompatible markets elsewhere.
            outcomes: List[Outcome] = []
            unexpected = False
            for s in raw_stakes:
                name  = s.get('n', '')
                odds  = s.get('f')
                label = outcome_map.get(name)
                if label is None:
                    if odds and float(odds) > 1.0:
                        unexpected = True
                        break
                    continue
                if not odds or odds <= 1.0:
                    continue
                outcomes.append(Outcome(name=label, odds=float(odds), bookmaker=BOOKMAKER, event_url=event_url))

            if unexpected:
                continue

            if len(outcomes) == len(outcome_map):
                result.append(Event(
                    event_id  = f'{id_prefix}_{event_id}_{st_id}',
                    bookmaker = BOOKMAKER,
                    sport     = sport,
                    home_team = home,
                    away_team = away,
                    market    = market_label,
                    outcomes  = outcomes,
                    starts_at = starts_at,
                    league    = league,
                ))

        return result


# ── Helpers ───────────────────────────────────────────────────────────────────

def _build_event_url(event_id, cid, rid, sid, home: str, away: str) -> Optional[str]:
    if not (event_id and cid and rid and sid):
        return None
    home_slug = re.sub(r'[^a-z0-9]+', '-', home.lower()).strip('-')
    away_slug = re.sub(r'[^a-z0-9]+', '-', away.lower()).strip('-')
    return (
        f'https://betjara.ng/en/sport/event-details/'
        f'{cid}-{event_id}-{rid}-{sid}-0-0-0-{home_slug}-{away_slug}'
    )


def _parse_date(s: Optional[str]) -> Optional[datetime]:
    if not s:
        return None
    try:
        return datetime.fromisoformat(s.rstrip('Z').replace('+00:00', ''))
    except Exception:
        return None
