"""
1win Nigeria scraper.

Platform: 1win.ng — sports betting iframe powered by api-gateway.top-parser.com

API (no authentication / cookies required — just x-external-partner-id header):
  Base:    https://api-gateway.top-parser.com
  Partner: 44ba10e5-7df2-47ab-a44d-dc93803c7a6e

Flow per sport:
  1. POST /matches/get-many  → paginate to get all prematch match IDs + metadata
  2. WebSocket subscribe     → receive base oddsGroups per match

WebSocket endpoint:
  wss://api-gateway.top-parser.com/push-server-v2/
      ?Language=en-001&externalPartnerId=<PARTNER>&EIO=4&transport=websocket

  Socket.IO protocol (Engine.IO v4):
    recv "0{...}"   → engine.io OPEN; send "40" to establish socket.io session
    recv "40{...}"  → socket.io connected; send subscribe event
    send 42["subscribe", {"messageType":"subscribe-match-odds",
                          "data":{"matchIds":[...],"isBaseOddsGroups":true}}]
    recv 42["u", {"data":{"matchId":N,"oddsGroups":[...]}}]

Market → oddsGroup name:
  Football  1X2           → "Full time result"   outcomes: "1"=Home "x"=Draw "2"=Away
  Football  Double Chance → "Double chance"       outcomes: "1X"=1X "X2"=X2 "12"=12
  Football  O/U 2.5       → "Total"              filter name=="Over 2.5" / "Under 2.5"
  Football  BTTS          → "Both teams to score" outcomes: "1"=Yes "2"=No
  Football  Asian Handicap→ "Handicap"           outcome "1"=Home "2"=Away,
                                                  name "{team} {val}" e.g. "Genoa -0.5"
                                                  pairs: home_val + away_val == 0
  Basketball H/A          → "Winner (incl. OT)"   outcomes: "1"=Home "2"=Away
  Tennis    H/A           → "Winner"              outcomes: "1"=Home "2"=Away

  Draw No Bet: not offered by 1win.

Sport IDs:
  Football=18  Basketball=23  Tennis=33

cf = coefficient (decimal odds)
"""
import json
import logging
import threading
import time
from concurrent.futures import Future
from datetime import datetime
from typing import List, Dict, Optional

import websocket
from fuzzywuzzy import fuzz

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

logger = logging.getLogger(__name__)

BASE_URL  = 'https://api-gateway.top-parser.com'
WS_URL    = ('wss://api-gateway.top-parser.com/push-server-v2/'
             '?Language=en-001&externalPartnerId=44ba10e5-7df2-47ab-a44d-dc93803c7a6e'
             '&EIO=4&transport=websocket')
PARTNER   = '44ba10e5-7df2-47ab-a44d-dc93803c7a6e'

SPORT_IDS = {
    'football':   18,
    'basketball': 23,
    'tennis':     33,
}

# oddsGroup names → market name + outcome mapping
# key = (sport, group_name_lower_fragment)
MARKET_GROUPS = {
    'football': {
        'full time result':      ('1X2',           {'1': 'Home', 'x': 'Draw', '2': 'Away'}),
        'double chance':         ('Double Chance',  {'1X': '1X',  'X2': 'X2',  '12': '12'}),
        'both teams to score':   ('BTTS',           {'1': 'Yes',  '2': 'No'}),
        'first half result':     ('HT 1X2',         {'1': 'Home', 'x': 'Draw', '2': 'Away'}),
        '1st half result':       ('HT 1X2',         {'1': 'Home', 'x': 'Draw', '2': 'Away'}),
        'halftime':              ('HT 1X2',         {'1': 'Home', 'x': 'Draw', '2': 'Away'}),
        'half time result':      ('HT 1X2',         {'1': 'Home', 'x': 'Draw', '2': 'Away'}),
    },
    'basketball': {
        'winner (incl. ot)': ('Home/Away',    {'1': 'Home', '2': 'Away'}),
    },
    'tennis': {
        'winner':            ('Home/Away',    {'1': 'Home', '2': 'Away'}),
    },
}

# Over/Under 2.5 group — identified by name fragment, filter on oddsList name
OU_GROUP_FRAGMENT = 'total'  # group name contains "total" (but NOT "1st", "2nd" etc.)
OU_LINE = 2.5                # filter oddsList entries whose name ends with "2.5"


