"""
Arbitrage Execution Bot
=======================
Run:
  python3 execution/bot.py              # prematch + live
  python3 execution/bot.py --prematch   # prematch only
  python3 execution/bot.py --live       # live only

Scans 1xBet and Bet9ja (configurable in config.py) for arb opportunities.
When a new arb is found:
  1. Prints details to terminal + sends Telegram alert
  2. Prompts you to enter a total stake (₦) within EXEC_STAKE_TIMEOUT seconds
  3. Re-verifies odds are still valid
  4. Calculates per-leg stakes
  5. Places both legs in parallel
  6. Logs result to bets.csv + sends Telegram confirmation

Placement drivers for 1xBet and Bet9ja are fully implemented.
Update credentials in config.py when tokens expire (every ~4 hours for 1xBet;
Bet9ja livsid is persistent but ak_bmsc/bm_sv rotate occasionally).
"""
import argparse
import logging
import os
import queue as _queue
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from typing import List, Optional

# Allow running as `python3 execution/bot.py` from project root
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

import config
from execution.scanner        import ArbScanner
from execution.stake_calc     import calc_from_total, calc_from_profit, format_allocation, LegStake
from execution.ledger         import log_bet
from execution.telegram_alert import arb_found_alert, bet_placed_alert, error_alert
from execution.drivers.base   import BetResult

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
    datefmt='%H:%M:%S',
)
logger = logging.getLogger('execution.bot')

# ── Colour helpers (terminal) ─────────────────────────────────────────────────
_GREEN  = '\033[92m'
_YELLOW = '\033[93m'
_RED    = '\033[91m'
_CYAN   = '\033[96m'
_BOLD   = '\033[1m'
_RESET  = '\033[0m'

def _c(colour, text): return f'{colour}{text}{_RESET}'


# ── Driver loader ─────────────────────────────────────────────────────────────

def _load_driver(bookmaker: str):
    """Return an initialised placement driver for the given bookmaker, or None."""
    try:
        if bookmaker == '1xBet':
            from execution.drivers.onexbet import OneXBetDriver
            d = OneXBetDriver.from_config()
            if not d.login():
                logger.warning('[Bot] 1xBet tokens invalid/expired — update config.py')
            return d
        if bookmaker == 'Bet9ja':
            from execution.drivers.bet9ja import Bet9jaDriver
            d = Bet9jaDriver.from_config()
            if not d.login():
                logger.warning('[Bot] Bet9ja livsid not configured — update config.py')
            return d
        if bookmaker == 'BetKing':
            from execution.drivers.betking import BetKingDriver
            d = BetKingDriver.from_config()
            if not d.login():
                logger.warning('[Bot] BetKing credentials not configured — update config.py')
            return d
        if bookmaker == 'Accessbet':
            from execution.drivers.accessbet import AccessbetDriver
            d = AccessbetDriver.from_config()
            d.login()
            return d
        if bookmaker == 'BCGame':
            from execution.drivers.bcgame import BCGameDriver
            d = BCGameDriver.from_config()
            d.login()
            return d
    except Exception as ex:
        logger.warning(f'[Bot] Could not load driver for {bookmaker}: {ex}')
    return None


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

def _verify_leg(driver, leg_info: dict) -> Optional[float]:
    """
    Ask the driver for current odds. Returns odds float or None if unavailable.
    """
    if driver is None:
        return None
    try:
        return driver.verify_odds(
            leg_info['event_id'],
            leg_info['outcome'],
            market=leg_info.get('market', ''),
            is_live=leg_info.get('is_live', False),
        )
    except NotImplementedError:
        return None
    except Exception as ex:
        logger.warning(f'[Bot] verify_odds failed: {ex}')
        return None


# ── Stake input ───────────────────────────────────────────────────────────────
#
# A single daemon thread reads stdin continuously and puts stripped lines into
# _stdin_q.  _prompt_stake() drains any stale input that arrived while we
# weren't listening, then waits on the queue with the given timeout.
#
# This avoids the previous bug where a timed-out readline() thread stayed alive
# and consumed the user's input for the *next* prompt.

