"""
Betpawa Nigeria scraper.

Uses Playwright to bypass Cloudflare; all API calls are made via page.evaluate()
from within the browser context so the Cloudflare cookies are sent automatically.

API endpoint:
  GET /api/sportsbook/v3/events/lists/by-queries?q=<JSON>

Query format (confirmed working):
  {"queries": [{"query": {"eventType": "UPCOMING", "categories": ["<id>"], "zones": {}, "hasOdds": true},
                "view":  {"marketTypes": ["<mktId>"]},
                "skip": 0, "take": 50}]}

Response structure:
  {"responses": [{"request": {...}, "responses": [<events>]}]}

Sport category IDs (strings):
  Football="2"  Basketball="3"  Tennis="452"

Market type IDs (strings):
  1X2="3743"            — prices: name "1"/"X"/"2"
  Over/Under="5000"     — prices: name "Over"/"Under", filter handicap==2.5
  Draw No Bet="4703"    — prices: name "1"/"2" (no handicap)
  Asian Handicap="3774" — prices: name "1"/"2", handicap field e.g. "-0.5"/"+0.5"
  Basketball W/L="4791" — prices: name "1"/"2"
  Tennis W/L="2043818"  — prices: name "1"/"2"

Event structure:
  id            — event ID
  participants  — [{id, name, position}] position 1=home, 2=away
  startTime     — ISO UTC string, e.g. "2026-03-27T23:15:00Z"
  markets[]     — [{marketType: {id, name}, row[]: [{prices[]}]}]
  prices[]      — {id, name, price (decimal), suspended, handicap}
  competition.name — league name
"""
import json
import logging
import queue
import threading
import time
from datetime import datetime
from typing import List, Optional

import psutil

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

logger = logging.getLogger(__name__)

BASE_URL   = 'https://www.betpawa.ng'
EVENTS_URL = f'{BASE_URL}/api/sportsbook/v3/events/lists/by-queries'

SPORT_IDS = {
    'football':   '2',
    'basketball': '3',
    'tennis':     '452',
}

# (market_type_id, market_name, price_name_map, handicap_filter)
MARKETS = {
    'football': [
        ('3743', '1X2',            {'1': 'Home', 'X': 'Draw', '2': 'Away'}, None),
        ('3741', 'HT 1X2',         {'1': 'Home', 'X': 'Draw', '2': 'Away'}, None),
        ('3742', 'Double Chance',  {'1X': '1X', 'X2': 'X2', '12': '12'},    None),
        ('5000', 'Over/Under 0.5', {'Over': 'Over', 'Under': 'Under'},       0.5),
        ('5000', 'Over/Under 1.5', {'Over': 'Over', 'Under': 'Under'},       1.5),
        ('5000', 'Over/Under 2.5', {'Over': 'Over', 'Under': 'Under'},       2.5),
        ('5000', 'Over/Under 3.5', {'Over': 'Over', 'Under': 'Under'},       3.5),
        ('5000', 'Over/Under 4.5', {'Over': 'Over', 'Under': 'Under'},       4.5),
        ('3795', 'BTTS',           {'Yes': 'Yes', 'No': 'No'},               None),
        ('4703', 'Draw No Bet',    {'1': 'Home', '2': 'Away'},               None),
        ('3774', 'Asian Handicap', {'1': 'Home', '2': 'Away'},               '__ah__'),
    ],
    'basketball': [
        ('4791', 'Home/Away', {'1': 'Home', '2': 'Away'}, None),
    ],
    'tennis': [
        ('2043818', 'Home/Away', {'1': 'Home', '2': 'Away'}, None),
    ],
}

PAGE_SIZE = 50

# JS executed inside browser context via page.evaluate() — single arg dict
_FETCH_JS = """
async ({url, queryJson}) => {
    const resp = await fetch(url + '?q=' + encodeURIComponent(queryJson), {
        method: 'GET',
        credentials: 'include',
        headers: {
            'Accept':           'application/json',
            'devicetype':       'web',
            'x-pawa-language':  'en',
            'x-pawa-brand':     'betpawa-nigeria',
        }
    });
    if (!resp.ok) throw new Error('HTTP ' + resp.status);
    return await resp.json();
}
"""


