"""
live_app.py — Standalone live (in-play) arbitrage scanner.

Run independently of the prematch bot:
    python3 web/live_app.py

Serves the live dashboard on LIVE_PORT (default 5001) and refreshes
live odds every LIVE_REFRESH_INTERVAL seconds without waiting for prematch.
Pushes opportunities to Betsnipper via /api/live-ingest after each cycle.
"""
import logging
import sys
import os
import json
from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError as FuturesTimeoutError
from datetime import datetime

import requests as _requests
import psutil as _psutil
import time as _time

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

from flask import Flask, render_template, jsonify, request
from apscheduler.schedulers.background import BackgroundScheduler

from scrapers.sportybet import SportyBetScraper
from scrapers.msport import MSportScraper
from scrapers.betking import BetKingScraper
from scrapers.nairabet import NairaBetScraper
from scrapers.betway import BetwayScraper
from scrapers.bet9ja import Bet9jaScraper
from scrapers.oneXbet import OneXBetScraper
from scrapers.bcgame import BCGameScraper
from scrapers.betwinner import BetWinnerScraper
from scrapers.betpawa import BetpawaScraper
from scrapers.melbet import MelbetScraper
from scrapers.onewin import OneWinScraper
from scrapers.stake import StakeScraper
from scrapers.surebet247 import Surebet247Scraper
from scrapers.bangbet import BangbetScraper
from scrapers.accessbet import AccessbetScraper
from scrapers.betjara import BetjaraScraper
from scrapers.twentytwoBet import TwentyTwoBetScraper
from core.calculator import find_arb_opportunities
import config

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

# Playwright process kills via psutil leave asyncio with pending pipe writes that
# raise BrokenPipeError internally.  The kill succeeds; the noise does not.
class _IgnoreBrokenPipe(logging.Filter):
    def filter(self, record):
        msg = record.getMessage()
        return 'BrokenPipeError' not in msg and 'Broken pipe' not in msg

logging.getLogger('asyncio').addFilter(_IgnoreBrokenPipe())

app = Flask(__name__)

# ── Global state ───────────────────────────────────────────────────────────────
live_state = {
    'opportunities': [],
    'last_updated':  None,
    'stats': {
        'total_events':  0,
        'opportunities': 0,
        'best_arb':      0.0,
    },
    'errors': [],
}

# All scraper instances — same set as prematch bot.
# Each has its own internal state so the two processes don't share objects.
scrapers = {
    'SportyBet':  SportyBetScraper(),
    'MSport':     MSportScraper(),
    'BetKing':    BetKingScraper(),
    'NairaBet':   NairaBetScraper(),
    'Betway':     BetwayScraper(),
    '1xBet':      OneXBetScraper(),
    'BetWinner':  BetWinnerScraper(),
    'Bet9ja':     Bet9jaScraper(),
    'BCGame':     BCGameScraper(),
    'Betpawa':    BetpawaScraper(),
    'Melbet':     MelbetScraper(),
    '1win':       OneWinScraper(),
    'Stake':      StakeScraper(),
    'Surebet247': Surebet247Scraper(),
    'Bangbet':    BangbetScraper(),
    'Accessbet':  AccessbetScraper(),
    'Betjara':    BetjaraScraper(),
    '22bet':      TwentyTwoBetScraper(),
}

# ── Toggle state persistence ───────────────────────────────────────────────────
# Shares the same bookmakers_state.json as the prematch bot so enabling/disabling
# a bookmaker in either dashboard affects both processes consistently.
TOGGLES_FILE  = os.path.join(os.path.dirname(__file__), '..', 'bookmakers_state.json')
LIVE_FLAG_KEY = '__live_enabled__'


def _load_live_enabled() -> bool:
    try:
        with open(TOGGLES_FILE) as f:
            saved = json.load(f)
        return bool(saved.get(LIVE_FLAG_KEY, config.LIVE_ENABLED))
    except (FileNotFoundError, json.JSONDecodeError):
        return config.LIVE_ENABLED


