"""
Multi-bookmaker arb scanner for the execution bot.

Runs independent scan loops (prematch and/or live) across all bookmakers in
EXEC_BOOKMAKERS.  Scrapers are fetched in parallel each cycle so adding more
bookmakers does not increase the wall-clock scan time.  New or changed arbs
are pushed onto self.queue for the main bot loop to action.

Separate from the web dashboard scanner — this one runs at the EXEC_*_SCAN_INTERVAL
rate and only covers the bookmakers we can actually place on.
"""
import logging
import threading
import time
import queue
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List

from scrapers.oneXbet   import OneXBetScraper
from scrapers.bet9ja    import Bet9jaScraper
from scrapers.betking   import BetKingScraper
from scrapers.accessbet import AccessbetScraper
from scrapers.bcgame    import BCGameScraper
from core.calculator    import find_arb_opportunities
import config

logger = logging.getLogger(__name__)

# Map bookmaker name → scraper class
_SCRAPER_MAP = {
    '1xBet':     OneXBetScraper,
    'Bet9ja':    Bet9jaScraper,
    'BetKing':   BetKingScraper,
    'Accessbet': AccessbetScraper,
    'BCGame':    BCGameScraper,
}


class ArbScanner:
    """
    Continuously scans all configured bookmakers and pushes new arb dicts onto
    self.queue.  Each arb dict carries an extra '_mode' key ('prematch'/'live').
    """

    def __init__(self, bookmakers: List[str], mode: str = 'both'):
        """
        bookmakers : list of bookmaker names (must be keys in _SCRAPER_MAP)
        mode       : 'prematch', 'live', or 'both'
        """
        for bm in bookmakers:
            if bm not in _SCRAPER_MAP:
                raise ValueError(
                    f'No scraper registered for "{bm}". '
                    f'Available: {list(_SCRAPER_MAP)}'
                )

        self.bookmakers = bookmakers
        self.mode       = mode
        self.queue: queue.Queue = queue.Queue()

        self._stop = threading.Event()
        # Separate seen-sets per mode so a prematch arb doesn't block a live one
        self._seen: dict[str, set] = {'prematch': set(), 'live': set()}

        # One scraper instance per bookmaker (shared across prematch + live loops)
        self._scrapers: dict = {bm: _SCRAPER_MAP[bm]() for bm in bookmakers}

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

    def start(self):
        """Start background scan threads. Non-blocking."""
        modes = []
        if self.mode in ('prematch', 'both'):
            modes.append('prematch')
        if self.mode in ('live', 'both'):
            modes.append('live')

        bm_list = ', '.join(self.bookmakers)
        for m in modes:
            t = threading.Thread(
                target=self._loop, args=(m,),
                daemon=True, name=f'scanner-{m}',
            )
            t.start()
            logger.info(f'[Scanner] {m} thread started ({bm_list})')

    def stop(self):
        self._stop.set()

    # ── Internal ──────────────────────────────────────────────────────────────

    def _loop(self, mode: str):
        interval = (
            config.EXEC_LIVE_SCAN_INTERVAL if mode == 'live'
            else config.EXEC_SCAN_INTERVAL
        )
        while not self._stop.is_set():
            try:
                self._scan_once(mode)
            except Exception as ex:
                logger.error(f'[Scanner] {mode} error: {ex}')
            self._stop.wait(timeout=interval)

    def _fetch_one(self, bm: str, mode: str):
        """Fetch events for a single bookmaker. Returns (bm, events) or (bm, None)."""
        scraper = self._scrapers[bm]
        try:
            events = (
                scraper.get_all_live_events() if mode == 'live'
                else scraper.get_all_events()
            )
            if events:
                logger.debug(f'[Scanner] {bm} {mode}: {len(events)} events')
                return bm, events
        except Exception as ex:
            logger.warning(f'[Scanner] {bm} {mode} fetch error: {ex}')
        return bm, None

    def _scan_once(self, mode: str):
        # Fetch all bookmakers in parallel
        events_by_bm: dict = {}
        with ThreadPoolExecutor(max_workers=len(self.bookmakers)) as pool:
            futures = {
                pool.submit(self._fetch_one, bm, mode): bm
                for bm in self.bookmakers
            }
            for fut in as_completed(futures):
                bm, events = fut.result()
                if events:
                    events_by_bm[bm] = events

        if len(events_by_bm) < 2:
            return

        opps         = find_arb_opportunities(events_by_bm)
        current_fps: set = set()

        for opp in opps:
            if opp.arb_percentage < config.EXEC_MIN_PROFIT_PCT:
                continue
            fp = _fingerprint(opp)
            current_fps.add(fp)
            if fp not in self._seen[mode]:
                self._seen[mode].add(fp)
                d        = opp.to_dict()
                d['_mode'] = mode
                self.queue.put(d)
                logger.info(
                    f'[Scanner] NEW {mode} arb: {opp.event_name} '
                    f'{opp.market} — {opp.arb_percentage:.2f}%'
                )

        # Prune stale fingerprints (arbs that have disappeared from the feed)
        self._seen[mode] &= current_fps


def _fingerprint(opp) -> str:
    outcomes = sorted(opp.outcomes, key=lambda o: (o['bookmaker'], o['outcome']))
    return '|'.join([
        opp.event_name.lower(),
        opp.market,
        *[f'{o["bookmaker"]}:{o["outcome"]}:{o["odds"]:.3f}' for o in outcomes],
    ])