_stdin_q: _queue.Queue = _queue.Queue()
_stdin_started = False


def _ensure_stdin_reader():
    global _stdin_started
    if _stdin_started:
        return
    _stdin_started = True

    def _reader():
        while True:
            try:
                line = sys.stdin.readline()
                _stdin_q.put(line.strip() if line else '')
            except (EOFError, OSError):
                break

    threading.Thread(target=_reader, daemon=True, name='stdin-reader').start()


def _prompt_stake(timeout: float) -> Optional[str]:
    """
    Wait up to `timeout` seconds for the user to type a line.
    Drains stale buffered input first so old keystrokes are ignored.
    Suppresses background log output while waiting so typing isn't interrupted.
    Returns the stripped input string, or None on timeout / empty.
    """
    _ensure_stdin_reader()

    # Discard any lines that arrived while we were busy (e.g. placing a previous bet)
    while not _stdin_q.empty():
        try:
            _stdin_q.get_nowait()
        except _queue.Empty:
            break

    # Suppress INFO/WARNING log noise from scanner threads so the terminal is
    # clean while the user is typing.
    root = logging.getLogger()
    old_level = root.level
    root.setLevel(logging.CRITICAL)
    try:
        line = _stdin_q.get(timeout=timeout)
        return line if line else None
    except _queue.Empty:
        return None
    finally:
        root.setLevel(old_level)


# ── Display helpers ───────────────────────────────────────────────────────────

def _print_arb(opp: dict):
    mode    = opp.get('_mode', 'prematch')
    label   = _c(_RED,  '● LIVE') if mode == 'live' else _c(_CYAN, '○ PRE-MATCH')
    profit  = opp.get('arb_percentage', 0)
    name    = opp.get('event_name', '')
    market  = opp.get('market', '')
    league  = opp.get('league', '')
    sport   = opp.get('sport', '').title()

    print()
    print(_c(_BOLD, '─' * 60))
    print(f'  {label}  {_c(_GREEN, f"{profit:.2f}% PROFIT")}')
    print(f'  {_c(_BOLD, name)}')
    print(f'  {sport}  |  {league}  |  {market}')
    print()
    for o in opp.get('outcomes', []):
        print(f'    {o["bookmaker"]:<14} {o["outcome"]:<8} @ {_c(_BOLD, str(o["odds"]))}')
    print()


def _print_placed(legs: list, results: list):
    all_ok = all(r.success for r in results)
    for leg, result in zip(legs, results):
        icon = _c(_GREEN, '✓') if result.success else _c(_RED, '✗')
        print(
            f'  {icon}  {leg.bookmaker:<12} {leg.outcome:<8} @ {leg.odds}'
            f'  ₦{leg.stake:,.0f} placed'
        )
        if result.success and result.bet_id:
            print(f'       Bet ID: {result.bet_id}')
        elif not result.success:
            print(f'       {_c(_RED, result.message)}')
    print()
    if all_ok:
        print(_c(_GREEN, '  ✓ Both legs placed successfully.'))
    else:
        print(_c(_RED, '  ⚠  Partial/failed placement — check your accounts manually.'))
    print()


# ── Bet execution ─────────────────────────────────────────────────────────────

def _extract_legs_from_opp(opp: dict) -> List[dict]:
    """
    Convert an arb opportunity dict into a list of leg dicts for stake_calc.

    Each leg dict:
      bookmaker, outcome, odds, event_id, odds_key, market, is_live
    """
    is_live = opp.get('_mode', 'prematch') == 'live'
    market  = opp.get('market', '')
    legs = []
    for o in opp.get('outcomes', []):
        legs.append({
            'bookmaker': o['bookmaker'],
            'outcome':   o['outcome'],
            'odds':      o['odds'],
            'event_id':  o.get('event_id', ''),   # full scraper event_id, e.g. "1x_12345_1X2"
            'market':    market,
            'sport':     opp.get('sport', 'football'),
            'is_live':   is_live,
        })
    return legs




