"""
BetKing Nigeria bet placement driver.

Endpoint:
  POST https://www.betking.com/api/sports/v1/bet/InsertCoupon

Authentication:
  Authorization: Bearer <JWT>   (header)
  inbehalfof:    <user_id>      (header — must match JWT sub claim)
  ARRAffinity / ARRAffinitySameSite cookies (Azure sticky-session; optional but recommended)

JWT lifetime:
  ~21 days. Set BETKING_AUTHORIZATION in config.py.
  Update by capturing any authenticated request from DevTools on betking.com.

Market TypeIds (same as scraper):
  Football:   110=1X2, 146=Double Chance, 147=Draw No Bet, 160=O/U, 302=BTTS
  Basketball: 9300=Moneyline (Home/Away)
  Tennis:     9388=Home/Away

Selection name → outcome label (used in payload SelectionName):
  1X2:         '1'=Home, 'X'=Draw, '2'=Away
  Double Chance: '1X'='1X', 'X2'='X2', '12'='12'
  Draw No Bet: '1 DNB'=Home, '2 DNB'=Away
  Over/Under:  'Over'=Over, 'Under'=Under  (SpecialValue = 1.5 / 2.5 / 3.5)
  BTTS:        'GG'=Yes, 'NG'=No
  Basketball/Tennis: '1'=Home, '2'=Away

How place_bet works:
  1. Fetches /api/feeds/live/{match_id}/en to obtain SelectionId, MarketId, TournamentId, etc.
  2. Builds the full InsertCoupon JSON body
  3. POSTs to the placement endpoint
"""
import logging
import re
import time
from typing import Optional

import requests

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

logger = logging.getLogger(__name__)

BET_URL       = 'https://www.betking.com/api/sports/v1/bet/InsertCoupon'
LIVE_FEED_URL = 'https://sportsapicdn-desktop.betking.com/api/feeds/live'
PREMATCH_FEED_URL = 'https://sportsapicdn-desktop.betking.com/api/feeds/prematch'
BASE_SITE_URL = 'https://www.betking.com'

# ── Market / selection mappings ───────────────────────────────────────────────

_FOOTBALL_MARKETS = {
    110: ('1X2',           {'Home': '1',     'Draw': 'X',   'Away': '2'}),
    146: ('Double Chance', {'1X': '1X',      'X2': 'X2',    '12': '12'}),
    147: ('Draw No Bet',   {'Home': '1 DNB', 'Away': '2 DNB'}),
    160: ('Over/Under',    {'Over': 'Over',  'Under': 'Under'}),
    302: ('BTTS',          {'Yes': 'GG',     'No': 'NG'}),
}
_BASKETBALL_MARKETS = {
    9300: ('Home/Away', {'Home': '1', 'Away': '2'}),
}
_TENNIS_MARKETS = {
    9388: ('Home/Away', {'Home': '1', 'Away': '2'}),
}
_SPORT_MARKETS = {
    'football':   _FOOTBALL_MARKETS,
    'basketball': _BASKETBALL_MARKETS,
    'tennis':     _TENNIS_MARKETS,
}
_SPORT_IDS = {'football': 1, 'basketball': 2, 'tennis': 5}
_SPORT_NAMES = {'football': 'Football', 'basketball': 'Basketball', 'tennis': 'Tennis'}


def _get_type_id(market: str, sport: str = 'football') -> Optional[int]:
    sport_map = _SPORT_MARKETS.get(sport, _FOOTBALL_MARKETS)
    for type_id, (market_name, _) in sport_map.items():
        if market.startswith(market_name):
            return type_id
    return None


def _get_selection_name(market: str, outcome: str, sport: str = 'football') -> Optional[str]:
    type_id = _get_type_id(market, sport)
    if type_id is None:
        return None
    _, outcome_map = _SPORT_MARKETS.get(sport, _FOOTBALL_MARKETS)[type_id]
    return outcome_map.get(outcome)


