"""
Accessbet Nigeria scraper.

Platform: Beto/Zapnet white-label sportsbook (accessbet.com)

Data source:
  WebSocket: wss://accessbet.com/ws/
  Protocol:  Initial blob is LZString.compressToUTF16() compressed JSON.
  No authentication required.

Flow per refresh:
  1. Connect to wss://accessbet.com/ws/
  2. Receive subscription ACK (plain JSON)
  3. Send {"requestArguments":{}} to trigger full state dump
  4. Receive one LZString UTF-16 compressed blob (~125 KB → ~1.2 MB JSON)
  5. Parse typed message objects: sport / category / tournament / match / market

Prematch markets:
  Football  1X2:     marketTypeId=3   outcomes: "1"=Home  "X"=Draw  "2"=Away
  Over/Under / BTTS: NOT available for prematch on this platform's feed

Live markets:
  Football  1X2:     marketTypeId=332  outcomes: "1"=Home  "X"=Draw  "2"=Away
  Basketball H/A:    marketTypeId=347  outcomes: "1"=Home  "2"=Away
  Tennis    H/A:     marketTypeId=403  outcomes: "1"=Home  "2"=Away

The WebSocket blob is cached for CACHE_TTL seconds so prematch and live
parsing share a single connection per refresh cycle.

Sport IDs (internal):
  Football=1  Basketball=2  Tennis=5
"""

import json
import logging
import time
from datetime import datetime
from typing import List, Optional

import websocket

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

logger = logging.getLogger(__name__)

WS_URL      = 'wss://accessbet.com/ws/'
RECV_TIMEOUT = 30   # seconds to wait for the blob
CACHE_TTL    = 25   # seconds to reuse the same blob within a refresh cycle

# Internal sport ID → our sport name
_SPORT_MAP = {
    1: 'football',
    2: 'basketball',
    5: 'tennis',
}

# ── Prematch markets ──────────────────────────────────────────────────────────
_PREMATCH_MARKET = 3   # 1X2 football only
_PREMATCH_OUTCOMES = {
    'football': {'1': 'Home', 'X': 'Draw', '2': 'Away'},
}

# ── Live markets ──────────────────────────────────────────────────────────────
# Priority-ordered list of marketTypeIds to try per sport (first found wins)
_LIVE_MARKET_PRIORITY = {
    'football':   [332, 3],
    'basketball': [347, 369, 382, 403],
    'tennis':     [403, 989, 382],
}

# Outcome key → label for each live marketTypeId
_LIVE_OUTCOME_MAP = {
    332: {'1': 'Home', 'X': 'Draw', '2': 'Away'},
    3:   {'1': 'Home', 'X': 'Draw', '2': 'Away'},
    347: {'1': 'Home', '2': 'Away'},
    369: {'1': 'Home', '2': 'Away'},
    382: {'1': 'Home', '2': 'Away'},
    403: {'1': 'Home', '2': 'Away'},
    989: {'1': 'Home', '2': 'Away'},
}


# ── LZString decompressFromUTF16 (Python port) ────────────────────────────────

