from typing import List, Optional, Dict
from datetime import datetime, timedelta
import re
from core.models import Event, Outcome, ArbOpportunity
from fuzzywuzzy import fuzz
import config
import logging

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Event category detection (prevents cross-gender / cross-age-group matches)
# ---------------------------------------------------------------------------

_WOMENS_WORDS = frozenset(['women', "women's", 'woman', 'ladies', 'lady', 'female', 'girls', 'dames', 'damen'])
_YOUTH_RE = re.compile(r'\bu(1[5-9]|2[0-3])\b')   # U15–U23


def _event_category(event: Event):
    """Return (is_womens: bool, youth_group: str|None) derived from league + team names."""
    text = f"{event.league} {event.home_team} {event.away_team}".lower()
    is_womens = any(w in text.split() or w in text for w in _WOMENS_WORDS)
    m = _YOUTH_RE.search(text)
    return (is_womens, m.group(0) if m else None)


def _same_category(a: Event, b: Event) -> bool:
    """Return False if the two events are from different gender/age-group categories."""
    return _event_category(a) == _event_category(b)


# ---------------------------------------------------------------------------
# Arbitrage maths
# ---------------------------------------------------------------------------

def arb_margin(odds: List[float]) -> Optional[float]:
    """Return profit % if arbitrage exists, else None."""
    inverse_sum = sum(1.0 / o for o in odds)
    if inverse_sum < 1.0:
        return ((1.0 / inverse_sum) - 1.0) * 100
    return None


def optimal_stakes(total_stake: float, outcomes: List[Outcome]) -> List[dict]:
    """
    Calculate stakes that guarantee the same return regardless of result.

    stake_i = total_stake / (odds_i * sum(1/odds_j))
    guaranteed_return = total_stake / sum(1/odds_j)
    """
    inv_sum = sum(1.0 / o.odds for o in outcomes)
    guaranteed = total_stake / inv_sum

    result = []
    for o in outcomes:
        stake = guaranteed / o.odds
        result.append({
            'bookmaker': o.bookmaker,
            'outcome': o.name,
            'odds': o.odds,
            'stake': round(stake, 2),
            'potential_return': round(stake * o.odds, 2),
        })
    return result


# ---------------------------------------------------------------------------
# Event matching across bookmakers
# ---------------------------------------------------------------------------

def _times_close(t1: Optional[datetime], t2: Optional[datetime]) -> bool:
    if t1 is None or t2 is None:
        return True
    # Strip timezone info so naive and aware datetimes can be compared
    t1 = t1.replace(tzinfo=None)
    t2 = t2.replace(tzinfo=None)
    diff = abs((t1 - t2).total_seconds()) / 60
    return diff <= config.TIME_TOLERANCE_MINUTES


def _teams_match(a: Event, b: Event) -> bool:
    """Fuzzy name match — handles slight spelling differences across sites."""
    score = fuzz.token_sort_ratio(a.match_key, b.match_key)
    return score >= 75


def group_events_by_match(all_events: List[Event]) -> Dict[str, List[Event]]:
    """
    Group events from different bookmakers that represent the same match.
    Uses the normalized match_key first; falls back to fuzzy matching.
    """
    groups: Dict[str, List[Event]] = {}

    for event in all_events:
        key = event.match_key
        placed = False

        # Try exact key match first — add any event with the same key
        if key in groups:
            ref = groups[key][0]
            if _times_close(ref.starts_at, event.starts_at) and _same_category(ref, event):
                groups[key].append(event)
                placed = True

        if not placed:
            # Fuzzy fallback for slight spelling differences
            for gkey, gevents in groups.items():
                ref = gevents[0]
                if (ref.sport == event.sport
                        and _times_close(ref.starts_at, event.starts_at)
                        and _same_category(ref, event)
                        and _teams_match(ref, event)):
                    gevents.append(event)
                    placed = True
                    break

        if not placed:
            groups[key] = [event]

    return groups


# ---------------------------------------------------------------------------
# Main pipeline
# ---------------------------------------------------------------------------

def find_arb_opportunities(events_by_bookmaker: Dict[str, List[Event]]) -> List[ArbOpportunity]:
    """Find all arbitrage opportunities across bookmakers."""
    all_events = [e for events in events_by_bookmaker.values() for e in events]
    groups = group_events_by_match(all_events)

    opportunities: List[ArbOpportunity] = []

    for match_key, events in groups.items():
        # Need events from at least 2 different bookmakers
        bm_set = {e.bookmaker for e in events}
        if len(bm_set) < 2:
            continue

        # Collect all markets present
        markets = {e.market for e in events}

        for market in markets:
            market_events = [e for e in events if e.market == market]
            bm_in_market = {e.bookmaker for e in market_events}
            if len(bm_in_market) < 2:
                continue

            # All distinct outcome names in this market
            outcome_names = {o.name for e in market_events for o in e.outcomes}

            # Best odds for each outcome (across all bookmakers)
            best: Dict[str, Outcome] = {}
            for name in outcome_names:
                for event in market_events:
                    for outcome in event.outcomes:
                        if outcome.name == name:
                            if name not in best or outcome.odds > best[name].odds:
                                best[name] = outcome

            if len(best) != len(outcome_names):
                continue

            # Best odds must come from at least 2 different bookmakers
            if len({o.bookmaker for o in best.values()}) < 2:
                continue

            best_outcomes = list(best.values())
            odds_vals = [o.odds for o in best_outcomes]
            pct = arb_margin(odds_vals)

            if pct is not None and config.MIN_ARB_PERCENTAGE <= pct <= config.MAX_ARB_PERCENTAGE:
                ref = market_events[0]
                stakes = optimal_stakes(100, best_outcomes)
                opportunities.append(ArbOpportunity(
                    sport=ref.sport,
                    event_name=f"{ref.home_team} vs {ref.away_team}",
                    league=ref.league,
                    market=market,
                    outcomes=stakes,
                    arb_percentage=pct,
                    profit_per_100=pct,
                    starts_at=ref.starts_at,
                ))

    opportunities.sort(key=lambda x: x.arb_percentage, reverse=True)
    logger.info(f"Arb scan complete: {len(opportunities)} opportunities found")
    return opportunities
