#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import math
import re
from pathlib import Path
from typing import Any, Dict, List, Optional

import pandas as pd

from oddsportal_bootstrap import (
    LEAGUES,
    OddsPortalBootstrapper,
    discover_routes,
    ensure_parent,
    resolve_results_url,
)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description='Collect dynamic O/U lines from OddsPortal for goal-number training.')
    parser.add_argument('--league', choices=sorted(LEAGUES.keys()), required=True)
    parser.add_argument('--season', required=True)
    parser.add_argument('--page-start', type=int, default=1)
    parser.add_argument('--page-end', type=int, default=1)
    parser.add_argument('--max-matches', type=int, default=10)
    parser.add_argument('--headed', action='store_true')
    parser.add_argument('--output-csv', required=True)
    parser.add_argument('--output-json', help='optional raw json output')
    return parser.parse_args()


def american_to_decimal(raw: str | None) -> Optional[float]:
    if raw is None:
        return None
    s = str(raw).strip()
    if not s:
        return None
    try:
        # already decimal odds
        if re.fullmatch(r'\d+(?:\.\d+)?', s):
            val = float(s)
            if val > 1.0:
                return val
            return None
        if s.startswith('+'):
            return float(s[1:]) / 100.0 + 1.0
        if s.startswith('-'):
            v = abs(float(s))
            return 100.0 / v + 1.0
    except Exception:
        return None
    return None


def parse_percent(raw: str | None) -> Optional[float]:
    if raw is None:
        return None
    m = re.search(r'(\d+(?:\.\d+)?)%', str(raw))
    return float(m.group(1)) if m else None


def parse_ou_line(raw: str | None) -> Optional[float]:
    if raw is None:
        return None
    m = re.search(r'([+-]?\d+(?:\.\d+)?)\s*$', str(raw).strip())
    return float(m.group(1)) if m else None


def parse_kickoff_date(raw: str | None) -> Optional[str]:
    if not raw:
        return None
    # Example: Saturday, 07 Dec 2024, 12:05
    dt = pd.to_datetime(raw, format='%A, %d %b %Y, %H:%M', errors='coerce')
    if pd.isna(dt):
        return None
    return dt.strftime('%Y-%m-%d')


def normalize_team(name: str | None) -> str:
    return re.sub(r'\s+', ' ', (name or '').strip())