def _lz_decompress(length, reset_value, get_next_value):
    dictionary = {}
    result = []
    data = {'val': get_next_value(0), 'position': reset_value, 'index': 1}

    for i in range(3):
        dictionary[i] = i

    bits = 0
    maxpower = 4
    power = 1
    while power != maxpower:
        resb = data['val'] & data['position']
        data['position'] >>= 1
        if data['position'] == 0:
            data['position'] = reset_value
            data['val'] = get_next_value(data['index'])
            data['index'] += 1
        bits |= (1 if resb > 0 else 0) * power
        power <<= 1

    next_item = bits
    if next_item == 0:
        bits = 0; maxpower = 256; power = 1
        while power != maxpower:
            resb = data['val'] & data['position']
            data['position'] >>= 1
            if data['position'] == 0:
                data['position'] = reset_value
                data['val'] = get_next_value(data['index'])
                data['index'] += 1
            bits |= (1 if resb > 0 else 0) * power
            power <<= 1
        c = chr(bits)
    elif next_item == 1:
        bits = 0; maxpower = 65536; power = 1
        while power != maxpower:
            resb = data['val'] & data['position']
            data['position'] >>= 1
            if data['position'] == 0:
                data['position'] = reset_value
                data['val'] = get_next_value(data['index'])
                data['index'] += 1
            bits |= (1 if resb > 0 else 0) * power
            power <<= 1
        c = chr(bits)
    elif next_item == 2:
        return ''
    else:
        return None

    dictionary[3] = c
    w = c
    result.append(c)
    enlargeIn = 4
    dictSize = 4
    numBits = 3

    while True:
        bits = 0
        maxpower = pow(2, numBits)
        power = 1
        while power != maxpower:
            resb = data['val'] & data['position']
            data['position'] >>= 1
            if data['position'] == 0:
                data['position'] = reset_value
                data['val'] = get_next_value(data['index'])
                data['index'] += 1
            bits |= (1 if resb > 0 else 0) * power
            power <<= 1

        cc = bits
        if cc == 0:
            bits = 0; maxpower = 256; power = 1
            while power != maxpower:
                resb = data['val'] & data['position']
                data['position'] >>= 1
                if data['position'] == 0:
                    data['position'] = reset_value
                    data['val'] = get_next_value(data['index'])
                    data['index'] += 1
                bits |= (1 if resb > 0 else 0) * power
                power <<= 1
            dictionary[dictSize] = chr(bits)
            dictSize += 1
            cc = dictSize - 1
            enlargeIn -= 1
        elif cc == 1:
            bits = 0; maxpower = 65536; power = 1
            while power != maxpower:
                resb = data['val'] & data['position']
                data['position'] >>= 1
                if data['position'] == 0:
                    data['position'] = reset_value
                    data['val'] = get_next_value(data['index'])
                    data['index'] += 1
                bits |= (1 if resb > 0 else 0) * power
                power <<= 1
            dictionary[dictSize] = chr(bits)
            dictSize += 1
            cc = dictSize - 1
            enlargeIn -= 1
        elif cc == 2:
            return ''.join(result)

        if enlargeIn == 0:
            enlargeIn = pow(2, numBits)
            numBits += 1

        if cc in dictionary:
            entry = dictionary[cc]
        elif cc == dictSize:
            entry = w + w[0]
        else:
            return None

        result.append(entry)
        dictionary[dictSize] = w + entry[0]
        dictSize += 1
        enlargeIn -= 1

        if enlargeIn == 0:
            enlargeIn = pow(2, numBits)
            numBits += 1

        w = entry


def _lz_decompress_utf16(compressed: str) -> Optional[str]:
    if not compressed:
        return None
    return _lz_decompress(
        len(compressed), 16384,
        lambda i: ord(compressed[i]) - 32
    )


# ── Scraper ───────────────────────────────────────────────────────────────────