def _place_legs_parallel(
    legs:    list,         # LegStake objects
    drivers: dict,         # {bookmaker_name: driver_instance}
    expected_odds: list,   # verified odds per leg (float or None)
) -> List[BetResult]:
    """Place all legs in parallel. Returns results in same order as legs."""
    results = [None] * len(legs)

    def _place(index: int, leg: LegStake) -> tuple:
        driver = drivers.get(leg.bookmaker)
        if driver is None:
            return index, BetResult(
                success=False, bet_id=None, odds_placed=None,
                stake_placed=None,
                message=f'No driver configured for {leg.bookmaker}'
            )
        try:
            result = driver.place_bet(
                    leg.event_id, leg.outcome, leg.odds, leg.stake,
                    market=leg.market, is_live=leg.is_live, sport=leg.sport,
                )
            return index, result
        except NotImplementedError:
            return index, BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=f'{leg.bookmaker} driver not yet implemented (awaiting network capture)'
            )
        except Exception as ex:
            return index, BetResult(
                success=False, bet_id=None, odds_placed=None, stake_placed=None,
                message=str(ex)
            )

    with ThreadPoolExecutor(max_workers=len(legs)) as pool:
        futures = [pool.submit(_place, i, leg) for i, leg in enumerate(legs)]
        for fut in as_completed(futures):
            idx, res = fut.result()
            results[idx] = res

    return results


# ── Main loop ─────────────────────────────────────────────────────────────────