def choose_main_ou_line(collapsed_rows: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
    candidates = []
    for row in collapsed_rows:
        line = parse_ou_line(row.get('line'))
        over_dec = american_to_decimal(row.get('over_odds') or row.get('price_1'))
        under_dec = american_to_decimal(row.get('under_odds') or row.get('price_2'))
        if line is None or over_dec is None or under_dec is None:
            continue
        over_prob = 1.0 / over_dec
        under_prob = 1.0 / under_dec
        fair_over = over_prob / (over_prob + under_prob) if (over_prob + under_prob) > 0 else None
        payout = parse_percent(row.get('payout'))
        bookmaker_count = int(float(row.get('bookmaker_count', 0) or 0))
        candidates.append({
            'raw': row,
            'ou_line': line,
            'over_odds': over_dec,
            'under_odds': under_dec,
            'bookmaker_count': bookmaker_count,
            'payout': payout,
            'fair_over': fair_over,
            'even_gap': abs((fair_over or 0.5) - 0.5),
        })
    if not candidates:
        return None
    candidates.sort(key=lambda x: (
        -x['bookmaker_count'],
        x['even_gap'],
        -(x['payout'] or 0),
        abs(x['ou_line'] - 2.5),
    ))
    return candidates[0]


def choose_main_1x2(bookmaker_rows: List[Dict[str, Any]]) -> Dict[str, Optional[float]]:
    best = None
    for row in bookmaker_rows:
        home_odds = american_to_decimal(row.get('home_odds'))
        draw_odds = american_to_decimal(row.get('draw_odds'))
        away_odds = american_to_decimal(row.get('away_odds'))
        payout = parse_percent(row.get('payout')) or 0.0
        if home_odds and draw_odds and away_odds:
            cand = {
                'home_odds': home_odds,
                'draw_odds': draw_odds,
                'away_odds': away_odds,
                'bookmaker': row.get('bookmaker'),
                'payout': payout,
            }
            if best is None or cand['payout'] > best['payout']:
                best = cand
    if best is None:
        return {
            'home_odds': None,
            'draw_odds': None,
            'away_odds': None,
            'one_x2_bookmaker': None,
        }
    return {
        'home_odds': best['home_odds'],
        'draw_odds': best['draw_odds'],
        'away_odds': best['away_odds'],
        'one_x2_bookmaker': best['bookmaker'],
    }


def main() -> None:
    args = parse_args()
    discovery = discover_routes(args.league)
    results_url = resolve_results_url(discovery, args.season)
    bootstrapper = OddsPortalBootstrapper(headless=not args.headed)
    all_rows: List[Dict[str, Any]] = []
    match_logs: List[Dict[str, Any]] = []
    slug = LEAGUES[args.league]['slug']
    season = str(args.season)
    seen_urls = set()
    try:
        for page_num in range(args.page_start, args.page_end + 1):
            page_probe = bootstrapper.scrape_results_page(results_url, page_number=page_num, limit=5)
            urls = page_probe.get('all_visible_match_urls', []) or []
            for match_url in urls:
                if len(all_rows) >= args.max_matches:
                    break
                if not match_url or match_url in seen_urls:
                    continue
                if slug not in match_url or season not in match_url:
                    continue
                seen_urls.add(match_url)
                try:
                    market_probe = bootstrapper.scrape_match_markets(match_url)
                    participants = market_probe.get('participants_raw', {})
                    ou = market_probe.get('over_under', {})
                    one_x2 = market_probe.get('one_x_two', {})
                    chosen_ou = choose_main_ou_line(ou.get('collapsed_rows', []) or [])
                    if not chosen_ou:
                        match_logs.append({'match_url': match_url, 'status': 'skip_no_ou'})
                        continue
                    one_x2_best = choose_main_1x2(one_x2.get('bookmaker_rows', []) or [])
                    row = {
                        'source': 'oddsportal',
                        'league': args.league,
                        'season': season,
                        'match_url': match_url,
                        'Date': parse_kickoff_date(market_probe.get('kickoff')),
                        'kickoff_raw': market_probe.get('kickoff'),
                        'HomeTeam': normalize_team(participants.get('home_team')),
                        'AwayTeam': normalize_team(participants.get('away_team')),
                        'FTHG': pd.to_numeric(participants.get('home_score'), errors='coerce'),
                        'FTAG': pd.to_numeric(participants.get('away_score'), errors='coerce'),
                        'ou_line': chosen_ou['ou_line'],
                        'over_odds': chosen_ou['over_odds'],
                        'under_odds': chosen_ou['under_odds'],
                        'ou_bookmaker_count': chosen_ou['bookmaker_count'],
                        'ou_payout': chosen_ou['payout'],
                        'ou_fair_over': chosen_ou['fair_over'],
                        'ou_line_raw': chosen_ou['raw'].get('line'),
                        'home_odds': one_x2_best['home_odds'],
                        'draw_odds': one_x2_best['draw_odds'],
                        'away_odds': one_x2_best['away_odds'],
                        'one_x2_bookmaker': one_x2_best['one_x2_bookmaker'],
                    }
                    all_rows.append(row)
                    match_logs.append({
                        'match_url': match_url,
                        'status': 'ok',
                        'ou_line': row['ou_line'],
                        'bookmaker_count': row['ou_bookmaker_count'],
                    })
                    print(f"[OK] {row['HomeTeam']} vs {row['AwayTeam']} | ou_line={row['ou_line']} | {match_url}")
                except Exception as exc:  # noqa: BLE001
                    match_logs.append({'match_url': match_url, 'status': 'error', 'error': str(exc)})
                    print(f"[FAIL] {match_url}: {exc}")
            if len(all_rows) >= args.max_matches:
                break
    finally:
        bootstrapper.close()

    df = pd.DataFrame(all_rows)
    ensure_parent(args.output_csv)
    df.to_csv(args.output_csv, index=False, encoding='utf-8-sig')

    payload = {
        'league': args.league,
        'season': season,
        'results_url': results_url,
        'page_start': args.page_start,
        'page_end': args.page_end,
        'max_matches': args.max_matches,
        'collected_rows': int(len(df)),
        'output_csv': args.output_csv,
        'logs': match_logs,
    }
    if args.output_json:
        ensure_parent(args.output_json)
        with open(args.output_json, 'w', encoding='utf-8') as f:
            json.dump(payload, f, ensure_ascii=False, indent=2)
    print(json.dumps(payload, ensure_ascii=False, indent=2))


if __name__ == '__main__':
    main()