class AccessbetScraper(BaseScraper):

    def __init__(self):
        super().__init__('Accessbet')
        self._cache_messages: list = []
        self._cache_time: float = 0.0

    # ── Public — prematch ─────────────────────────────────────────────────────

    def get_events(self, sport: str) -> List[Event]:
        if sport not in _PREMATCH_OUTCOMES:
            return []
        try:
            return self._parse_prematch(self._get_messages(), sport)
        except Exception as ex:
            logger.error(f'[Accessbet] {sport} error: {ex}')
            return []

    def get_all_events(self) -> List[Event]:
        """Single WebSocket fetch → parse all prematch sports."""
        try:
            messages = self._get_messages()
        except Exception as ex:
            logger.error(f'[Accessbet] fetch error: {ex}')
            return []
        all_events: List[Event] = []
        for sport in ['football', 'basketball', 'tennis']:
            events = self._parse_prematch(messages, sport)
            all_events.extend(events)
            logger.info(f'[Accessbet] {sport}: {len(events)} events')
        return all_events

    # ── Public — live ─────────────────────────────────────────────────────────

    def get_live_events(self, sport: str) -> List[Event]:
        if sport not in _LIVE_MARKET_PRIORITY:
            return []
        try:
            return self._parse_live(self._get_messages(), sport)
        except Exception as ex:
            logger.error(f'[Accessbet] live {sport} error: {ex}')
            return []

    def get_all_live_events(self) -> List[Event]:
        """Single WebSocket fetch → parse all live sports (shares cache with prematch)."""
        try:
            messages = self._get_messages()
        except Exception as ex:
            logger.error(f'[Accessbet] live fetch error: {ex}')
            return []
        all_events: List[Event] = []
        for sport in ['football', 'basketball', 'tennis']:
            events = self._parse_live(messages, sport)
            all_events.extend(events)
            if events:
                logger.info(f'[Accessbet] live {sport}: {len(events)} events')
        return all_events

    # ── Cache + WebSocket ─────────────────────────────────────────────────────

    def _get_messages(self) -> list:
        """Return cached messages if fresh, else fetch a new blob."""
        if self._cache_messages and (time.time() - self._cache_time) < CACHE_TTL:
            return self._cache_messages
        messages = self._fetch_messages()
        self._cache_messages = messages
        self._cache_time = time.time()
        return messages

    def _fetch_messages(self) -> list:
        """Connect to WebSocket, receive the initial state blob, return messages list."""
        ws = websocket.WebSocket()
        ws.connect(
            WS_URL,
            header={
                'Origin': 'https://accessbet.com',
                'User-Agent': (
                    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                    'AppleWebKit/537.36 (KHTML, like Gecko) '
                    'Chrome/120.0.0.0 Safari/537.36'
                ),
            },
            timeout=RECV_TIMEOUT,
        )
        try:
            ws.recv()                          # subscription ACK — discard
            ws.send('{"requestArguments":{}}') # trigger full state dump
            raw = ws.recv()                    # compressed blob
        finally:
            ws.close()

        # Decompress if not plain JSON
        if raw and raw[0] == '{':
            data = json.loads(raw)
        else:
            decompressed = _lz_decompress_utf16(raw)
            if not decompressed:
                raise RuntimeError('LZString decompression failed')
            data = json.loads(decompressed)

        return data.get('messages', [])

    # ── Parsers ───────────────────────────────────────────────────────────────

    def _parse_prematch(self, messages: list, sport: str) -> List[Event]:
        import config as _cfg

        target_sport_id = next((k for k, v in _SPORT_MAP.items() if v == sport), None)
        if target_sport_id is None:
            return []

        cutoff = datetime.utcnow().timestamp() + _cfg.HOURS_AHEAD * 3600

        idx = self._index_messages(messages)
        outcome_map = _PREMATCH_OUTCOMES.get(sport)
        if not outcome_map:
            return []

        events: List[Event] = []
        for match in idx['matches']:
            if match.get('isLive'):
                continue
            if self._sport_id(match, idx) != target_sport_id:
                continue

            start_ts = self._start_ts(match)
            if start_ts and start_ts > cutoff:
                continue
            starts_at = datetime.utcfromtimestamp(start_ts) if start_ts else None

            home, away = self._competitors(match)
            if not home or not away:
                continue

            match_id      = match['id']
            tournament_id = match.get('tournamentId', '')
            sport_id      = self._sport_id(match, idx) or target_sport_id
            league        = idx['tournaments'].get(tournament_id, {}).get('name', '')
            event_url     = f'https://accessbet.com/sports/match?id={match_id}&t={tournament_id}&cs={sport_id}'

            for mk in idx['markets_by_match'].get(match_id, []):
                if mk.get('marketTypeId') != _PREMATCH_MARKET:
                    continue
                outcomes = self._parse_selections(mk, outcome_map, event_url)
                if len(outcomes) == len(outcome_map):
                    events.append(Event(
                        event_id  = f'ab_{match_id}',
                        bookmaker = 'Accessbet',
                        sport     = sport,
                        home_team = home,
                        away_team = away,
                        market    = '1X2',
                        outcomes  = outcomes,
                        starts_at = starts_at,
                        league    = league,
                    ))
                break

        return events

    def _parse_live(self, messages: list, sport: str) -> List[Event]:
        target_sport_id = next((k for k, v in _SPORT_MAP.items() if v == sport), None)
        if target_sport_id is None:
            return []

        mt_priority = _LIVE_MARKET_PRIORITY.get(sport, [])
        idx = self._index_messages(messages)

        events: List[Event] = []
        for match in idx['matches']:
            if not match.get('isLive'):
                continue
            if self._sport_id(match, idx) != target_sport_id:
                continue

            start_ts  = self._start_ts(match)
            starts_at = datetime.utcfromtimestamp(start_ts) if start_ts else None

            home, away = self._competitors(match)
            if not home or not away:
                continue

            match_id      = match['id']
            tournament_id = match.get('tournamentId', '')
            sport_id      = self._sport_id(match, idx) or target_sport_id
            league        = idx['tournaments'].get(tournament_id, {}).get('name', '')
            event_url     = f'https://accessbet.com/sports/match?id={match_id}&t={tournament_id}&cs={sport_id}'

            # Build a lookup of marketTypeId → market for this match
            mks_by_type: dict = {}
            for mk in idx['markets_by_match'].get(match_id, []):
                mt = mk.get('marketTypeId')
                if mt not in mks_by_type:
                    mks_by_type[mt] = mk

            # Try market types in priority order
            for mt_id in mt_priority:
                mk = mks_by_type.get(mt_id)
                if not mk:
                    continue
                outcome_map = _LIVE_OUTCOME_MAP.get(mt_id, {})
                outcomes = self._parse_selections(mk, outcome_map, event_url)
                if len(outcomes) == len(outcome_map):
                    market = '1X2' if sport == 'football' else 'Home/Away'
                    events.append(Event(
                        event_id  = f'ab_live_{match_id}',
                        bookmaker = 'Accessbet',
                        sport     = sport,
                        home_team = home,
                        away_team = away,
                        market    = market,
                        outcomes  = outcomes,
                        starts_at = starts_at,
                        league    = league,
                    ))
                    break

        return events

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

    def _index_messages(self, messages: list) -> dict:
        tournaments       = {m['id']: m for m in messages if m.get('object') == 'tournament'}
        categories        = {m['id']: m for m in messages if m.get('object') == 'category'}
        matches           = [m for m in messages if m.get('object') == 'match']
        markets_by_match: dict = {}
        for mk in messages:
            if mk.get('object') != 'market':
                continue
            mid = mk.get('matchId')
            if mid is not None:
                markets_by_match.setdefault(mid, []).append(mk)
        return {
            'tournaments':      tournaments,
            'categories':       categories,
            'matches':          matches,
            'markets_by_match': markets_by_match,
        }

    def _sport_id(self, match: dict, idx: dict) -> Optional[int]:
        t = idx['tournaments'].get(match.get('tournamentId'), {})
        c = idx['categories'].get(t.get('categoryId'), {})
        return c.get('sportId')

    @staticmethod
    def _start_ts(match: dict) -> Optional[int]:
        ms = match.get('startTs', 0)
        return (ms // 1000) if ms else None

    @staticmethod
    def _competitors(match: dict):
        c = match.get('competitors', [])
        return (c[0] if c else ''), (c[1] if len(c) > 1 else '')

    @staticmethod
    def _parse_selections(mk: dict, outcome_map: dict, event_url: str) -> List[Outcome]:
        outcomes = []
        for sel in mk.get('selections', []):
            label = outcome_map.get(sel.get('outcome'))
            if label is None:
                continue
            try:
                odds = float(sel['odds'])
            except (KeyError, TypeError, ValueError):
                continue
            if odds > 1.0:
                outcomes.append(Outcome(
                    name=label, odds=odds,
                    bookmaker='Accessbet', event_url=event_url,
                ))
        return outcomes