def _save_live_enabled(value: bool) -> None:
    try:
        try:
            with open(TOGGLES_FILE) as f:
                data = json.load(f)
        except (FileNotFoundError, json.JSONDecodeError):
            data = {}
        data[LIVE_FLAG_KEY] = value
        with open(TOGGLES_FILE, 'w') as f:
            json.dump(data, f, indent=2)
    except Exception as ex:
        logger.warning(f'Could not save live toggle: {ex}')


def _load_toggles() -> dict:
    defaults = {
        name: cfg.get('enabled', False)
        for cfg in config.BOOKMAKERS.values()
        for name in [cfg['name']]
        if name in scrapers
    }
    try:
        with open(TOGGLES_FILE) as f:
            saved = json.load(f)
        return {name: saved.get(name, defaults[name]) for name in defaults}
    except (FileNotFoundError, json.JSONDecodeError):
        return defaults


def _save_toggles(toggles: dict) -> None:
    try:
        # Preserve other keys (e.g. __live_enabled__)
        try:
            with open(TOGGLES_FILE) as f:
                data = json.load(f)
        except (FileNotFoundError, json.JSONDecodeError):
            data = {}
        data.update(toggles)
        with open(TOGGLES_FILE, 'w') as f:
            json.dump(data, f, indent=2)
    except Exception as ex:
        logger.warning(f'Could not save toggle state: {ex}')


bookmakers_enabled = _load_toggles()
live_enabled       = _load_live_enabled()


# ── Playwright process sweep ───────────────────────────────────────────────────
_SCRAPE_CYCLE = 300

def _sweep_stale_playwright():
    now = _time.time()
    killed = 0
    for p in _psutil.process_iter(['pid', 'name', 'cmdline', 'create_time']):
        try:
            age = now - p.info['create_time']
            if age < _SCRAPE_CYCLE:
                continue
            cmd = ' '.join(p.info['cmdline'] or [])
            name = p.info['name'] or ''
            if ('playwright/driver/node' in cmd or
                    'chrome-headless-shell' in name or
                    ('chrome' in name and 'playwright_chromiumdev_profile' in cmd)):
                p.kill()
                killed += 1
        except (_psutil.NoSuchProcess, _psutil.AccessDenied):
            pass
        except Exception:
            pass
    if killed:
        logger.info(f'[sweep] killed {killed} stale Playwright/Chrome processes (>{_SCRAPE_CYCLE}s old)')


# ── Refresh logic ──────────────────────────────────────────────────────────────
def _fetch_one_live(name: str, scraper) -> tuple:
    try:
        events = scraper.get_all_live_events()
        return name, events, None
    except Exception as ex:
        return name, [], ex


def refresh_live_odds():
    global live_enabled
    if not live_enabled:
        logger.debug('Live scanning disabled — skipping refresh.')
        return

    _sweep_stale_playwright()
    logger.info('── Live odds refresh started ──')

    enabled = {
        name: scraper for name, scraper in scrapers.items()
        if bookmakers_enabled.get(name, False)
    }

    events_by_bm = {}
    errors = []

    pool = ThreadPoolExecutor(max_workers=min(max(len(enabled), 1), 10))
    futures = {pool.submit(_fetch_one_live, name, scraper): name
               for name, scraper in enabled.items()}
    try:
        for future in as_completed(futures, timeout=90):
            name, events, err = future.result()
            if err:
                msg = f'{name}(live): {err}'
                errors.append(msg)
                logger.error(f'[live] {name}: {err}')
            elif events:
                events_by_bm[name] = events
    except FuturesTimeoutError:
        for future, name in futures.items():
            if not future.done():
                errors.append(f'{name}(live): timed out')
                logger.warning(f'[live][timeout] {name} did not finish within 90s, skipping')
    pool.shutdown(wait=False, cancel_futures=True)

    total_events = sum(len(v) for v in events_by_bm.values())
    opps = find_arb_opportunities(events_by_bm)

    live_state['opportunities'] = [o.to_dict() for o in opps]
    live_state['last_updated']  = datetime.now().strftime('%H:%M:%S')
    live_state['errors']        = errors
    live_state['stats'] = {
        'total_events':  total_events,
        'opportunities': len(opps),
        'best_arb':      round(opps[0].arb_percentage, 2) if opps else 0.0,
    }
    logger.info(f'── Live done: {len(opps)} arb opportunities | {total_events} live events ──')
    _push_live_to_betsnipper(live_state['opportunities'])


