"""
Quantitative Finance Module — V2 Betting Engine
Edge calculation, Fractional Kelly Criterion staking, bet grading, and risk assessment.
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from typing import Any


# ═══════════════════════════════════════════════════════════════════════════
#  Constants
# ═══════════════════════════════════════════════════════════════════════════

BANKROLL_UNITS: float = 10.0      # Total bankroll in abstract units
KELLY_FRACTION: float = 0.25     # Quarter-Kelly (conservative, anti-ruin)
MIN_EDGE_PLAYABLE: float = 0.05  # 5% edge minimum to mark as playable
MIN_ODDS_PLAYABLE: float = 1.30  # Skip extreme chalk below 1.30


# ═══════════════════════════════════════════════════════════════════════════
#  Edge Calculation
# ═══════════════════════════════════════════════════════════════════════════

def calculate_edge(true_prob: float, decimal_odds: float) -> float:
    """
    Edge = (True_Probability × Decimal_Odds) - 1.0
    Positive edge → the model says we have an advantage over the bookmaker.
    """
    if decimal_odds <= 1.0 or true_prob <= 0.0:
        return -1.0
    return round((true_prob * decimal_odds) - 1.0, 4)


# ═══════════════════════════════════════════════════════════════════════════
#  Kelly Criterion Staking
# ═══════════════════════════════════════════════════════════════════════════

def kelly_stake(true_prob: float, decimal_odds: float) -> float:
    """
    Fractional Kelly Criterion for a bankroll of BANKROLL_UNITS.

    Full Kelly: f* = ((b × p) - q) / b
      where b = decimal_odds - 1, p = true_prob, q = 1 - true_prob

    We use KELLY_FRACTION (25%) to reduce variance and avoid ruin.
    Returns stake in units, rounded to 0.1.
    """
    if decimal_odds <= 1.0 or true_prob <= 0.0 or true_prob >= 1.0:
        return 0.0

    b = decimal_odds - 1.0
    p = true_prob
    q = 1.0 - p

    f_star = ((b * p) - q) / b

    if f_star <= 0.0:
        return 0.0

    # Scale by fraction and bankroll
    stake = f_star * KELLY_FRACTION * BANKROLL_UNITS

    # Cap at a sensible maximum (3 units on a 10-unit bankroll)
    stake = min(stake, 3.0)

    return round(max(0.0, stake), 1)


# ═══════════════════════════════════════════════════════════════════════════
#  Bet Grading
# ═══════════════════════════════════════════════════════════════════════════

def grade_bet(edge: float, playable: bool) -> str:
    """
    Assign a letter grade based on edge magnitude.
    A: Edge > 10% — Elite value, rare
    B: Edge > 5%  — Strong value, core bets
    C: Edge > 2%  — Marginal value, supporting picks only
    PASS: Below threshold — Do not bet
    """
    if not playable or edge < 0.02:
        return "PASS"
    if edge > 0.10:
        return "A"
    if edge > 0.05:
        return "B"
    return "C"


def is_playable(edge: float, decimal_odds: float) -> bool:
    """A pick is playable if it has sufficient edge AND reasonable odds."""
    return edge >= MIN_EDGE_PLAYABLE and decimal_odds >= MIN_ODDS_PLAYABLE


# ═══════════════════════════════════════════════════════════════════════════
#  Play Score (0-100 composite)
# ═══════════════════════════════════════════════════════════════════════════

def calculate_play_score(
    edge: float,
    true_prob: float,
    data_quality: float,
) -> float:
    """
    Composite score combining edge strength, probability confidence,
    and data quality. Used for ranking picks and filtering.

    Components:
      - Edge contribution (0-50):  edge * 250, capped at 50
      - Prob contribution (0-30):  probability * 30
      - DQ contribution (0-20):    data_quality * 20
    """
    edge_score = min(50.0, max(0.0, edge * 250.0))
    prob_score = min(30.0, max(0.0, true_prob * 30.0))
    dq_score = min(20.0, max(0.0, data_quality * 20.0))
    return round(edge_score + prob_score + dq_score, 1)


# ═══════════════════════════════════════════════════════════════════════════
#  Risk Assessment
# ═══════════════════════════════════════════════════════════════════════════

@dataclass
class RiskResult:
    level: str          # LOW, MEDIUM, HIGH, EXTREME
    score: float        # 0.0 - 1.0
    is_surprise_risk: bool
    surprise_type: str | None
    warnings: list[str]


def assess_risk(
    missing_players_impact: float,
    data_quality_score: float,
    elo_diff: float,
    implied_prob_fav: float,
) -> RiskResult:
    """
    Multi-factor risk assessment.

    Factors:
    1. Missing key players (injuries/suspensions)
    2. Data quality (missing stats, odds)
    3. ELO closeness (tight matches are riskier)
    4. Surprise potential (heavy favorite vulnerable)
    """
    warnings: list[str] = []
    risk_score = 0.0

    # ─── Factor 1: Missing players ────────────────────────────────────
    if missing_players_impact > 0.3:
        risk_score += 0.35
        warnings.append(
            f"High missing-player impact: {missing_players_impact:.2f}"
        )
    elif missing_players_impact > 0.15:
        risk_score += 0.15
        warnings.append(
            f"Moderate missing-player impact: {missing_players_impact:.2f}"
        )

    # ─── Factor 2: Data quality ───────────────────────────────────────
    if data_quality_score < 0.5:
        risk_score += 0.25
        warnings.append(
            f"Low data quality: {data_quality_score:.2f}"
        )
    elif data_quality_score < 0.75:
        risk_score += 0.10

    # ─── Factor 3: ELO closeness ──────────────────────────────────────
    abs_elo_diff = abs(elo_diff)
    if abs_elo_diff < 50:
        risk_score += 0.15
        warnings.append("Very tight ELO difference — coin-flip territory")
    elif abs_elo_diff < 100:
        risk_score += 0.05

    # ─── Factor 4: Surprise detection ─────────────────────────────────
    is_surprise = False
    surprise_type: str | None = None

    if implied_prob_fav > 0.65 and abs_elo_diff < 80:
        # Heavy favorite by odds but ELO says match is closer
        is_surprise = True
        surprise_type = "odds_elo_divergence"
        risk_score += 0.15
        warnings.append(
            "Upset potential: bookmaker odds suggest heavy favorite "
            "but ELO says the match is closer than the market thinks"
        )

    # ─── Classify ─────────────────────────────────────────────────────
    risk_score = min(1.0, risk_score)
    if risk_score >= 0.7:
        level = "EXTREME"
    elif risk_score >= 0.45:
        level = "HIGH"
    elif risk_score >= 0.2:
        level = "MEDIUM"
    else:
        level = "LOW"

    return RiskResult(
        level=level,
        score=round(risk_score, 3),
        is_surprise_risk=is_surprise,
        surprise_type=surprise_type,
        warnings=warnings,
    )


# ═══════════════════════════════════════════════════════════════════════════
#  Market Analysis (orchestrates edge/kelly/grade per market)
# ═══════════════════════════════════════════════════════════════════════════

@dataclass
class MarketPick:
    market: str
    pick: str
    probability: float
    odds: float
    edge: float
    playable: bool
    bet_grade: str
    stake_units: float
    play_score: float
    decision_reasons: list[str]


def analyze_market(
    market: str,
    probs: dict[str, float],
    odds_map: dict[str, float],
    data_quality_score: float,
) -> MarketPick:
    """
    For a given market (MS, OU25, BTTS), find the best pick,
    calculate edge, kelly stake, and grade it.

    Parameters:
        market: "MS", "OU25", "BTTS"
        probs: {"1": 0.55, "X": 0.25, "2": 0.20} — calibrated model probs
        odds_map: {"1": 2.10, "X": 3.40, "2": 3.50} — decimal odds
        data_quality_score: 0.0-1.0
    """
    best_pick: str = ""
    best_edge: float = -99.0
    best_prob: float = 0.0
    best_odds: float = 0.0
    reasons: list[str] = []

    for pick_name, prob in probs.items():
        odd = odds_map.get(pick_name, 0.0)
        if odd <= 1.0:
            continue

        edge = calculate_edge(prob, odd)
        if edge > best_edge:
            best_edge = edge
            best_pick = pick_name
            best_prob = prob
            best_odds = odd

    if not best_pick:
        return MarketPick(
            market=market, pick="", probability=0.0, odds=0.0,
            edge=0.0, playable=False, bet_grade="PASS",
            stake_units=0.0, play_score=0.0,
            decision_reasons=["no_valid_odds_found"],
        )

    playable = is_playable(best_edge, best_odds)
    grade = grade_bet(best_edge, playable)
    stake = kelly_stake(best_prob, best_odds) if playable else 0.0
    play_score = calculate_play_score(best_edge, best_prob, data_quality_score)

    # Build decision reasons
    if playable:
        reasons.append(f"edge_{best_edge:.1%}_above_threshold")
        reasons.append(f"kelly_stake_{stake:.1f}_units")
    else:
        if best_edge < MIN_EDGE_PLAYABLE:
            reasons.append(f"edge_{best_edge:.1%}_below_{MIN_EDGE_PLAYABLE:.0%}_threshold")
        if best_odds < MIN_ODDS_PLAYABLE:
            reasons.append(f"odds_{best_odds:.2f}_below_{MIN_ODDS_PLAYABLE:.2f}_minimum")

    return MarketPick(
        market=market,
        pick=best_pick,
        probability=round(best_prob, 4),
        odds=round(best_odds, 2),
        edge=round(best_edge, 4),
        playable=playable,
        bet_grade=grade,
        stake_units=stake,
        play_score=play_score,
        decision_reasons=reasons,
    )
