"""
Accessbet Nigeria bet placement driver.

Platform: Beto/Zapnet white-label sportsbook

Endpoint:
  POST https://accessbet.com/api/endpoint
  Content-Type: text/plain   (JSON body, but server expects text/plain)

Authentication:
  Cookie: USERSESSION=<session_id>
  No Bearer token required.
  Session persists until logout — update ACCESSBET_SESSION in config.py
  by capturing any authenticated request from DevTools.

Payload (JSON-RPC over HTTP — same protocol as the WebSocket feed):
  {
    "headers": {},
    "requestMethod": "betting.betslip.place",
    "requestArguments": {
      "slipData": {
        "acceptbet": true,
        "auth": true,
        "type": "match",
        "perline": false,
        "matches": [{
          "id":      "<match_id>",        -- from WebSocket match.id
          "banker":  false,
          "group":   null,
          "code":    "<match_code>",      -- from WebSocket match.code
          "selections": [{
            "id":           "<sel_id>",        -- from WebSocket sel.id
            "marketId":     "<market_id>",     -- from WebSocket market.id
            "marketTypeId": <sel_type_id>,     -- from WebSocket sel.typeId (NOT market.marketTypeId)
            "odds":         "<odds>",          -- decimal odds as string
            "stake":        <stake>            -- NGN, integer
          }]
        }],
        "acceptanyodds": true,
        "tags": {},
        "placeBetTimeStamp": <unix_ms>
      }
    }
  }

Odds verification:
  Reuses the scraper's WebSocket connection (25-second blob cache).
  The driver holds a shared scraper instance, so no extra WebSocket
  connections within a refresh cycle.

Markets available:
  Prematch: Football 1X2 only
  Live:     Football 1X2, Basketball H/A, Tennis H/A
"""
import logging
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://accessbet.com/api/endpoint'

# Outcome label → WebSocket selection 'outcome' key
_OUTCOME_KEYS = {
    'Home': '1',
    'Draw': 'X',
    'Away': '2',
}

# Mirrors scrapers/accessbet.py constants
_SPORT_MAP = {1: 'football', 2: 'basketball', 5: 'tennis'}
_PREMATCH_MARKET_TYPE = 3
_LIVE_MARKET_PRIORITY = {
    'football':   [332, 3],
    'basketball': [347, 369, 382, 403],
    'tennis':     [403, 989, 382],
}
_LIVE_OUTCOME_MAPS = {
    332: {'1': 'Home', 'X': 'Draw', '2': 'Away'},
    3:   {'1': 'Home', 'X': 'Draw', '2': 'Away'},
    347: {'1': 'Home', '2': 'Away'},
    369: {'1': 'Home', '2': 'Away'},
    382: {'1': 'Home', '2': 'Away'},
    403: {'1': 'Home', '2': 'Away'},
    989: {'1': 'Home', '2': 'Away'},
}
_PREMATCH_OUTCOME_MAP = {'1': 'Home', 'X': 'Draw', '2': 'Away'}