def _get_ou_line(market: str) -> Optional[float]:
    m = re.search(r'(\d+\.?\d*)$', market)
    if m:
        try:
            return float(m.group(1))
        except ValueError:
            pass
    return None


def _jwt_user_id(token: str) -> Optional[str]:
    """Extract the 'sub' (user ID) claim from a JWT without verification."""
    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 str(payload.get('sub', ''))
    except Exception:
        return None


def _jwt_expires_at(token: str) -> Optional[float]:
    try:
        import base64, json as _json
        parts = token.split('.')
        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:
    exp = _jwt_expires_at(token)
    if exp is None:
        return True
    remaining = exp - time.time()
    if remaining < min_remaining_secs:
        logger.warning(
            f'[BetKing] Token expires in {remaining:.0f}s '
            f'(< {min_remaining_secs}s). Update BETKING_AUTHORIZATION in config.py.'
        )
        return remaining > 0
    return True


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

class BetKingDriver(PlacementDriver):
    """
    Placement driver for BetKing Nigeria.

    Before placing a bet the driver re-fetches the live event detail
    to obtain SelectionId, MarketId and TournamentId — required by InsertCoupon.
    This costs one extra HTTP call but guarantees the payload is correct.
    """

    def __init__(
        self,
        authorization:      str,   # Bearer JWT from Authorization header
        user_id:            str = '',  # derived from JWT sub if empty
        arr_affinity:       str = '',  # ARRAffinity cookie (sticky-session)
        arr_affinity_same:  str = '',  # ARRAffinitySameSite cookie
    ):
        self.authorization     = authorization
        self.arr_affinity      = arr_affinity
        self.arr_affinity_same = arr_affinity_same

        # Derive user_id from JWT if not provided
        self.user_id = user_id or _jwt_user_id(authorization) or ''

        self.session = requests.Session()
        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-US,en;q=0.9',
            'Origin':          BASE_SITE_URL,
            'Content-Type':    'application/json',
        })
        self._apply_auth()

    def _apply_auth(self):
        if self.authorization:
            self.session.headers['authorization'] = f'Bearer {self.authorization}'
        if self.user_id:
            self.session.headers['inbehalfof'] = self.user_id
        if self.arr_affinity:
            self.session.cookies.set('ARRAffinity', self.arr_affinity)
            self.session.cookies.set('ARRAffinitySameSite', self.arr_affinity_same or self.arr_affinity)

    @classmethod
    def from_config(cls) -> 'BetKingDriver':
        import config
        return cls(
            authorization     = getattr(config, 'BETKING_AUTHORIZATION',      ''),
            user_id           = getattr(config, 'BETKING_USER_ID',            ''),
            arr_affinity      = getattr(config, 'BETKING_ARR_AFFINITY',       ''),
            arr_affinity_same = getattr(config, 'BETKING_ARR_AFFINITY_SAME',  ''),
        )

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

    def login(self) -> bool:
        if not self.authorization:
            logger.warning(
                '[BetKing] No authorization token. '
                'Capture any request on betking.com and set '
                'BETKING_AUTHORIZATION in config.py.'
            )
            return False
        if not self.user_id:
            logger.warning('[BetKing] Could not derive user_id from JWT.')
            return False
        valid = _token_valid(self.authorization)
        if not valid:
            logger.error('[BetKing] Token expired. Update BETKING_AUTHORIZATION in config.py.')
        else:
            exp = _jwt_expires_at(self.authorization)
            days = (exp - time.time()) / 86400 if exp else 0
            logger.info(f'[BetKing] Session ready. user_id={self.user_id}, token expires in {days:.1f}d')
        return valid

    def get_balance(self) -> Optional[float]:
        raise NotImplementedError('[BetKing] Balance endpoint not yet captured.')

    # ── Event detail fetch ────────────────────────────────────────────────────

    def _fetch_event_detail(self, match_id: str, is_live: bool) -> Optional[dict]:
        """Fetch full event data containing SelectionId, MarketId, TournamentId."""
        try:
            if is_live:
                url = f'{LIVE_FEED_URL}/{match_id}/en'
            else:
                url = f'{PREMATCH_FEED_URL}/event/{match_id}/en'
            r = self.session.get(url, timeout=10)
            r.raise_for_status()
            return r.json()
        except Exception as ex:
            logger.warning(f'[BetKing] event detail fetch failed for {match_id}: {ex}')
            return None

    def _find_selection(
        self,
        data:           dict,
        type_id:        int,
        selection_name: str,
        ou_line:        Optional[float],
        outer_match_id: str,
    ) -> Optional[dict]:
        """
        Scan the event detail response for the matching market + selection.
        Returns a dict with all IDs and metadata needed for InsertCoupon.
        """
        for tournament in data.get('Tournaments', []):
            t_id   = tournament.get('Id',   0)
            t_name = tournament.get('Name', '')
            for event in tournament.get('Events', []):
                ev_id       = event.get('Id', int(outer_match_id) if outer_match_id.isdigit() else 0)
                match_name  = event.get('Name', '')
                event_date  = event.get('Date') or event.get('StartDate') or ''
                for market in event.get('Markets', []):
                    if market.get('TypeId') != type_id:
                        continue
                    if market.get('Status', 1) == 0:
                        continue
                    sv = market.get('SpecialValue')
                    if ou_line is not None:
                        try:
                            if sv is None or abs(float(sv) - ou_line) > 1e-9:
                                continue
                        except (TypeError, ValueError):
                            continue
                    for sel in market.get('Selections', []):
                        if sel.get('Name') != selection_name:
                            continue
                        odds_list = sel.get('Odds', [])
                        if not odds_list:
                            continue
                        odd = odds_list[0]
                        if odd.get('Status', 1) == 0:
                            return None  # selection suspended
                        try:
                            live_odds = float(odd.get('Value', 0))
                        except (TypeError, ValueError):
                            continue
                        return {
                            'tournament_id':   t_id,
                            'tournament_name': t_name,
                            'event_id':        ev_id,
                            'match_name':      match_name,
                            'event_date':      event_date,
                            'market_id':       market.get('Id', 0),
                            'market_name':     market.get('Name', ''),
                            'selection_id':    sel.get('Id', 0),
                            'selection_name':  sel.get('Name', ''),
                            'sel_type_id':     sel.get('TypeId', sel.get('IDType', 5)),
                            'odd_id':          odd.get('Id', 0),
                            'live_odds':       live_odds,
                            'special_value':   sv,
                        }
        return None

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

    def verify_odds(
        self,
        event_id:     str,
        outcome_name: str,
        market:       str  = '',
        is_live:      bool = False,
        sport:        str  = 'football',
    ) -> Optional[float]:
        event_id       = parse_numeric_event_id(event_id)
        type_id        = _get_type_id(market, sport)
        selection_name = _get_selection_name(market, outcome_name, sport)
        ou_line        = _get_ou_line(market) if 'Over/Under' in market else None

        if type_id is None or selection_name is None:
            logger.warning(f'[BetKing] Cannot map market={market!r} outcome={outcome_name!r}')
            return None

        data = self._fetch_event_detail(event_id, is_live)
        if not data:
            return None

        sel_info = self._find_selection(data, type_id, selection_name, ou_line, event_id)
        return sel_info['live_odds'] if sel_info else 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 BetKing.

        event_id:     numeric BetKing match ID
        outcome_name: 'Home', 'Away', 'Draw', 'Over', 'Under', etc.
        odds:         expected decimal odds
        stake:        amount in NGN (will be rounded to whole number)
        """
        if not _token_valid(self.authorization, min_remaining_secs=60):
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message='Token expired — update BETKING_AUTHORIZATION in config.py'
            )

        event_id       = parse_numeric_event_id(event_id)
        type_id        = _get_type_id(market, sport)
        selection_name = _get_selection_name(market, outcome_name, sport)
        ou_line        = _get_ou_line(market) if 'Over/Under' in market else None

        if type_id is None or selection_name is None:
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=f'Cannot map market={market!r} outcome={outcome_name!r} sport={sport!r}'
            )

        # Fetch event detail to get SelectionId, MarketId, TournamentId
        data = self._fetch_event_detail(event_id, is_live)
        if not data:
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=f'Could not fetch event detail for match_id={event_id}'
            )

        sel_info = self._find_selection(data, type_id, selection_name, ou_line, event_id)
        if not sel_info:
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=f'Selection not found or suspended: {outcome_name} @ {market} (event {event_id})'
            )

        # Warn if odds have moved significantly
        live_odds = sel_info['live_odds']
        if abs(live_odds - odds) / odds > 0.05:
            logger.warning(
                f'[BetKing] Odds drift: expected {odds}, live feed shows {live_odds}'
            )

        stake_int = int(stake)
        min_win   = round(stake_int * live_odds)
        odds_val  = round(live_odds, 2)

        # Unique request transaction ID — timestamp-based
        request_txn_id = f"{int(time.time() * 1000)}{str(self.user_id)[-7:]}"

        payload = self._build_payload(
            match_id       = event_id,
            sel_info       = sel_info,
            type_id        = type_id,
            odds_val       = odds_val,
            stake_int      = stake_int,
            min_win        = min_win,
            is_live        = is_live,
            sport          = sport,
            request_txn_id = request_txn_id,
        )

        try:
            r = self.session.post(
                BET_URL,
                json=payload,
                headers={'Referer': f'{BASE_SITE_URL}/live/l' if is_live else f'{BASE_SITE_URL}/sports/s/football/'},
                timeout=15,
            )
            r.raise_for_status()
            return self._parse_response(r.json(), live_odds, stake_int)
        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[:300]}'
            )
        except Exception as ex:
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=str(ex)
            )

    # ── Payload builder ───────────────────────────────────────────────────────

    def _build_payload(
        self,
        match_id:       str,
        sel_info:       dict,
        type_id:        int,
        odds_val:       float,
        stake_int:      int,
        min_win:        int,
        is_live:        bool,
        sport:          str,
        request_txn_id: str,
    ) -> dict:
        sport_id   = _SPORT_IDS.get(sport, 1)
        sport_name = _SPORT_NAMES.get(sport, 'Football')
        match_id_i = int(match_id) if match_id.isdigit() else 0

        _zero_ucl = {
            'MaxStake': 0, 'MaxLoss': 0, 'MaxStakeMulti': 0, 'MaxLossMulti': 0,
            'UserFixedRiskValue': 1, 'UserLiveRiskValue': 1,
            'UserLiveDelayMultiplier': 1,
            'UserMaxStake': 0, 'UserMaxLoss': 0,
            'UserMaxStakeMulti': 0, 'UserMaxLossMulti': 0,
            'UserLiveEnableAggregatedLimits': True,
            'UserFixedEnableAggregatedLimits': True,
            'UserAllowSplitBets': False,
        }
        _zero_risk = {
            'IDLimitItemType': 0, 'IDChannel': 0,
            'SingleEvalStakeLimit': 0, 'SingleEvalLossLimit': 0,
            'MultiEvalStakeLimit': 0, 'MultiEvalLossLimit': 0,
            'SingleMaxCouponStakeLimit': 0, 'SingleMaxCouponLossLimit': 0,
            'MultiMaxCouponStakeLimit': 0, 'MultiMaxCouponLossLimit': 0,
            'MaxCombinability': 0, 'LimitCategoryType': 0,
        }

        grouping_entry = {
            'MaxBonusForUnit': 0, 'MinBonusForUnit': 0,
            'MaxBonusPerc': 0, 'MinBonusPerc': 0,
            'MinEventForBonus': 0,
            'MinWin': min_win, 'MaxWin': min_win,
            'MinBonus': 0, 'MaxBonus': 0,
            'Grouping': 1, 'Combinations': 1,
            'Stake': stake_int, 'NetStake': stake_int,
            'NetMinWin': min_win, 'NetMaxWin': min_win,
            'NetStakeMaxWin': min_win, 'NetStakeMinWin': min_win,
        }

        return {
            'BetCoupon': {
                'IsClientSideCoupon':  False,
                'IsFreeBet':           False,
                'IsFlexiCut':          False,
                'PopUpUrl':            None,
                'UserId':              str(self.user_id),
                'CodeZoneProfileId':   None,
                'Username':            None,
                'OriginatorId':        None,
                'OriginatorUsername':  None,
                'CouponTypeId':        1,
                'MinWin':              min_win,
                'MaxWin':              min_win,
                'MinBonus':            0,
                'MaxBonus':            0,
                'MaxBonusForUnit':     0,
                'MinBonusForUnit':     0,
                'MaxBonusPerc':        0,
                'MinBonusPerc':        0,
                'MinEventForBonus':    0,
                'MinOdd':              odds_val,
                'MaxOdd':              odds_val,
                'TotalOdds':           odds_val,
                'Stake':               stake_int,
                'UseGroupsStake':      False,
                'StakeGross':          stake_int,
                'StakeTaxed':          0,
                'TaxPercentage':       0,
                'Tax':                 0,
                'MinWinNet':           min_win,
                'MaxWinNet':           min_win,
                'NetStakeMaxWin':      min_win,
                'NetStakeMinWin':      min_win,
                'MinWithholdingTax':   0,
                'MaxWithholdingTax':   0,
                'TurnoverTax':         0,
                'TotalCombinations':   1,
                'Odds': [{
                    'IsBetBuilder':        False,
                    'TournamentId':        sel_info['tournament_id'],
                    'TournamentName':      sel_info['tournament_name'],
                    'MatchId':             match_id_i,
                    'MatchName':           sel_info['match_name'],
                    'EventId':             sel_info['event_id'],
                    'ParentEventID':       match_id_i,
                    'ProviderEventId':     None,
                    'EventName':           sport_name,
                    'HomeTeam':            None,
                    'AwayTeam':            None,
                    'IDHomeTeam':          0,
                    'IDAwayTeam':          0,
                    'SmartCode':           0,
                    'SpecialValue':        sel_info['special_value'],
                    'OddId':               sel_info['odd_id'],
                    'GamePlay':            1 if is_live else 0,
                    'GamePlayDescription': None,
                    'SelectionId':         sel_info['selection_id'],
                    'SelectionName':       sel_info['selection_name'],
                    'SelectionShortcut':   None,
                    'MarketId':            sel_info['market_id'],
                    'MarketName':          sel_info['market_name'],
                    'MarketTypeId':        type_id,
                    'IDSelectionType':     sel_info['sel_type_id'],
                    'OddValue':            odds_val,
                    'UnboostedOddValue':   None,
                    'ConfirmedOddValue':   odds_val,
                    'AllowFixed':          True,
                    'Fixed':               False,
                    'EventCategory':       'L' if is_live else 'S',
                    'EventTypeId':         0,
                    'UserCouponLimit':     _zero_ucl,
                    'RiskLimit':           _zero_risk,
                    'LiveSportDelay':      0,
                    'OnlyRegularTime':     False,
                    'SelectionNoWinValues': [],
                    'CompatibilityLevel':  0,
                    'IDGroup':             0,
                    'EventDate':           sel_info['event_date'],
                    'IDSport':             sport_id,
                    'SportName':           sport_name,
                    'AAMSMatchID':         None,
                    'AAMSTeamID':          None,
                    'AAMSSelectionID':     None,
                    'Outright':            0,
                    'Spread':              0,
                    'Expired':             None,
                    'NewPrice':            None,
                    'Banker':              'No',
                    'DataBanker':          'Sí',
                }],
                'Groupings':                [grouping_entry],
                'PossibleMissingGroupings': [],
                'CurrencyId':              16,   # NGN
                'ChannelId':               36,   # web
                'ChannelName':             None,
                'IsLive':                  is_live,
                'IsVirtual':               False,
                'IsPokerPool':             False,
                'MatchsMarketsCompatibilities': None,
                'UserCouponLimit':         _zero_ucl,
                'Outcome':                 0,
                'OutcomeInfo':             [],
                'CurrentEvalMotivation':   0,
                'CurrentEvalReason':       0,
                'LiveDelay':               0,
                'Language':                'en',
                'BravoCardPayment':        None,
                'AcceptedOn':              '0001-01-01T00:00:00+00:00',
                'HashValue':               None,
                'MaxStake':                0,
                'IsEligibleForStakeBack':  False,
                'BetDetails':              {'FreeBetDetails': None, 'FlexiCutDetails': None},
                'BetCouponGlobalVariable': {
                    'BonusMultiplier':      1,
                    'AccumBonus':           0,
                    'StakeReduction':       0,
                    'StakeReductionFactor': 0,
                    'CashReductionFactor':  0,
                },
                'CouponCode':              None,
                'UseNewSettlement':        False,
                '_':                       False,
                'HasLive':                 is_live,
                'CouponType':              'Single',
                'AllGroupings': [{
                    **grouping_entry,
                    'IsChecked': True,
                    'ShowStake': stake_int,
                }],
                'ConfirmedGroupings': [{
                    **grouping_entry,
                    'IsChecked': True,
                    'ShowStake': stake_int,
                }],
                'GroupingsStatus':   'none',
                'StakeGrossValue':   stake_int,
                'IsModifyOdds':      True,
                'IsStakeReduction':  False,
                'IsAllowTransfer':   False,
                'AgentUser':         str(self.user_id),
                'IsLoggedIn':        True,
            },
            'AllowOddChanges':          True,
            'AllowStakeReduction':      False,
            'TransferStakeFromAgent':   False,
            'RequestTransactionId':     request_txn_id,
            'BookedCouponCode':         None,
        }

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

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

        Success indicators observed:
          data['BetCoupon']['CouponCode']  — non-null coupon reference
          data['Success'] == True          — explicit success flag

        Error indicators:
          data['ErrorCode'] / data['Error'] / data['Message']
          data['BetCoupon']['Outcome'] != 0
        """
        if not isinstance(data, dict):
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=f'Unexpected response: {str(data)[:200]}'
            )

        # Try explicit success flag first
        if data.get('Success') is True:
            coupon  = data.get('BetCoupon', {})
            bet_id  = coupon.get('CouponCode') or coupon.get('CouponId') or data.get('CouponCode')
            actual_odds = expected_odds
            if coupon.get('TotalOdds'):
                try:
                    actual_odds = float(coupon['TotalOdds'])
                except (TypeError, ValueError):
                    pass
            logger.info(f'[BetKing] 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',
            )

        # Check inner BetCoupon for CouponCode as fallback success indicator
        coupon = data.get('BetCoupon', {})
        if isinstance(coupon, dict):
            code = coupon.get('CouponCode')
            if code and coupon.get('Outcome', -1) == 0:
                logger.info(f'[BetKing] Bet placed: id={code} odds={expected_odds} stake={stake}')
                return BetResult(
                    success=True,
                    bet_id=str(code),
                    odds_placed=expected_odds,
                    stake_placed=stake,
                    message='OK',
                )

        # Failed — extract error message
        err = (
            data.get('Error') or
            data.get('ErrorMessage') or
            data.get('Message') or
            (coupon.get('OutcomeInfo') if isinstance(coupon, dict) else None) or
            f'ErrorCode={data.get("ErrorCode", "?")}'
        )
        if isinstance(err, list):
            err = '; '.join(str(e) for e in err)
        if not err or err in ('0', 'None'):
            err = f'Unexpected response: {str(data)[:200]}'

        logger.warning(f'[BetKing] Bet rejected: {err}')
        return BetResult(
            success=False, bet_id=None, odds_placed=None, stake_placed=None,
            message=str(err),
        )