def run(mode: str = 'both'):
    bookmakers = config.EXEC_BOOKMAKERS

    print(_c(_BOLD, '\n  Arbitrage Execution Bot'))
    print(f'  Watching: {", ".join(bookmakers)}')
    print(f'  Mode:     {mode}')
    print(f'  Min profit threshold: {config.EXEC_MIN_PROFIT_PCT}%')
    print(f'  Stake input timeout:  {config.EXEC_STAKE_TIMEOUT}s')
    if not config.TELEGRAM_BOT_TOKEN:
        print(_c(_YELLOW, '  Telegram: not configured (set TELEGRAM_BOT_TOKEN in config.py)'))
    else:
        print(_c(_GREEN, '  Telegram: configured'))
    print()

    # Load drivers for all configured bookmakers
    drivers = {}
    for bm in bookmakers:
        d = _load_driver(bm)
        if d:
            drivers[bm] = d
            logger.info(f'[Bot] Driver loaded: {bm}')
        else:
            logger.warning(f'[Bot] No driver for {bm} — placement will be skipped')

    # Start scanner
    scanner = ArbScanner(bookmakers, mode=mode)
    scanner.start()
    print(_c(_GREEN, '  Scanner started. Waiting for arb opportunities...\n'))

    while True:
        try:
            # Block until a new arb arrives (check every 0.5s so KeyboardInterrupt works)
            opp = None
            while opp is None:
                try:
                    opp = scanner.queue.get(timeout=0.5)
                except Exception:
                    pass

            _print_arb(opp)

            # Send Telegram alert
            arb_found_alert(
                config.TELEGRAM_BOT_TOKEN, config.TELEGRAM_CHAT_ID,
                opp, opp.get('_mode', 'prematch')
            )

            # Prompt for stake
            print(
                f'  Enter total stake ₦  '
                f'(or press Enter to skip, {config.EXEC_STAKE_TIMEOUT}s timeout): ',
                end='', flush=True,
            )
            raw = _prompt_stake(config.EXEC_STAKE_TIMEOUT)

            if not raw:
                print(_c(_YELLOW, '  Skipped.\n'))
                continue

            # Parse stake — accept "5000" (total) or "p500" (target profit)
            target_profit = None
            total_stake   = None
            raw = raw.strip().lower().replace(',', '').replace('₦', '').replace('ngn', '')
            if raw.startswith('p'):
                try:
                    target_profit = float(raw[1:])
                except ValueError:
                    print(_c(_RED, '  Invalid input. Use a number (e.g. 5000) or p<profit> (e.g. p500)\n'))
                    continue
            else:
                try:
                    total_stake = float(raw)
                except ValueError:
                    print(_c(_RED, '  Invalid input. Enter a number.\n'))
                    continue

            # Build legs
            leg_info = _extract_legs_from_opp(opp)
            if not leg_info:
                print(_c(_RED, '  Could not extract leg info from arb.\n'))
                continue

            # Verify current odds
            print('  Verifying current odds...')
            verified_odds = []
            for i, leg in enumerate(leg_info):
                driver = drivers.get(leg['bookmaker'])
                v_odds = _verify_leg(driver, leg)
                verified_odds.append(v_odds)
                if v_odds is None:
                    print(_c(_YELLOW,
                        f'  ?  {leg["bookmaker"]} {leg["outcome"]} '
                        f'— could not verify (using scraped odds {leg["odds"]})'
                    ))
                else:
                    drift = abs(v_odds - leg['odds']) / leg['odds']
                    if drift > 0.02:  # > 2% odds drift
                        print(_c(_YELLOW,
                            f'  ⚠  {leg["bookmaker"]} {leg["outcome"]} '
                            f'odds moved: {leg["odds"]} → {v_odds}'
                        ))
                        # Update to current odds for recalculation
                        leg_info[i]['odds'] = v_odds
                    else:
                        print(_c(_GREEN,
                            f'  ✓  {leg["bookmaker"]} {leg["outcome"]} '
                            f'@ {v_odds} (confirmed)'
                        ))

            # Recalculate with verified odds
            try:
                if target_profit:
                    allocation = calc_from_profit(leg_info, target_profit)
                else:
                    allocation = calc_from_total(leg_info, total_stake)
            except ValueError as ex:
                print(_c(_RED, f'  Arb no longer valid: {ex}\n'))
                continue

            print(format_allocation(allocation))

            # Confirm before placing
            print(f'  Confirm placement? (y/N): ', end='', flush=True)
            confirm = _prompt_stake(15)
            if not confirm or confirm.lower() not in ('y', 'yes'):
                print(_c(_YELLOW, '  Cancelled.\n'))
                continue

            # Place legs in parallel
            print('  Placing bets...')
            results = _place_legs_parallel(allocation.legs, drivers, verified_odds)
            _print_placed(allocation.legs, results)

            # Telegram confirmation
            bet_placed_alert(
                config.TELEGRAM_BOT_TOKEN, config.TELEGRAM_CHAT_ID,
                opp, allocation.legs, results, opp.get('_mode', 'prematch')
            )

            # Log to ledger
            log_bet(
                mode        = opp.get('_mode', 'prematch'),
                sport       = opp.get('sport', ''),
                home_team   = opp.get('event_name', ''),
                away_team   = '',
                market      = opp.get('market', ''),
                league      = opp.get('league', ''),
                legs        = allocation.legs,
                results     = results,
                verified_odds = verified_odds,
                total_stake  = allocation.total_stake,
                expected_profit = allocation.guaranteed_profit,
                profit_pct   = allocation.profit_pct,
            )

        except KeyboardInterrupt:
            print('\n\n  Shutting down...')
            scanner.stop()
            break
        except Exception as ex:
            logger.error(f'[Bot] Unexpected error: {ex}', exc_info=True)
            time.sleep(2)


# ── Entry point ───────────────────────────────────────────────────────────────

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Arbitrage Execution Bot')
    group  = parser.add_mutually_exclusive_group()
    group.add_argument('--prematch', action='store_true', help='Scan pre-match only')
    group.add_argument('--live',     action='store_true', help='Scan live only')
    args = parser.parse_args()

    if args.prematch:
        scan_mode = 'prematch'
    elif args.live:
        scan_mode = 'live'
    else:
        scan_mode = 'both'

    run(mode=scan_mode)