class AccessbetDriver(PlacementDriver):
    """
    Placement driver for Accessbet Nigeria.

    Both verify_odds() and place_bet() query the WebSocket feed via the
    scraper's cached blob — no extra connections within a refresh cycle.
    """

    def __init__(self, session: str = ''):
        self.session_id = session
        self._scraper   = None  # lazy-init on first use

        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, text/plain, */*',
            'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
            'Origin':          'https://accessbet.com',
            'Referer':         'https://accessbet.com/',
            'Content-Type':    'text/plain',   # intentional — server expects this
        })
        if session:
            self._http.cookies.set('USERSESSION', session, domain='accessbet.com')

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

    @classmethod
    def from_config(cls) -> 'AccessbetDriver':
        import config
        return cls(session=getattr(config, 'ACCESSBET_SESSION', ''))

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

    def login(self) -> bool:
        if not self.session_id:
            logger.warning(
                '[Accessbet] No USERSESSION configured. '
                'Odds verification works without auth, but place_bet() will fail. '
                'Log into accessbet.com → DevTools → Cookies → copy USERSESSION '
                'and set ACCESSBET_SESSION in config.py.'
            )
            return True  # verify_odds still works via public WebSocket
        logger.info('[Accessbet] Session configured.')
        return True

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

    # ── WebSocket detail lookup ───────────────────────────────────────────────

    def _get_bet_detail(
        self,
        match_id:     str,
        outcome_name: str,
        is_live:      bool,
    ) -> Optional[dict]:
        """
        Query the cached WebSocket blob for the match + market + selection.

        Returns a dict with all fields needed by InsertCoupon:
          match_id, match_code, market_id, market_type_id, sel_id, live_odds
        """
        scraper  = self._get_scraper()
        messages = scraper._get_messages()
        idx      = scraper._index_messages(messages)

        ws_outcome = _OUTCOME_KEYS.get(outcome_name)
        if not ws_outcome:
            logger.warning(f'[Accessbet] Unknown outcome_name: {outcome_name!r}')
            return None

        # Find the match by ID
        match = next(
            (m for m in idx['matches'] if str(m.get('id')) == str(match_id)),
            None,
        )
        if not match:
            return None
        if is_live and not match.get('isLive'):
            return None
        if not is_live and match.get('isLive'):
            return None

        match_code = str(match.get('code', ''))
        sport_id   = scraper._sport_id(match, idx)
        sport      = _SPORT_MAP.get(sport_id, 'football')

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

        # Choose correct market type
        if is_live:
            priority = _LIVE_MARKET_PRIORITY.get(sport, [])
            market   = None
            for mt_id in priority:
                mk = mks_by_type.get(mt_id)
                if not mk:
                    continue
                # Only use if this market has the outcome we want
                if any(str(s.get('outcome')) == ws_outcome for s in mk.get('selections', [])):
                    market = mk
                    break
        else:
            market = mks_by_type.get(_PREMATCH_MARKET_TYPE)

        if not market:
            return None

        # Find the specific selection
        for sel in market.get('selections', []):
            if str(sel.get('outcome')) != ws_outcome:
                continue
            try:
                live_odds = float(sel['odds'])
            except (KeyError, TypeError, ValueError):
                continue

            # marketTypeId in the placement payload comes from the selection's own
            # typeId (not the market's marketTypeId). e.g. market 332 → sel typeId 2002.
            sel_type_id = (
                sel.get('typeId') or
                sel.get('marketTypeId') or
                market.get('marketTypeId', 0)
            )

            return {
                'match_id':       str(match_id),
                'match_code':     match_code,
                'market_id':      str(market.get('id', '')),
                'sel_id':         str(sel.get('id', '')),
                'sel_type_id':    sel_type_id,
                'live_odds':      live_odds,
            }

        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]:
        """
        Return current odds via WebSocket (cached blob — no extra connection).
        event_id: numeric match ID extracted from the scraped event_id.
        """
        try:
            detail = self._get_bet_detail(parse_numeric_event_id(event_id), outcome_name, is_live)
            return detail['live_odds'] if detail else None
        except Exception as ex:
            logger.warning(f'[Accessbet] verify_odds error 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 Accessbet.

        event_id:     numeric Accessbet match ID
        outcome_name: 'Home', 'Away', or 'Draw'
        odds:         expected decimal odds
        stake:        amount in NGN (rounded to whole number)
        """
        if not self.session_id:
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message='No USERSESSION — set ACCESSBET_SESSION in config.py'
            )

        # Fetch current WebSocket state for this match
        detail = self._get_bet_detail(parse_numeric_event_id(event_id), outcome_name, is_live)
        if not detail:
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=f'Match/selection not found or suspended: {event_id} {outcome_name}'
            )

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

        stake_int = int(stake)
        payload   = {
            'headers': {},
            'requestMethod': 'betting.betslip.place',
            'requestArguments': {
                'slipData': {
                    'acceptbet':        True,
                    'auth':             True,
                    'type':             'match',
                    'perline':          False,
                    'matches': [{
                        'id':      detail['match_id'],
                        'banker':  False,
                        'group':   None,
                        'code':    detail['match_code'],
                        'selections': [{
                            'id':           detail['sel_id'],
                            'marketId':     detail['market_id'],
                            'marketTypeId': detail['sel_type_id'],
                            'odds':         str(round(live_odds, 2)),
                            'stake':        stake_int,
                        }],
                    }],
                    'acceptanyodds':     True,
                    'tags':              {},
                    'placeBetTimeStamp': int(time.time() * 1000),
                }
            },
        }

        try:
            import json
            r = self._http.post(BET_URL, data=json.dumps(payload, separators=(',', ':')), 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, expected_odds: float, stake: float) -> BetResult:
        """
        Parse the betting.betslip.place response.

        The response uses the same JSON-RPC envelope as the WebSocket feed:
          {"messages": [{"object": "betslip", "id": "<bet_id>", ...}], ...}

        Look for a message with object="betslip" or object="bet" that contains
        a bet ID and status. Adjust once the actual response is observed.
        """
        if not isinstance(data, dict):
            return BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=f'Unexpected response type: {str(data)[:200]}'
            )

        # Check for explicit error — either via errorCode or non-empty error string
        err_code = data.get('errorCode') or data.get('error_code')
        err_msg  = data.get('errorMessage') or data.get('error') or data.get('message')
        has_err_code = err_code and str(err_code) not in ('0', 'null', 'None', '')
        has_err_msg  = isinstance(err_msg, str) and err_msg.strip()
        if has_err_code or has_err_msg:
            logger.warning(f'[Accessbet] 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}')
            )

        # Check returnValue dict (primary success path)
        rv = data.get('returnValue')
        if isinstance(rv, dict):
            if rv.get('result') == 'ok':
                bet_id = str(
                    rv.get('id') or
                    (rv.get('betslip') or {}).get('id') or
                    rv.get('betId') or ''
                )
                logger.info(f'[Accessbet] Bet placed: id={bet_id} odds={expected_odds} stake={stake}')
                return BetResult(
                    success=True,
                    bet_id=bet_id or None,
                    odds_placed=expected_odds,
                    stake_placed=stake,
                    message='OK',
                )
            # returnValue present but result != 'ok'
            rv_err = rv.get('message') or rv.get('description') or rv.get('result') or ''
            if rv_err:
                logger.warning(f'[Accessbet] Bet rejected via returnValue: {rv_err}')
                return BetResult(
                    success=False, bet_id=None, odds_placed=None, stake_placed=None,
                    message=str(rv_err),
                )

        # Scan messages array for the bet/betslip object (older response format)
        bet_id = None
        for msg in data.get('messages', []):
            obj = msg.get('object', '')
            if obj in ('betslip', 'bet', 'coupon', 'slip'):
                bet_id = str(msg.get('id') or msg.get('betId') or msg.get('couponId') or '')
                if bet_id:
                    break

        # Also check top-level fields
        if not bet_id:
            bet_id = str(
                data.get('betId') or data.get('id') or
                data.get('couponId') or data.get('slip_id') or ''
            )

        if bet_id:
            logger.info(f'[Accessbet] Bet placed: id={bet_id} odds={expected_odds} stake={stake}')
            return BetResult(
                success=True,
                bet_id=bet_id,
                odds_placed=expected_odds,
                stake_placed=stake,
                message='OK',
            )

        # No error but also no bet ID — treat as unknown
        logger.warning(f'[Accessbet] Ambiguous response (no bet_id): {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]}'
        )