def _push_live_to_betsnipper(opportunities: list) -> None:
    """Replace the full live dataset on Betsnipper after every refresh."""
    url = config.BETSNIPPER_LIVE_INGEST_URL
    if not url:
        return
    for attempt in range(1, 4):
        try:
            resp = _requests.post(
                url,
                json={'opportunities': opportunities},
                headers={'X-Api-Key': config.BETSNIPPER_API_KEY},
                timeout=10,
            )
            if resp.status_code == 200:
                logger.info(f'[Betsnipper Live] push OK — {len(opportunities)} opportunities')
                return
            else:
                logger.warning(f'[Betsnipper Live] push failed: HTTP {resp.status_code} {resp.text[:120]}')
                return
        except Exception as ex:
            logger.warning(f'[Betsnipper Live] push error (attempt {attempt}/3): {ex}')
            if attempt < 3:
                _time.sleep(5)


# ── Routes ─────────────────────────────────────────────────────────────────────
@app.route('/')
def live_dashboard():
    return render_template(
        'live.html',
        live_refresh_interval=config.LIVE_REFRESH_INTERVAL,
        live_enabled=live_enabled,
        prematch_app_url=config.PREMATCH_APP_URL,
    )


@app.route('/api/live-opportunities')
def api_live_opportunities():
    sport_filter = request.args.get('sport', 'all')
    opps = live_state['opportunities']
    if sport_filter != 'all':
        opps = [o for o in opps if o['sport'] == sport_filter]
    return jsonify({
        'opportunities': opps,
        'last_updated':  live_state['last_updated'],
        'stats':         live_state['stats'],
        'errors':        live_state['errors'],
        'live_enabled':  live_enabled,
    })


@app.route('/api/bookmakers')
def api_bookmakers():
    return jsonify([
        {'name': name, 'enabled': bookmakers_enabled.get(name, False)}
        for name in scrapers
    ])


@app.route('/api/bookmakers/<name>/toggle', methods=['POST'])
def api_toggle_bookmaker(name):
    if name not in scrapers:
        return jsonify({'error': 'unknown bookmaker'}), 404
    bookmakers_enabled[name] = not bookmakers_enabled.get(name, False)
    _save_toggles(bookmakers_enabled)
    logger.info(f'[toggle] {name} → {"ON" if bookmakers_enabled[name] else "OFF"}')
    return jsonify({'name': name, 'enabled': bookmakers_enabled[name]})


@app.route('/api/live/toggle', methods=['POST'])
def api_toggle_live():
    global live_enabled
    live_enabled = not live_enabled
    _save_live_enabled(live_enabled)
    logger.info(f'[live toggle] → {"ON" if live_enabled else "OFF"}')
    return jsonify({'live_enabled': live_enabled})


@app.route('/api/refresh', methods=['POST'])
def api_refresh():
    refresh_live_odds()
    return jsonify({'status': 'ok', 'last_updated': live_state['last_updated']})


# ── Entry point ────────────────────────────────────────────────────────────────
if __name__ == '__main__':
    refresh_live_odds()

    scheduler = BackgroundScheduler(daemon=True)
    scheduler.add_job(refresh_live_odds, 'interval', seconds=config.LIVE_REFRESH_INTERVAL)
    scheduler.start()

    logger.info(f'Live dashboard running at http://localhost:{config.LIVE_PORT}')
    app.run(host='0.0.0.0', port=config.LIVE_PORT, debug=False)
