"""
1xBet Nigeria bet placement driver.

API endpoint (same for prematch AND live):
  POST https://1xbet.ng/service-api/LiveBet/Secure/MakeBetWeb

Key differences between prematch and live:
  live=True  → Kind=1, live=true,  betGUID present
  live=False → Kind=3, live=false, betGUID absent

Authentication:
  x-auth: Bearer <user_token>   (header)
  Cookies: access_token, user_token, SESSION, authenticated=1

Outcome Type codes (= T field from LineFeed scraper):
  1X2:           Home=1,   Draw=2,   Away=3
  Double Chance: 1X=4,     12=5,     X2=6
  Draw No Bet:   Home=7,   Away=8
  Asian Handicap:Home=7,   Away=8    (Param = home handicap, e.g. -0.5)
  Over/Under:    Over=9,   Under=10  (Param = line, e.g. 2.5)
  BTTS:          Yes=1170, No=1171
  Home/Away (bball): Home=401, Away=402
  Home/Away (tennis):Home=1,   Away=3

Token refresh:
  Tokens expire every ~4 hours. Set ONEXBET_USER_TOKEN / ONEXBET_ACCESS_TOKEN
  in config.py. The driver will warn when tokens are near expiry.
  Auto-login endpoint TBD — capture /user/login or /service-api/Auth request.
"""
import logging
import re
import time
import uuid
from typing import Optional

import requests

from execution.drivers.base import PlacementDriver, BetResult, parse_numeric_event_id

logger = logging.getLogger(__name__)

BET_URL  = 'https://1xbet.ng/service-api/LiveBet/Secure/MakeBetWeb'
BASE_URL = 'https://1xbet.ng'

# ── Outcome type mapping ──────────────────────────────────────────────────────
# market_name_fragment → {outcome_label → Type code}
# Fragment matching: market.startswith(fragment) or market == fragment
_OUTCOME_TYPES: dict = {
    '1X2':            {'Home': 1,    'Draw': 2,    'Away': 3},
    'Double Chance':  {'1X': 4,      '12': 5,      'X2': 6},
    'Draw No Bet':    {'Home': 7,    'Away': 8},
    'Asian Handicap': {'Home': 7,    'Away': 8},
    'Over/Under':     {'Over': 9,    'Under': 10},
    'BTTS':           {'Yes': 1170,  'No': 1171},
    'Home/Away':      {'Home': 401,  'Away': 402},  # basketball
    # Tennis 1X2 uses same G=1 but T=1/3 (not T=1/2/3 like football)
    # Detected by sport=tennis context; treated as Home/Away with T=1,3
}
_TENNIS_TYPES = {'Home': 1, 'Away': 3}


def _get_outcome_type(market: str, outcome: str, sport: str = '') -> int:
    """Return the Type code for a given market + outcome combination."""
    # Tennis Home/Away uses different type codes
    if sport == 'tennis' and market in ('Home/Away', '1X2'):
        return _TENNIS_TYPES.get(outcome, 1)

    for fragment, type_map in _OUTCOME_TYPES.items():
        if market.startswith(fragment):
            t = type_map.get(outcome)
            if t is not None:
                return t
    logger.warning(f'[1xBet] Unknown outcome type for market={market!r} outcome={outcome!r}')
    return 1  # fallback to Home


def _get_param(market: str) -> float:
    """Extract the numeric line parameter from the market name (0 if none)."""
    # 'Over/Under 2.5' → 2.5
    # 'Asian Handicap -0.5' → -0.5
    # 'Asian Handicap +1.5' → 1.5
    m = re.search(r'(?<=\s)([+-]?\d+(?:\.\d+)?)\s*$', market)
    if m:
        return float(m.group(1))
    return 0.0


# ── JWT expiry check ──────────────────────────────────────────────────────────

def _jwt_expires_at(token: str) -> Optional[float]:
    """Decode exp claim from a JWT without verification. Returns Unix timestamp or None."""
    try:
        import base64, json as _json
        parts = token.split('.')
        if len(parts) < 2:
            return None
        padded = parts[1] + '=' * (-len(parts[1]) % 4)
        payload = _json.loads(base64.urlsafe_b64decode(padded))
        return float(payload.get('exp', 0))
    except Exception:
        return None


def _token_valid(token: str, min_remaining_secs: float = 300) -> bool:
    """Return True if the token has at least min_remaining_secs left."""
    exp = _jwt_expires_at(token)
    if exp is None:
        return True  # can't determine, assume valid
    remaining = exp - time.time()
    if remaining < min_remaining_secs:
        logger.warning(
            f'[1xBet] Token expires in {remaining:.0f}s '
            f'(< {min_remaining_secs}s). Update ONEXBET_USER_TOKEN in config.py.'
        )
        return remaining > 0
    return True


# ── Driver ────────────────────────────────────────────────────────────────────