class _BetpawaWorker(threading.Thread):
    """
    Single dedicated daemon thread that owns the Playwright browser session.
    One Chrome process for all Betpawa scraping regardless of how many
    APScheduler/ThreadPoolExecutor threads call get_events().
    """

    def __init__(self):
        super().__init__(daemon=True, name='betpawa-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=60):
            raise RuntimeError('[Betpawa] browser worker timed out during init')
        if self._failed:
            raise self._failed

    def run(self):
        try:
            from playwright.sync_api import sync_playwright
            pw = sync_playwright().start()
            self._pw = pw
            browser = pw.chromium.launch(
                headless=True,
                args=[
                    '--no-sandbox',
                    '--disable-gpu',
                    '--disable-dev-shm-usage',
                    '--disable-extensions',
                    '--disable-background-networking',
                    '--no-first-run',
                ],
            )
            ctx = browser.new_context(locale='en-US')
            page = ctx.new_page()
            page.goto(f'{BASE_URL}/events', wait_until='domcontentloaded', timeout=30_000)
            time.sleep(3)
            logger.info('[Betpawa] browser worker ready')
        except Exception as ex:
            self._failed = ex
            self._ready.set()
            return

        self._ready.set()

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

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

    def call(self, fn, timeout: float = 60):
        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('[Betpawa] worker call timed out')
        if status == 'err':
            raise val
        return val

    def _kill_processes(self):
        """Force-kill the Playwright node driver and all its Chrome children."""
        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)