class OneWinScraper(BaseScraper):

    def __init__(self):
        super().__init__('1win')
        self.session.headers.update({
            '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'),
            'Accept':         'application/json',
            'Referer':        'https://1win.ng/',
            'Origin':         'https://1win.ng',
            'x-lang':         'en-001',
            'x-external-partner-id': PARTNER,
            'content-type':   'application/json',
        })

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

    def get_events(self, sport: str) -> List[Event]:
        sport_id = SPORT_IDS.get(sport)
        if not sport_id:
            return []
        try:
            return self._fetch_sport(sport, sport_id)
        except Exception as ex:
            logger.error(f'[1win] {sport} error: {ex}')
            return []

    def get_live_events(self, sport: str) -> List[Event]:
        sport_id = SPORT_IDS.get(sport)
        if not sport_id:
            return []
        try:
            return self._fetch_sport(sport, sport_id, live=True)
        except Exception as ex:
            logger.error(f'[1win] live {sport} error: {ex}')
            return []

    # ── Fetch ─────────────────────────────────────────────────────────────────

    def _fetch_sport(self, sport: str, sport_id: int, live: bool = False) -> List[Event]:
        # 1) Get all matches for this sport
        match_meta = self._fetch_all_matches(sport_id, live=live)
        label = 'live' if live else 'prematch'
        if not match_meta:
            logger.warning(f'[1win] no {label} matches for {sport}')
            return []

        match_ids = list(match_meta.keys())
        logger.info(f'[1win] {sport}: {len(match_ids)} {label} matches')

        # 2) Fetch odds via WebSocket
        odds_by_match = self._fetch_odds_ws(match_ids)
        logger.info(f'[1win] {sport}: received odds for {len(odds_by_match)}/{len(match_ids)} matches')

        # 3) Parse into Events
        events: List[Event] = []
        for match_id, meta in match_meta.items():
            groups = odds_by_match.get(match_id)
            if not groups:
                continue
            events.extend(self._parse(meta, groups, sport, live=live))
        return events

    def _fetch_all_matches(self, sport_id: int, live: bool = False) -> Dict[int, dict]:
        """Fetch all prematch or live matches for a sport in a single request."""
        import config as _cfg
        import time as _time

        service = 'live' if live else 'prematch'
        try:
            r = self.session.post(
                f'{BASE_URL}/matches/get-many',
                json={
                    'service':          service,
                    'sportId':          sport_id,
                    'excludeSportType': 'polybet',
                    'limit':            2000,
                },
                timeout=15,
            )
            r.raise_for_status()
            items = r.json().get('result', {}).get('items', [])
        except Exception as ex:
            logger.error(f'[1win] matches fetch error: {ex}')
            return {}

        result = {}
        for m in items:
            mid = m.get('id')
            if not mid:
                continue
            # For prematch only: skip events too far in the future
            if not live:
                cutoff_ts = _time.time() + _cfg.HOURS_AHEAD * 3600
                start_ts = m.get('startAt', 0)
                if start_ts and start_ts > cutoff_ts:
                    continue
            result[mid] = m
        return result

    def _fetch_odds_ws(self, match_ids: List[int]) -> Dict[int, list]:
        """Subscribe to WebSocket and collect base oddsGroups for all match IDs.

        Stops when either:
          - All subscribed match IDs have returned odds, OR
          - IDLE_TIMEOUT seconds pass with no new responses (server done pushing).
        """
        BATCH        = 100   # IDs per subscribe message
        IDLE_TIMEOUT = 5.0   # stop if nothing new for this many seconds
        MAX_WAIT     = 45.0  # hard upper bound

        received: Dict[int, list] = {}
        total        = len(match_ids)
        last_recv    = [time.time()]  # mutable for closure
        done         = threading.Event()

        def on_message(ws, msg):
            if msg == '2':
                ws.send('3')
                return
            if msg.startswith('0') and not msg.startswith('40'):
                ws.send('40')
                return
            if msg.startswith('40'):
                for i in range(0, len(match_ids), BATCH):
                    batch = match_ids[i:i + BATCH]
                    ws.send('42' + json.dumps([
                        'subscribe',
                        {'messageType': 'subscribe-match-odds',
                         'data': {'matchIds': batch, 'isBaseOddsGroups': False}},
                    ]))
                return
            if msg.startswith('42'):
                try:
                    payload = json.loads(msg[2:])
                except Exception:
                    return
                if len(payload) >= 2 and isinstance(payload[1], dict):
                    data = payload[1].get('data', {})
                    mid  = data.get('matchId')
                    if mid and 'oddsGroups' in data:
                        received[mid] = data['oddsGroups']
                        last_recv[0] = time.time()
                        if len(received) >= total:
                            done.set()

        def on_error(ws, err):
            # Suppress the benign teardown error from websocket-client when
            # ws.close() is called before the socket fully establishes.
            if 'is_ssl' in str(err):
                return
            logger.warning(f'[1win] WS error: {err}')

        ws = websocket.WebSocketApp(
            WS_URL,
            header={'Origin': 'https://1win.ng', 'Referer': 'https://1win.ng/'},
            on_message=on_message,
            on_error=on_error,
        )
        t = threading.Thread(target=lambda: ws.run_forever(), daemon=True)
        t.start()

        deadline = time.time() + MAX_WAIT
        while not done.is_set() and time.time() < deadline:
            time.sleep(0.5)
            if received and (time.time() - last_recv[0]) >= IDLE_TIMEOUT:
                break  # no new data for IDLE_TIMEOUT seconds → server is done

        try:
            ws.close()
        except Exception:
            pass
        return received

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

    def _parse(self, meta: dict, odds_groups: list, sport: str, live: bool = False) -> List[Event]:
        home = (meta.get('homeTeam') or {}).get('name', '').strip()
        away = (meta.get('awayTeam') or {}).get('name', '').strip()
        if not home or not away:
            return []

        ts = meta.get('startAt')
        try:
            starts_at = datetime.utcfromtimestamp(ts) if ts else None
        except Exception:
            starts_at = None

        league   = (meta.get('tournament') or {}).get('slug', '').replace('-', ' ').title()
        match_id = meta.get('id', '')
        match_slug = meta.get('slug', '')
        if match_id and match_slug:
            section = 'live/sport' if live else 'betting/match/sport'
            event_url = f'https://1win.ng/en/{section}/{match_slug}-{match_id}'
        else:
            event_url = None

        result: List[Event] = []
        groups_map = MARKET_GROUPS.get(sport, {})

        for group in odds_groups:
            name_lower = (group.get('name') or '').lower()

            # Correct Score — not a base group; handle before the isBase gate
            if 'correct score' in name_lower and sport == 'football':
                cs_outcomes = self._parse_cs_outcomes(group.get('oddsList', []), event_url)
                if len(cs_outcomes) >= 2:
                    result.append(self._make_event(
                        f'1w_{match_id}_cs',
                        sport, home, away, 'Correct Score', cs_outcomes, starts_at, league,
                    ))
                continue

            if not group.get('isBase'):
                continue

            # Standard markets (1X2, H/A) — exact match to avoid e.g.
            # "1st set. Winner" matching tennis "winner" fragment
            for fragment, (market_name, outcome_map) in groups_map.items():
                if name_lower == fragment:
                    # Use name-based matching for any Home/Away market and for
                    # football 1X2 — 1win assigns outcome codes ('1'/'2') by
                    # listing order, not reliably by home/away convention.
                    if market_name in ('Home/Away', '1X2', 'HT 1X2'):
                        outcomes = self._parse_ha_by_name(
                            group.get('oddsList', []), home, away, event_url
                        )
                    else:
                        outcomes = self._parse_outcomes_simple(
                            group.get('oddsList', []), outcome_map, event_url
                        )
                    if len(outcomes) == len(outcome_map):
                        result.append(self._make_event(
                            f'1w_{match_id}_{market_name}',
                            sport, home, away, market_name, outcomes, starts_at, league
                        ))
                    break

            # Over/Under — football and basketball; emit separate Event per line
            if (OU_GROUP_FRAGMENT in name_lower
                    and '1st' not in name_lower and '2nd' not in name_lower
                    and 'half' not in name_lower and 'quarter' not in name_lower
                    and sport in ('football', 'basketball')):
                odds_list = group.get('oddsList', [])
                for lv in ('0.5', '1.5', '2.5', '3.5', '4.5'):
                    outcomes = self._parse_outcomes_ou_line(odds_list, event_url, lv)
                    if len(outcomes) == 2:
                        result.append(self._make_event(
                            f'1w_{match_id}_ou{lv}',
                            sport, home, away, f'Over/Under {lv}', outcomes, starts_at, league
                        ))

            # Asian Handicap — FT only (exact group name to avoid half-time / corners)
            if name_lower == 'handicap' and sport == 'football':
                home_hcap: dict = {}   # hcap_float -> odds
                away_hcap: dict = {}
                for o in group.get('oddsList', []):
                    outcome_key = o.get('outcome')
                    name_str = (o.get('name') or '').strip()
                    try:
                        hcap_val = float(name_str.rsplit(' ', 1)[-1])
                    except (ValueError, IndexError):
                        continue
                    try:
                        cf = float(o['cf'])
                    except (KeyError, TypeError, ValueError):
                        continue
                    if cf <= 1.0:
                        continue
                    if outcome_key == '1':
                        home_hcap[hcap_val] = cf
                    elif outcome_key == '2':
                        away_hcap[hcap_val] = cf

                for home_val, home_cf in home_hcap.items():
                    if abs(home_val * 4) % 2 != 0:   # reject quarter-ball lines
                        continue
                    away_val = -home_val
                    # tolerance-safe lookup
                    away_cf = away_hcap.get(away_val)
                    if away_cf is None:
                        away_cf = next((v for k, v in away_hcap.items() if abs(k - away_val) < 1e-9), None)
                    if away_cf is None:
                        continue
                    line = f'+{home_val:g}' if home_val > 0 else f'{home_val:g}'
                    result.append(self._make_event(
                        f'1w_{match_id}_ah{line}',
                        sport, home, away, f'Asian Handicap {line}',
                        [
                            Outcome(name='Home', odds=home_cf, bookmaker='1win', event_url=event_url),
                            Outcome(name='Away', odds=away_cf, bookmaker='1win', event_url=event_url),
                        ],
                        starts_at, league,
                    ))


        return result

    def _parse_ha_by_name(self, odds_list: list, home: str, away: str,
                          event_url: Optional[str]) -> List[Outcome]:
        """Home/Away (tennis) and 1X2 (football) via name-based matching.

        1win assigns outcome codes ('1'/'2') by listing order, not reliably
        by home/away convention. Match each outcome's name field against the
        actual team/player names instead.

        Draw handling: outcome code 'x' is universally the draw across all
        providers — label it 'Draw' directly without name matching.
        """
        outcomes = []
        for o in odds_list:
            try:
                cf = float(o['cf'])
            except (KeyError, TypeError, ValueError):
                continue
            if cf <= 1.0:
                continue
            code = str(o.get('outcome', '')).lower()
            name = (o.get('name') or '').strip()

            # Draw is always outcome code 'x' — no ambiguity
            if code == 'x':
                label = 'Draw'
            else:
                h_score = fuzz.token_set_ratio(name.lower(), home.lower())
                a_score = fuzz.token_set_ratio(name.lower(), away.lower())
                if h_score >= 70 and h_score > a_score:
                    label = 'Home'
                elif a_score >= 70 and a_score > h_score:
                    label = 'Away'
                else:
                    # Fallback to numeric code
                    label = {'1': 'Home', '2': 'Away'}.get(code)
                    if not label:
                        logger.debug(f'[1win] unmatched outcome name={name!r} code={code!r} home={home!r} away={away!r}')
                        continue

            outcomes.append(Outcome(name=label, odds=cf, bookmaker='1win', event_url=event_url))

        # Deduplicate: keep best odds per label
        best: dict = {}
        for oc in outcomes:
            if oc.name not in best or oc.odds > best[oc.name].odds:
                best[oc.name] = oc
        return list(best.values())

    def _parse_outcomes_simple(self, odds_list: list, outcome_map: dict,
                                event_url: Optional[str]) -> List[Outcome]:
        outcomes = []
        for o in odds_list:
            label = outcome_map.get(o.get('outcome'))
            if label is None:
                continue
            try:
                cf = float(o['cf'])
            except (KeyError, TypeError, ValueError):
                continue
            if cf > 1.0:
                outcomes.append(Outcome(name=label, odds=cf, bookmaker='1win', event_url=event_url))
        return outcomes

    def _parse_cs_outcomes(self, odds_list: list, event_url: Optional[str]) -> List[Outcome]:
        """Parse Correct Score outcomes from 1win; outcome name field is the score string."""
        outcomes = []
        for o in odds_list:
            name = normalize_cs_score(o.get('name'))
            if not name:
                continue
            try:
                cf = float(o['cf'])
            except (KeyError, TypeError, ValueError):
                continue
            if cf > 1.0:
                outcomes.append(Outcome(name=name, odds=cf, bookmaker='1win', event_url=event_url))
        return outcomes

    def _parse_outcomes_ou_line(self, odds_list: list, event_url: Optional[str], line: str = '2.5') -> List[Outcome]:
        """Extract Over/Under outcomes for a specific line from a Total oddsList."""
        outcomes = []
        for o in odds_list:
            name = (o.get('name') or '').strip()
            if name not in (f'Over {line}', f'Under {line}'):
                continue
            try:
                cf = float(o['cf'])
            except (KeyError, TypeError, ValueError):
                continue
            if cf > 1.0:
                label = 'Over' if name.startswith('Over') else 'Under'
                outcomes.append(Outcome(name=label, odds=cf, bookmaker='1win', event_url=event_url))
        return outcomes

    @staticmethod
    def _make_event(event_id, sport, home, away, market, outcomes, starts_at, league) -> Event:
        return Event(
            event_id  = event_id,
            bookmaker = '1win',
            sport     = sport,
            home_team = home,
            away_team = away,
            market    = market,
            outcomes  = outcomes,
            starts_at = starts_at,
            league    = league,
        )