class OneXBetDriver(PlacementDriver):
    """
    Placement driver for 1xBet Nigeria.

    Instantiated with credentials from config.py. Tokens must be refreshed
    every ~4 hours. Run the bot, and if you get auth errors, re-capture the
    cookies and update config.py.
    """

    def __init__(
        self,
        user_id:      int,
        user_token:   str,
        access_token: str,
        session_id:   str,
        partner:      int = 159,
        group:        int = 412,
        x_hd:         str = '',    # optional request signature header
    ):
        self.user_id      = int(user_id)
        self.user_token   = user_token
        self.access_token = access_token
        self.session_id   = session_id
        self.partner      = partner
        self.group        = group
        self.x_hd         = x_hd

        self.session = requests.Session()
        self._setup_session()

    @classmethod
    def from_config(cls) -> 'OneXBetDriver':
        """Convenience: build driver from config.py values."""
        import config
        return cls(
            user_id      = config.ONEXBET_USER_ID,
            user_token   = config.ONEXBET_USER_TOKEN,
            access_token = config.ONEXBET_ACCESS_TOKEN,
            session_id   = config.ONEXBET_SESSION,
            partner      = getattr(config, 'ONEXBET_PARTNER', 159),
            group        = getattr(config, 'ONEXBET_GROUP',   412),
            x_hd         = getattr(config, 'ONEXBET_X_HD',    ''),
        )

    def _setup_session(self):
        self.session.headers.update({
            'User-Agent':            'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',
            'Accept':                'application/json, text/plain, */*',
            'Accept-Language':       'en-GB,en-US;q=0.9,en;q=0.8',
            'Origin':                BASE_URL,
            'Referer':               f'{BASE_URL}/en/line/football',
            'x-auth':                f'Bearer {self.user_token}',
            'x-app-n':               '__BETTING_APP__',
            'x-svc-source':          '__BETTING_APP__',
            'x-requested-with':      'XMLHttpRequest',
            'x-mobile-project-id':   '0',
            'is-srv':                'false',
        })
        if self.x_hd:
            self.session.headers['x-hd'] = self.x_hd

        self.session.cookies.update({
            'access_token':       self.access_token,
            'user_token':         self.user_token,
            'SESSION':            self.session_id,
            'authenticated':      '1',
            'application_locale': 'en',
            'lng':                'en',
            'platform_type':      'desktop',
            'fast_coupon':        'true',
            'coefview':           '0',
        })

    # ── Auth ──────────────────────────────────────────────────────────────────

    def login(self) -> bool:
        """
        Tokens are loaded from config — no active login needed.
        Warns if tokens are close to expiry.
        """
        valid = _token_valid(self.user_token)
        if not valid:
            logger.error(
                '[1xBet] User token has expired. '
                'Log into 1xbet.ng, capture fresh cookies, and update config.py:\n'
                '  ONEXBET_USER_TOKEN, ONEXBET_ACCESS_TOKEN, ONEXBET_SESSION'
            )
        return valid

    def get_balance(self) -> Optional[float]:
        """
        Fetch account balance.
        TODO: find the balance endpoint (capture /user/info or similar).
        """
        raise NotImplementedError('[1xBet] Balance endpoint not yet captured.')

    # ── Odds verification ──────────────────────────────────────────────────────

    def verify_odds(
        self,
        event_id:    str,
        outcome_name: str,
        market:      str  = '',
        is_live:     bool = False,
        sport:       str  = 'football',
    ) -> Optional[float]:
        """
        Re-fetch odds for a specific event+outcome to confirm they haven't moved.

        Uses the LineFeed (prematch) or LiveFeed (live) GetEvent-style endpoint.
        Falls back to None if the event can't be found (suspended/finished).
        """
        event_id = parse_numeric_event_id(event_id)
        try:
            if is_live:
                url = f'{BASE_URL}/service-api/LiveFeed/Get1x2_VZip'
                params = {
                    'sports':  '1',
                    'count':   '500',
                    'lng':     'en',
                    'mode':    '1',
                    'country': '132',
                    'partner': str(self.partner),
                }
            else:
                url = f'{BASE_URL}/service-api/LineFeed/Get1x2_VZip'
                params = {
                    'sports':        '1',
                    'count':         '500',
                    'lng':           'en',
                    'mode':          '4',
                    'country':       '132',
                    'partner':       str(self.partner),
                    'getEmpty':      'true',
                    'virtualSports': 'true',
                }

            r = self.session.get(url, params=params, timeout=10)
            r.raise_for_status()
            events = r.json().get('Value', [])

            target_id = str(event_id)
            for ev in events:
                if str(ev.get('I')) != target_id:
                    continue
                # Find the matching outcome in the odds array
                outcome_type = _get_outcome_type(market, outcome_name, sport)
                param        = _get_param(market)
                for entry in ev.get('E', []):
                    if entry.get('T') != outcome_type:
                        continue
                    # For O/U and AH, also check Param matches
                    if param != 0.0:
                        ep = entry.get('P')
                        if ep is None or abs(float(ep) - param) > 1e-9:
                            continue
                    return float(entry.get('C', 0)) or None
            return None  # event not found in feed (may be suspended)

        except Exception as ex:
            logger.warning(f'[1xBet] verify_odds failed for {event_id}: {ex}')
            return None

    # ── Bet placement ──────────────────────────────────────────────────────────

    def place_bet(
        self,
        event_id:     str,
        outcome_name: str,
        odds:         float,
        stake:        float,
        market:       str  = '',
        is_live:      bool = False,
        sport:        str  = 'football',
    ) -> BetResult:
        """
        Place a single bet on 1xBet.

        event_id:     numeric 1xBet game ID (e.g. '711938933')
        outcome_name: 'Home', 'Away', 'Draw', 'Over', 'Under', etc.
        odds:         expected decimal odds
        stake:        amount in NGN
        market:       market name for determining outcome type code
        is_live:      True for in-play, False for prematch
        """
        event_id = parse_numeric_event_id(event_id)
        if not _token_valid(self.user_token, min_remaining_secs=60):
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message='Token expired — update ONEXBET_USER_TOKEN in config.py'
            )

        try:
            game_id      = int(event_id)
        except (ValueError, TypeError):
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=f'Invalid event_id: {event_id!r}'
            )

        outcome_type = _get_outcome_type(market, outcome_name, sport)
        param        = _get_param(market)
        kind         = 1 if is_live else 3

        payload = {
            'UserId': self.user_id,
            'Events': [{
                'GameId':      game_id,
                'Type':        outcome_type,
                'Coef':        round(odds, 3),
                'Param':       param,
                'PV':          None,
                'PlayerId':    0,
                'Kind':        kind,
                'InstrumentId':0,
                'Seconds':     0,
                'Price':       0,
                'Expired':     0,
                'PlayersDuel': [],
            }],
            'Vid':              0,
            'partner':          self.partner,
            'Group':            self.group,
            'live':             is_live,
            'CheckCf':          2,          # accept any odds (even if slightly lower)
            'Lng':              'en',
            'notWait':          True,
            'promo':            None,
            'IsPowerBet':       False,
            'Summ':             int(stake), # 1xBet accepts whole numbers
            'isAutoBet':        True,
            'autoBetCf':        0,
            'TransformEventKind': True,
            'autoBetCfView':    0,
            'Source':           55,
            'OneClickBet':      2,
        }

        if is_live:
            payload['betGUID'] = uuid.uuid4().hex[:24]

        try:
            r = self.session.post(BET_URL, json=payload, timeout=10)
            r.raise_for_status()
            return self._parse_response(r.json(), odds, stake)
        except requests.HTTPError as ex:
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=f'HTTP {ex.response.status_code}: {ex.response.text[:200]}'
            )
        except Exception as ex:
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=str(ex)
            )

    # ── Response parser ───────────────────────────────────────────────────────

    def _parse_response(self, data: dict, expected_odds: float, stake: float) -> BetResult:
        """
        Parse the MakeBetWeb response.

        Confirmed live format:
          {
            "Value": {"Id": 80289047859, "Balance": 462.0,
                      "Coupon": {"Coef": 1.29, ...}, "CanPrint": True},
            "Id": 0,
            "Success": True,
            "Error": "",
            "ErrorCode": 0
          }

        Success when: data["Success"] == True  OR  data["Value"]["Id"] > 0
        Bet ID:       data["Value"]["Id"]
        Actual odds:  data["Value"]["Coupon"]["Coef"]
        Balance:      data["Value"]["Balance"]
        """
        # Primary success check
        if data.get('Success') is True or data.get('ErrorCode') == 0:
            value   = data.get('Value') or {}
            bet_id  = value.get('Id') or value.get('CouponId') or value.get('BetId')
            coupon  = value.get('Coupon') or {}
            actual_odds = float(coupon.get('Coef') or coupon.get('ResultCoef') or expected_odds)
            balance     = value.get('Balance')
            if balance is not None:
                logger.info(f'[1xBet] Balance after bet: ₦{balance:,.2f}')
            logger.info(f'[1xBet] Bet placed: id={bet_id} odds={actual_odds} stake={stake}')
            return BetResult(
                success      = True,
                bet_id       = str(bet_id) if bet_id else None,
                odds_placed  = actual_odds,
                stake_placed = stake,
                message      = 'OK',
            )

        # Fallback: check inner R code (older response format)
        inner  = data.get('Value') or data
        r_code = inner.get('R', inner.get('r'))
        if r_code == 0:
            bet_id = str(inner.get('Id') or inner.get('CouponId') or '')
            return BetResult(
                success=True, bet_id=bet_id or None,
                odds_placed=expected_odds, stake_placed=stake, message='OK',
            )

        # Failed
        err = (data.get('Error') or
               inner.get('Msg') or inner.get('Message') or
               f'ErrorCode={data.get("ErrorCode", "?")}')
        if not err or err == '0':
            err = f'Unexpected response: {str(data)[:200]}'
        logger.warning(f'[1xBet] Bet rejected: {err}')
        return BetResult(
            success=False, bet_id=None, odds_placed=None, stake_placed=None,
            message=err,
        )
