"""
BC.Game sportsbook bet placement driver.

Platform: 442hattrick.com white-label sportsbook
          Accessible at: https://prod{siteId}-dak-{agentId}.442hattrick.com

Auth:
  Two JWTs are required — both obtained simultaneously on login.

  1. BCGAME_TOKEN         — "authorization" header/cookie
     Contains: siteId, agentId, customerId, operatorToken, balance, preferences.
     No explicit exp claim — treated as long-lived; refresh when you get 401 errors.

  2. BCGAME_SESSION_TOKEN — "session" header/cookie
     Contains: customerId, expiredDate (~24 h lifetime), customerIp.

  To refresh:
    1. Log into bc.game in Chrome → open the Sports section
    2. DevTools → Network → filter to 442hattrick.com
    3. Right-click any authenticated request → Copy → Copy as cURL (bash)
    4. python3 tools/update_cookies.py  (paste the cURL)

Placement endpoint:
  POST /api/betslip/bets
  Headers: authorization: <BCGAME_TOKEN>, session: <BCGAME_SESSION_TOKEN>,
           ct-request-id: <uuid4>
  Body: JSON array (single element for single bet)

Event detail (selection IDs + anti-tampering tokens):
  Live:     GET /api/feeds/live/event/{eventId}
  Prematch: GET /api/feeds/event/{eventId}
  These tokens are verified server-side — they MUST come from the API, not be generated.

Odds verification:
  Uses the sptpub.com CDN (shared with the scraper — no auth required).
  Event IDs are the same across both APIs.

Selection ID format (442hattrick.com):
  {marketTypeId}{ML|OU|HC|...}{marketInstanceId}{outcomeCode}
  e.g.  "0ML828595483248525397H"  — 1X2 Home win
"""

import base64
import json as _json
import logging
import time
import uuid
from datetime import datetime, timezone
from fractions import Fraction
from typing import Any, Optional

import requests

from execution.drivers.base import PlacementDriver, BetResult

logger = logging.getLogger(__name__)

# ── Outcome / sport constants ─────────────────────────────────────────────────

# outcome_name → suffix appended to a 442hattrick.com selection ID
_OUTCOME_CODE: dict[str, str] = {
    'Home':  'H',
    'Draw':  'D',
    'Away':  'A',
    'Over':  'O',
    'Under': 'U',
    'Yes':   'Y',
    'No':    'N',
    '1X':    '1X',
    '12':    '12',
    'X2':    'X2',
}

# market_label substring → 442hattrick.com market-type-code fragment in selection ID
_MARKET_TYPE_CODE: dict[str, str] = {
    '1X2':           'ML',
    'Double Chance': 'DC',
    'Draw No Bet':   'DNB',
    'Home/Away':     'ML',
    'BTTS':          'GG',
    # Over/Under → 'OU', Asian Handicap → 'HC'  (handled below by prefix check)
}

_SPORT_IDS: dict[str, str] = {
    'football':   '1',
    'basketball': '2',
    'tennis':     '5',
}

# sptpub.com outcome IDs (mirrors scrapers/bcgame.py MARKET_CONFIG)
_SCRAPER_OUTCOME_IDS: dict[str, dict[str, str]] = {
    '1':   {'Home': '1',   'Draw': '2',   'Away': '3'},
    '10':  {'1X':  '9',   '12':  '10',  'X2':  '11'},
    '18':  {'Over': '12',  'Under': '13'},
    '26':  {'Home': '60',  'Away': '61'},
    '11':  {'Home': '714', 'Away': '715'},
    '29':  {'Yes':  '74',  'No':   '76'},
    '219': {'Home': '4',   'Away': '5'},
    '186': {'Home': '4',   'Away': '5'},
}


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

def _decode_jwt_payload(token: str) -> dict:
    """Decode JWT payload without signature verification."""
    try:
        parts  = token.split('.')
        padded = parts[1] + '=' * (-len(parts[1]) % 4)
        return _json.loads(base64.urlsafe_b64decode(padded))
    except Exception:
        return {}