class BetpawaScraper(BaseScraper):

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

    def __init__(self):
        super().__init__('Betpawa')

    # ── 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 = _BetpawaWorker()
                return True
            except Exception as ex:
                logger.error(f'[Betpawa] 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_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 page: self._fetch_sport_in_worker(page, sport, sport_id))
        except Exception as ex:
            logger.error(f'[Betpawa] {sport} error: {ex}')
            self._reset_worker()
            return []

    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 page: self._fetch_sport_in_worker(page, sport, sport_id, event_type='LIVE'))
        except Exception as ex:
            logger.error(f'[Betpawa] live {sport} error: {ex}')
            self._reset_worker()
            return []

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

    def _fetch_sport_in_worker(self, page, sport: str, sport_id: str, event_type: str = 'UPCOMING') -> List[Event]:
        """Called inside the worker thread with the page object."""
        return self._fetch_sport(sport, sport_id, event_type=event_type, page=page)

    def _fetch_sport(self, sport: str, sport_id: str, event_type: str = 'UPCOMING', page=None) -> List[Event]:
        sport_markets = MARKETS.get(sport, [])
        market_ids = list(dict.fromkeys(mt_id for mt_id, _, _, _ in sport_markets))
        raw_events = self._paginate(sport_id, market_ids, event_type=event_type, page=page)
        events: List[Event] = []
        for raw in raw_events:
            events.extend(self._parse(raw, sport))
        return events

    def _paginate(self, sport_id: str, market_ids: list, event_type: str = 'UPCOMING', page=None) -> list:
        all_items = []
        skip = 0
        while True:
            query = json.dumps({
                'queries': [{
                    'query': {
                        'eventType': event_type,
                        'categories': [sport_id],
                        'zones':      {},
                        'hasOdds':    True,
                    },
                    'view': {'marketTypes': market_ids},
                    'skip': skip,
                    'take': PAGE_SIZE,
                }]
            })
            try:
                data = page.evaluate(
                    _FETCH_JS, {'url': EVENTS_URL, 'queryJson': query}
                )
            except Exception as ex:
                logger.warning(f'[Betpawa] page.evaluate failed at skip={skip}: {ex}')
                break

            # Response: {"responses": [{"request":..., "responses": [events]}]}
            items = []
            outer = data.get('responses', []) if isinstance(data, dict) else []
            if outer:
                items = outer[0].get('responses', []) or []

            if not items:
                break
            all_items.extend(items)
            if len(items) < PAGE_SIZE:
                break
            skip += PAGE_SIZE
        return all_items

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

    def _parse(self, raw: dict, sport: str) -> List[Event]:
        participants = raw.get('participants') or []
        home = away = ''
        for p in participants:
            if p.get('position') == 1:
                home = (p.get('name') or '').strip()
            elif p.get('position') == 2:
                away = (p.get('name') or '').strip()
        if not home or not away:
            return []

        starts_at     = _parse_time(raw.get('startTime'))
        event_id_base = str(raw.get('id', ''))
        league        = (raw.get('competition') or {}).get('name', '')

        # Build market-type-id → prices lookup
        mkt_lookup: dict = {}
        for mkt in (raw.get('markets') or []):
            mt_id = str((mkt.get('marketType') or {}).get('id', ''))
            prices = []
            for row in (mkt.get('row') or []):    # note: "row" not "rows"
                prices.extend(row.get('prices') or [])
            if mt_id:
                mkt_lookup.setdefault(mt_id, []).extend(prices)

        event_url = f'https://www.betpawa.ng/event/{event_id_base}'

        result: List[Event] = []
        for mt_id, market_name, name_map, hcap_filter in MARKETS.get(sport, []):
            prices = mkt_lookup.get(mt_id, [])

            # Asian Handicap: group by line and emit one Event per line
            if hcap_filter == '__ah__':
                home_odds: dict = {}  # home_handicap_str -> odds
                away_odds: dict = {}  # away_handicap_str -> odds
                for price in prices:
                    if price.get('suspended'):
                        continue
                    name = price.get('name') or ''
                    hcap = price.get('handicap')
                    odds = _to_float(price.get('price'))
                    if not odds or odds <= 1.0 or not hcap:
                        continue
                    if name == '1':
                        home_odds[str(hcap)] = odds
                    elif name == '2':
                        away_odds[str(hcap)] = odds

                for home_hcap_str, home_odd in home_odds.items():
                    try:
                        home_val = float(home_hcap_str)
                    except ValueError:
                        continue
                    if abs(home_val * 4) % 2 != 0:  # reject quarter-ball
                        continue
                    away_val = -home_val
                    away_key = f'+{away_val:g}' if away_val > 0 else f'{away_val:g}'
                    away_odd = away_odds.get(away_key)
                    if not away_odd:
                        continue
                    line = f'+{home_val:g}' if home_val > 0 else f'{home_val:g}'
                    result.append(Event(
                        event_id  = f'bpw_{event_id_base}_{mt_id}_{line}',
                        bookmaker = 'Betpawa',
                        sport     = sport,
                        home_team = home,
                        away_team = away,
                        market    = f'Asian Handicap {line}',
                        outcomes  = [
                            Outcome(name='Home', odds=home_odd, bookmaker='Betpawa', event_url=event_url),
                            Outcome(name='Away', odds=away_odd, bookmaker='Betpawa', event_url=event_url),
                        ],
                        starts_at = starts_at,
                        league    = league,
                    ))
                continue

            if hcap_filter is not None:
                prices = [p for p in prices if _to_float(p.get('handicap')) == hcap_filter]

            outcomes = []
            for price in prices:
                if price.get('suspended'):
                    continue
                label = name_map.get(price.get('name') or '')
                if label is None:
                    continue
                odds = _to_float(price.get('price'))
                if odds and odds > 1.0:
                    outcomes.append(Outcome(name=label, odds=odds, bookmaker='Betpawa', event_url=event_url))

            if len(outcomes) == len(name_map):
                result.append(Event(
                    event_id  = f'bpw_{event_id_base}_{mt_id}',
                    bookmaker = 'Betpawa',
                    sport     = sport,
                    home_team = home,
                    away_team = away,
                    market    = market_name,
                    outcomes  = outcomes,
                    starts_at = starts_at,
                    league    = league,
                ))

        return result


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

def _to_float(val) -> Optional[float]:
    try:
        return float(val)
    except (TypeError, ValueError):
        return None


def _parse_time(s: str) -> Optional[datetime]:
    """Parse ISO UTC string like '2026-03-27T23:15:00Z' → naive UTC datetime."""
    if not s:
        return None
    try:
        s = s.rstrip('Z').replace('+00:00', '')
        return datetime.fromisoformat(s)
    except Exception:
        return None
