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, find_middle_opportunities, group_events_by_match, arb_margin, _is_teams_reversed
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 ──────────────────────────────────────────────────────────────
state = {
    'opportunities': [],
    'middles': [],
    'last_updated': None,
    'stats': {
        'total_events': 0,
        'opportunities': 0,
        'best_arb': 0.0,
        'bookmakers_online': 0,
        'middles': 0,
    },
    'errors': [],
}

# All scraper instances (always initialised; toggled at runtime via enabled dict)
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 ──────────────────────────────────────────────────
TOGGLES_FILE = os.path.join(os.path.dirname(__file__), '..', 'bookmakers_state.json')

def _load_toggles() -> dict:
    """Load persisted toggle state, falling back to config defaults."""
    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)
        # Merge: saved values override defaults, new scrapers get their default
        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__ written by live_app)
        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}')

# Runtime enabled state — loaded from file (or config defaults on first run)
bookmakers_enabled = _load_toggles()


# ── Playwright process sweep ──────────────────────────────────────────────────
# Chrome renderer sub-processes get reparented when their direct parent is killed,
# so tracking via the Node.js parent PID misses them. A sweep by age is the only
# reliable way to evict orphans that survive psutil per-scraper kills.
_SCRAPE_CYCLE = 300  # kill playwright/chrome processes older than this many seconds

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(name: str, scraper) -> tuple:
    """Fetch prematch events for one bookmaker. Returns (name, events, error)."""
    try:
        events = scraper.get_all_events()
        return name, events, None
    except Exception as ex:
        return name, [], ex


def refresh_odds():
    _sweep_stale_playwright()
    logger.info("── Odds refresh started ──")

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

    events_by_bm = {}
    errors = []
    online = 0

    pool = ThreadPoolExecutor(max_workers=min(len(enabled), 10))
    futures = {pool.submit(_fetch_one, name, scraper): name
               for name, scraper in enabled.items()}
    try:
        for future in as_completed(futures, timeout=150):
            name, events, err = future.result()
            if err:
                msg = f"{name}: {err}"
                errors.append(msg)
                logger.error(msg)
            elif events:
                events_by_bm[name] = events
                online += 1
    except FuturesTimeoutError:
        for future, name in futures.items():
            if not future.done():
                errors.append(f"{name}: timed out")
                logger.warning(f"[timeout] {name} did not finish within 150s, 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)
    middles = find_middle_opportunities(events_by_bm)

    state['opportunities'] = [o.to_dict() for o in opps]
    state['middles']       = [m.to_dict() for m in middles]
    state['last_updated']  = datetime.now().strftime('%H:%M:%S')
    state['errors']        = errors
    state['stats'] = {
        'total_events':      total_events,
        'opportunities':     len(opps),
        'best_arb':          round(opps[0].arb_percentage, 2) if opps else 0.0,
        'bookmakers_online': online,
        'middles':           len(middles),
    }
    logger.info(f"── Done: {len(opps)} arb | {len(middles)} middles | {total_events} events ──")
    _push_to_betsnipper(state['opportunities'], state['stats'], state['middles'])


def _push_to_betsnipper(opportunities: list, stats: dict, middles: list) -> None:
    """Push current opportunities and middles to Betsnipper.com. Non-blocking on failure."""
    url = config.BETSNIPPER_INGEST_URL
    if not url:
        return
    for attempt in range(1, 4):
        try:
            resp = _requests.post(
                url,
                json={'opportunities': opportunities, 'stats': stats, 'middles': middles},
                headers={'X-Api-Key': config.BETSNIPPER_API_KEY},
                timeout=10,
            )
            if resp.status_code == 200:
                logger.info(f'[Betsnipper] push OK — {len(opportunities)} opportunities')
                return
            else:
                logger.warning(f'[Betsnipper] push failed: HTTP {resp.status_code} {resp.text[:120]}')
                return
        except Exception as ex:
            logger.warning(f'[Betsnipper] push error (attempt {attempt}/3): {ex}')
            if attempt < 3:
                _time.sleep(5)


# ── Routes ────────────────────────────────────────────────────────────────────
@app.route('/')
def dashboard():
    return render_template(
        'dashboard.html',
        refresh_interval=config.REFRESH_INTERVAL,
        live_app_url=config.LIVE_APP_URL,
    )


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


@app.route('/api/middles')
def api_middles():
    sport_filter = request.args.get('sport', 'all')
    limit        = int(request.args.get('limit', 200))
    middles = state['middles']
    if sport_filter != 'all':
        middles = [m for m in middles if m['sport'] == sport_filter]
    total = len(middles)
    return jsonify({
        'middles':      middles[:limit],
        'total':        total,
        'last_updated': state['last_updated'],
        'stats':        state['stats'],
        'errors':       state['errors'],
    })


@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/debug/near-misses')
def api_near_misses():
    """Show matched events sorted by how close they are to arbitrage (sum of inverse odds)."""
    all_events = [e for events in
                  {name: scraper.get_all_events()
                   for name, scraper in scrapers.items()
                   if bookmakers_enabled.get(name)}.values()
                  for e in events]
    groups = group_events_by_match(all_events)

    rows = []
    for match_key, events in groups.items():
        bm_set = {e.bookmaker for e in events}
        if len(bm_set) < 2:
            continue
        markets = {e.market for e in events}
        for market in markets:
            me = [e for e in events if e.market == market]
            if len({e.bookmaker for e in me}) < 2:
                continue
            _SWAP = {'Home': 'Away', 'Away': 'Home'}
            ref = me[0]
            normalized = []
            for event in me:
                flipped = _is_teams_reversed(ref, event)
                for outcome in event.outcomes:
                    if flipped and outcome.name in _SWAP:
                        from core.models import Outcome as _Outcome
                        outcome = _Outcome(_SWAP[outcome.name], outcome.odds, outcome.bookmaker, outcome.event_url)
                    normalized.append(outcome)
            outcome_names = {o.name for o in normalized}
            best = {}
            for outcome in normalized:
                if outcome.name not in best or outcome.odds > best[outcome.name].odds:
                    best[outcome.name] = outcome
            if len(best) != len(outcome_names):
                continue
            inv_sum = sum(1.0 / o.odds for o in best.values())
            ref = me[0]
            rows.append({
                'event': f"{ref.home_team} vs {ref.away_team}",
                'sport': ref.sport,
                'market': market,
                'bookmakers': list({e.bookmaker for e in me}),
                'inverse_sum': round(inv_sum, 4),
                'margin_pct': round((1 / inv_sum - 1) * 100, 3),
                'best_odds': {o.name: {'odds': o.odds, 'bm': o.bookmaker} for o in best.values()},
            })

    rows.sort(key=lambda x: x['inverse_sum'])
    return jsonify({'total_matched': len(rows), 'near_misses': rows[:50]})


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


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

    scheduler = BackgroundScheduler(daemon=True)
    scheduler.add_job(refresh_odds, 'interval', seconds=config.REFRESH_INTERVAL)
    scheduler.start()

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