def _display_odds(decimal_odds: float) -> dict:
    """Convert decimal odds to the multi-format dict the 442hattrick.com API expects."""
    hk = decimal_odds - 1.0
    if decimal_odds >= 2.0:
        malay    = round(1.0 / hk, 4)
        indo     = round(hk, 4)
        american = round(hk * 100.0, 2)
    else:
        malay    = round(hk, 4)
        indo     = round(-1.0 / hk, 4)
        american = round(-100.0 / hk, 2)
    try:
        frac     = Fraction(hk).limit_denominator(1000)
        frac_str = f'{frac.numerator}/{frac.denominator}'
    except Exception:
        frac_str = str(round(hk, 4))
    return {
        'Decimal':    str(round(decimal_odds, 2)),
        'Malay':      str(round(malay, 2)),
        'HK':         str(round(hk, 2)),
        'Indo':       str(round(indo, 2)),
        'American':   str(int(american)),
        'Fractional': frac_str,
    }


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

class BCGameDriver(PlacementDriver):
    """
    Placement driver for BC.Game (442hattrick.com platform).

    verify_odds()  — uses the sptpub.com CDN (shared scraper cache, no auth).
    place_bet()    — fetches selection + anti-tampering tokens from 442hattrick.com,
                     then POSTs to /api/betslip/bets.
    """

    def __init__(self, token: str = '', session_token: str = '', fotd: Optional[int] = None):
        self.token         = token
        self.session_token = session_token
        self._fotd_override = fotd    # None = auto-derive from customerId
        self._scraper      = None     # lazy-init

        # Decode JWT for configuration
        self._jwt          = _decode_jwt_payload(token) if token else {}
        self._site_id      = str(self._jwt.get('siteId',    ''))
        self._agent_id     = str(self._jwt.get('agentID',   ''))
        self._customer_id  = int(self._jwt.get('customerId', 0))
        self._op_token     = self._jwt.get('operatorToken', '')
        self._currency     = self._jwt.get('currencyCode', 'NGN')

        if self._site_id and self._agent_id:
            self._base_url = (
                f'https://prod{self._site_id}-dak-{self._agent_id}.442hattrick.com'
            )
        else:
            self._base_url = ''

        domain = (
            f'prod{self._site_id}-dak-{self._agent_id}.442hattrick.com'
            if self._site_id else '442hattrick.com'
        )

        self._http = requests.Session()
        self._http.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',
            'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
            'Content-Type':    'application/json',
            'Origin':          self._base_url or 'https://bc.game',
            'Referer':         (
                f'{self._base_url}/en/spbkv3/sports/1'
                if self._base_url else 'https://bc.game/'
            ),
        })
        if token:
            self._http.headers['authorization'] = token
            self._http.cookies.set('authorization', token, domain=domain)
            self._http.cookies.set('operatorToken', self._op_token, domain=domain)
            # events_updates cookie = hex(str(customerId))
            self._http.cookies.set(
                'events_updates',
                str(self._customer_id).encode('ascii').hex(),
                domain=domain,
            )
        if session_token:
            self._http.headers['session'] = session_token
            self._http.cookies.set('session', session_token, domain=domain)

    @classmethod
    def from_config(cls) -> 'BCGameDriver':
        import config
        return cls(
            token         = getattr(config, 'BCGAME_TOKEN',         ''),
            session_token = getattr(config, 'BCGAME_SESSION_TOKEN', ''),
            fotd          = getattr(config, 'BCGAME_FOTD',          None),
        )

    def _get_scraper(self):
        if self._scraper is None:
            from scrapers.bcgame import BCGameScraper
            self._scraper = BCGameScraper()
        return self._scraper

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

    def login(self) -> bool:
        missing = []
        if not self.token:
            missing.append('BCGAME_TOKEN')
        if not self.session_token:
            missing.append('BCGAME_SESSION_TOKEN')
        if missing:
            logger.warning(
                f'[BCGame] Missing: {", ".join(missing)}. '
                'verify_odds() works without auth; place_bet() will fail. '
                'Run: python3 tools/update_cookies.py (paste a cURL from bc.game sports).'
            )
            return bool(not missing)

        if not self._base_url:
            logger.warning('[BCGame] Cannot decode siteId/agentId from token — token may be malformed.')
            return False

        # Warn if session token is close to expiry
        sess = _decode_jwt_payload(self.session_token)
        exp_ms = sess.get('expiredDate', 0)
        if exp_ms:
            secs_left = (exp_ms / 1000.0) - time.time()
            if secs_left < 3600:
                logger.warning(f'[BCGame] Session token expires in {secs_left/60:.0f}min — refresh soon.')
            else:
                logger.info(f'[BCGame] Session token valid for {secs_left/3600:.1f}h.')
        return True

    def get_balance(self) -> Optional[float]:
        if not self._base_url or not self.token:
            return None
        try:
            r = self._http.get(f'{self._base_url}/api/customer/balance', timeout=10)
            r.raise_for_status()
            d = r.json()
            for key in ('balance', 'Balance', 'availableBalance', 'available_balance'):
                if d.get(key) is not None:
                    return float(d[key])
        except Exception as ex:
            logger.warning(f'[BCGame] get_balance failed: {ex}')
        return None

    # ── Event detail from 442hattrick.com ─────────────────────────────────────

    def _fetch_event_detail(self, eid: str, is_live: bool) -> Optional[Any]:
        """
        Fetch the event detail JSON from 442hattrick.com.

        Returns the parsed JSON (dict or list), or None on failure.
        The response contains markets/selections with anti-tampering tokens
        (timestamp, intervalTiming) that must be forwarded in the bet payload.
        """
        if not self._base_url:
            return None

        path = f'/api/feeds/live/event/{eid}' if is_live else f'/api/feeds/event/{eid}'

        try:
            r = self._http.get(f'{self._base_url}{path}', timeout=15)
            r.raise_for_status()
            return r.json()
        except requests.HTTPError as ex:
            logger.warning(
                f'[BCGame] Event detail HTTP {ex.response.status_code} for eid={eid}. '
                'If this persists, capture a GET request to the event page in DevTools '
                'and share it to confirm the correct endpoint.'
            )
        except Exception as ex:
            logger.warning(f'[BCGame] Event detail fetch failed for eid={eid}: {ex}')
        return None

    # ── Selection search ──────────────────────────────────────────────────────

    def _collect_selections(self, data: Any, _depth: int = 0) -> list[dict]:
        """
        Recursively collect selection-like objects from the API response.
        A selection is a dict that has an 'id' field plus odds or a timestamp.
        """
        if _depth > 10:
            return []
        results = []
        if isinstance(data, dict):
            has_id    = 'id' in data or 'Id' in data
            has_odds  = any(k in data for k in ('trueOdds', 'TrueOdds', 'odds', 'Odds'))
            has_token = any(k in data for k in ('timestamp', 'Timestamp', 'intervalTiming'))
            if has_id and (has_odds or has_token):
                results.append(data)
            for v in data.values():
                results.extend(self._collect_selections(v, _depth + 1))
        elif isinstance(data, list):
            for item in data:
                results.extend(self._collect_selections(item, _depth + 1))
        return results

    def _find_selection(
        self,
        event_data:   Any,
        outcome_name: str,
        market_label: str,
        eid:          str,
    ) -> Optional[dict]:
        """
        Search the 442hattrick.com event response for a selection matching
        outcome_name + market_label. Returns a normalised selection dict or None.

        Selection IDs look like: "0ML828595483248525397H"
          market_type_code (e.g. "0ML") + market_instance_id + outcome_code ("H")
        """
        outcome_code = _OUTCOME_CODE.get(outcome_name)
        if not outcome_code:
            logger.warning(f'[BCGame] No outcome code for {outcome_name!r}')
            return None

        # Determine the market type code to look for in the selection ID
        mkt_code = _MARKET_TYPE_CODE.get(market_label, '')
        if not mkt_code:
            lbl = market_label.lower()
            if 'over' in lbl or 'under' in lbl or 'total' in lbl:
                mkt_code = 'OU'
            elif 'handicap' in lbl or 'asian' in lbl:
                mkt_code = 'HC'

        all_sels = self._collect_selections(event_data)
        logger.debug(f'[BCGame] Found {len(all_sels)} selection-like objects in event {eid} response')

        def _matches(sel: dict) -> bool:
            sel_id = str(sel.get('id') or sel.get('Id') or '')
            if not sel_id.endswith(outcome_code):
                return False
            if mkt_code and mkt_code.upper() not in sel_id.upper():
                return False
            return True

        candidates = [s for s in all_sels if _matches(s)]

        if not candidates:
            # Fall back: match only by outcome code suffix, ignore market type
            candidates = [
                s for s in all_sels
                if str(s.get('id') or s.get('Id') or '').endswith(outcome_code)
            ]
            if candidates:
                logger.debug(
                    f'[BCGame] market_type_code {mkt_code!r} not found in selection IDs; '
                    'matched by outcome suffix only'
                )

        if not candidates:
            logger.warning(
                f'[BCGame] No selection found: outcome={outcome_name!r} '
                f'market={market_label!r} eid={eid}. '
                f'Total objects scanned: {len(all_sels)}. '
                'The event API response may have a different structure — '
                'capture a GET request in DevTools and share it for debugging.'
            )
            return None

        # Prefer selections that already have timestamp tokens (live market data)
        with_tokens = [
            c for c in candidates
            if c.get('timestamp') or c.get('Timestamp') or c.get('intervalTiming')
        ]
        chosen = with_tokens[0] if with_tokens else candidates[0]

        sel_id = str(chosen.get('id') or chosen.get('Id') or '')
        mkt_id = str(chosen.get('marketId') or chosen.get('MarketId') or '')
        if not mkt_id and sel_id.endswith(outcome_code):
            # Derive by stripping outcome suffix
            mkt_id = sel_id[: -len(outcome_code)]

        odds_raw = (
            chosen.get('trueOdds') or chosen.get('TrueOdds') or
            chosen.get('odds')     or chosen.get('Odds')
        )
        timestamp = (
            chosen.get('timestamp') or chosen.get('Timestamp') or ''
        )
        interval = (
            chosen.get('intervalTiming') or chosen.get('IntervalTiming') or ''
        )
        max_stake = chosen.get('maxStake') or chosen.get('MaxStake') or 10_000_000
        min_stake = chosen.get('minStake') or chosen.get('MinStake') or 100
        sel_name  = (
            chosen.get('selectionName') or chosen.get('name') or
            chosen.get('Name') or outcome_name
        )

        if not timestamp:
            logger.warning(
                f'[BCGame] Selection {sel_id!r} found but has no timestamp token. '
                'Bet may be rejected. Ensure the event detail endpoint is live data.'
            )

        return {
            'selection_id':    sel_id,
            'market_id':       mkt_id,
            'event_id':        eid,
            'odds':            float(odds_raw) if odds_raw is not None else None,
            'timestamp':       timestamp,
            'interval_timing': interval,
            'max_stake':       max_stake,
            'min_stake':       min_stake,
            'selection_name':  sel_name,
        }

    # ── Odds verification (sptpub.com CDN — no auth) ──────────────────────────

    def verify_odds(
        self,
        event_id:     str,
        outcome_name: str,
        market:       str  = '',
        is_live:      bool = False,
        sport:        str  = 'football',
    ) -> Optional[float]:
        """
        Return current odds via the sptpub.com CDN (same data source as the scraper).
        No auth required. event_id must be in bcg_{eid}_{mkt_id}[_{line}] format.
        """
        try:
            return self._odds_from_scraper(event_id, outcome_name, is_live)
        except Exception as ex:
            logger.warning(f'[BCGame] verify_odds error for {event_id}: {ex}')
            return None

    def _odds_from_scraper(
        self, event_id: str, outcome_name: str, is_live: bool
    ) -> Optional[float]:
        parts = event_id.split('_')
        if len(parts) < 3 or parts[0] != 'bcg':
            return None
        eid, mkt_id = parts[1], parts[2]
        line = '_'.join(parts[3:]) if len(parts) > 3 else None

        outcome_id = _SCRAPER_OUTCOME_IDS.get(mkt_id, {}).get(outcome_name)
        if not outcome_id:
            return None

        if mkt_id == '18' and line:
            spec = f'total={line}'
        elif mkt_id == '11' and line:
            try:
                val  = float(line)
                spec = f'hcp={val:g}'
            except ValueError:
                spec = f'hcp={line}'
        else:
            spec = ''

        scraper = self._get_scraper()
        try:
            raw, _, _ = (
                scraper._fetch_all_live_events() if is_live
                else scraper._fetch_all_events()
            )
        except Exception as ex:
            logger.warning(f'[BCGame] scraper fetch error: {ex}')
            return None

        ev = raw.get(eid)
        if not ev:
            return None
        try:
            odds = float(ev['markets'][mkt_id][spec][outcome_id]['k'])
            return odds if odds > 1.0 else None
        except (KeyError, TypeError, ValueError):
            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 BC.Game via the 442hattrick.com platform.

        event_id:     scraped event_id in bcg_{eid}_{mkt_id}[_{line}] format
        outcome_name: 'Home', 'Away', 'Draw', 'Over', 'Under', 'Yes', 'No', '1X', '12', 'X2'
        odds:         expected decimal odds (verified before placement)
        stake:        NGN amount (rounded to whole number)
        """
        if not self.token or not self.session_token:
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=(
                    'Missing BCGAME_TOKEN or BCGAME_SESSION_TOKEN. '
                    'Run: python3 tools/update_cookies.py'
                ),
            )
        if not self._base_url:
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message='Cannot derive base URL from token — siteId/agentId missing.',
            )

        parts = event_id.split('_')
        if len(parts) < 3 or parts[0] != 'bcg':
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=f'Invalid event_id format: {event_id!r}',
            )
        eid = parts[1]

        # Fetch event data from 442hattrick.com for selection IDs + anti-tampering tokens
        event_data = self._fetch_event_detail(eid, is_live)
        if event_data is None:
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=f'Event detail fetch failed for eid={eid} is_live={is_live}',
            )

        sel = self._find_selection(event_data, outcome_name, market, eid)
        if not sel:
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=f'Selection not found: {outcome_name!r} market={market!r} eid={eid}',
            )

        live_odds = sel['odds'] if sel['odds'] else odds
        if sel['odds'] and abs(live_odds - odds) / odds > 0.05:
            logger.warning(
                f'[BCGame] Odds drift: expected {odds}, feed shows {live_odds}'
            )

        stake_int = int(stake)
        display   = _display_odds(live_odds)
        sport_id  = _SPORT_IDS.get(sport, '1')
        now_iso   = (
            datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
        )
        req_id    = str(uuid.uuid4())

        payload = [{
            'betName': 'single bet',
            'type':    'single',
            'count':   stake_int,
            'selectionsMapped': [{
                'id':             sel['selection_id'],
                'trueOdds':       live_odds,
                'displayOdds':    display,
                'marketId':       sel['market_id'],
                'eventId':        sel['event_id'],
                'timestamp':      sel['timestamp'],
                'intervalTiming': sel['interval_timing'],
                'promotionIds':   [],
            }],
            'trueOdds':       live_odds,
            'displayOdds':    display,
            'clientOdds':     str(live_odds),
            'comboSize':      0,
            'isLive':         is_live,
            'numberOfLines':  1,
            'maxStake':       sel['max_stake'],
            'minStake':       sel['min_stake'],
            'numberOfBets':   1,
            'oddStyleID':     '1',
            'sportID':        sport_id,
            'feRequestTime':  now_iso,
            'metaData': {
                'device':             'desktop',
                'isTablet':           False,
                'bettingView':        'South America View',
                'balancePriority':    1,
                'fullURL':            f'{self._base_url}/en/spbkv3/sports/{sport_id}',
                'userAgent':          (
                    'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
                    '(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36'
                ),
                'shareBetSlipCode':   '',
                'refererDomain':      'bc-game',
                'siteOption':         True,
                'featuredSelections': [],
                'isQuickBet':         False,
            },
            'selectionsNames':  [
                {'id': sel['selection_id'], 'selectionName': sel['selection_name']}
            ],
            'selectionsPlaced': [sel['selection_id']],
            'stake':            stake_int,
            'potentialReturns': round(stake_int * live_odds, 2),
            'freeBet': {'id': 0, 'amount': 0, 'gainDecimal': 0, 'isRiskFreeBet': False},
            'calculationSettings': {
                'useNewCalculationSettings': True,
                'oddsRoundingMode':          0,
                'gainRoundingMode':          1,
                'roundCombinationOdds':      False,
            },
            # fotd: customer-specific session value close to customerId.
            # The exact formula is unknown; BCGAME_FOTD in config.py overrides the
            # auto-derived value.  If bets are rejected, check the exact value in
            # a fresh DevTools cURL and set BCGAME_FOTD in config.py.
            'fotd': (
                self._fotd_override
                if self._fotd_override is not None
                else self._customer_id
            ),
            'tags': [],
        }]

        bet_url = f'{self._base_url}/api/betslip/bets'
        try:
            r = self._http.post(
                bet_url,
                data=_json.dumps(payload, separators=(',', ':')),
                headers={
                    'ct-request-id': req_id,
                    'session':       self.session_token,
                },
                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),
            )

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

    def _parse_response(self, data: Any, expected_odds: float, stake: float) -> BetResult:
        """
        Parse POST /api/betslip/bets response.
        Response is a JSON array — one element per bet submitted.
        """
        items = data if isinstance(data, list) else [data]

        for item in items:
            if not isinstance(item, dict):
                continue

            # Error check
            err_code = (
                item.get('errorCode')  or item.get('error_code') or
                item.get('statusCode') or item.get('status')
            )
            err_msg = (
                item.get('errorMessage') or item.get('message') or
                item.get('error')        or item.get('Error')
            )
            if err_code and str(err_code) not in ('0', '200', 'null', 'None', ''):
                logger.warning(f'[BCGame] Bet rejected: code={err_code} msg={err_msg}')
                return BetResult(
                    success=False, bet_id=None, odds_placed=None, stake_placed=None,
                    message=str(err_msg or f'errorCode={err_code}'),
                )

            # Successful response — look for a bet/slip ID
            bet_id = str(
                item.get('betId')    or item.get('BetId')    or
                item.get('id')       or item.get('Id')       or
                item.get('slipId')   or item.get('SlipId')   or
                item.get('couponId') or item.get('CouponId') or ''
            )
            if bet_id and bet_id not in ('', 'None', '0'):
                logger.info(
                    f'[BCGame] Bet placed: id={bet_id} '
                    f'odds={expected_odds} stake={stake}'
                )
                return BetResult(
                    success=True, bet_id=bet_id,
                    odds_placed=expected_odds, stake_placed=stake, message='OK',
                )

        logger.warning(f'[BCGame] Ambiguous response (no bet ID found): {str(data)[:300]}')
        return BetResult(
            success=False, bet_id=None, odds_placed=None, stake_placed=None,
            message=f'No bet ID in response: {str(data)[:200]}',
        )
