"""
iv_extractor.py
===============
Core module — NBA Implied Volatility

Extracts implied volatility (σ_implied) from bookmaker O/U prices
by inverting a simplified Black-Scholes framework adapted to player
performance distributions.

Concept:
    A bookmaker's O/U line is treated as a financial strike (K).
    The player's rolling average is the spot price (S).
    The market probability embedded in the odds is used to back out
    the implied volatility, exactly as one inverts BS to find IV from
    an option price.

Edge:
    When σ_implied > σ_realized → bookmaker overprices variance
    → vol selling opportunity (bet Under on overhyped players)

    When σ_implied < σ_realized → bookmaker underprices variance
    → vol buying opportunity (bet volatile/unpredictable players)
"""

import numpy as np
from scipy.optimize import brentq
from scipy.stats import norm
from dataclasses import dataclass


# ─────────────────────────────────────────────
# Data structures
# ─────────────────────────────────────────────

@dataclass
class IVResult:
    player_id:          int
    player_name:        str
    stat:               str
    spot:               float
    strike:             float
    moneyness:          float
    prob_over_implied:  float
    sigma_implied:      float
    sigma_realized:     float
    spread:             float
    signal:             str
    confidence:         float


@dataclass
class OddsLine:
    over_american:  int
    under_american: int
    line:           float


# ─────────────────────────────────────────────
# Odds utilities
# ─────────────────────────────────────────────

def american_to_prob(american_odds: int) -> float:
    if american_odds < 0:
        return abs(american_odds) / (abs(american_odds) + 100)
    return 100 / (american_odds + 100)


def remove_vig(prob_over: float, prob_under: float) -> tuple[float, float]:
    total = prob_over + prob_under
    return prob_over / total, prob_under / total


def extract_fair_prob(odds: OddsLine) -> float:
    raw_over  = american_to_prob(odds.over_american)
    raw_under = american_to_prob(odds.under_american)
    fair_over, _ = remove_vig(raw_over, raw_under)
    return fair_over


# ─────────────────────────────────────────────
# Black-Scholes adapted to NBA O/U
# ─────────────────────────────────────────────

def bs_prob_over(S: float, K: float, sigma: float) -> float:
    if sigma <= 0:
        return 1.0 if S > K else 0.0
    d = (S - K) / sigma
    return float(norm.cdf(d))


def extract_implied_vol(
    S: float,
    K: float,
    market_prob: float,
    sigma_low:  float = 0.5,
    sigma_high: float = 60.0,
    tolerance:  float = 1e-6
) -> float:
    def objective(sigma: float) -> float:
        return bs_prob_over(S, K, sigma) - market_prob
    try:
        return brentq(objective, sigma_low, sigma_high, xtol=tolerance)
    except ValueError:
        raise ValueError(
            f"No IV solution found for S={S}, K={K}, prob={market_prob:.3f}."
        )


# ─────────────────────────────────────────────
# Realized volatility
# ─────────────────────────────────────────────

def compute_realized_vol(
    performances: np.ndarray,
    decay: float = 0.94
) -> float:
    n = len(performances)
    if n < 3:
        return float(np.std(performances))
    weights = np.array([decay ** i for i in range(n)])[::-1]
    weights /= weights.sum()
    mean = np.average(performances, weights=weights)
    variance = np.average((performances - mean) ** 2, weights=weights)
    return float(np.sqrt(variance))


# ─────────────────────────────────────────────
# Edge detector
# ─────────────────────────────────────────────

EDGE_THRESHOLD  = 0.15
CONFIDENCE_SCALE = 2.0

def detect_edge(
    sigma_implied:   float,
    sigma_realized:  float,
    sentiment_score: float = 0.0
) -> tuple[str, float]:
    spread = sigma_implied - sigma_realized
    abs_spread = abs(spread)

    if abs_spread < EDGE_THRESHOLD:
        return "neutral", 0.0

    signal = "vol_selling" if spread > 0 else "vol_buying"

    sentiment_alignment = sentiment_score if signal == "vol_selling" else -sentiment_score
    sentiment_boost = max(0.0, sentiment_alignment)

    raw_confidence = min(abs_spread / CONFIDENCE_SCALE, 1.0)
    confidence = min(raw_confidence * (1 + 0.5 * sentiment_boost), 1.0)

    return signal, round(confidence, 3)


# ─────────────────────────────────────────────
# Main interface
# ─────────────────────────────────────────────

def analyze_player(
    player_id:          int,
    player_name:        str,
    stat:               str,
    rolling_avg:        float,
    past_performances:  np.ndarray,
    odds:               OddsLine,
    sentiment_score:    float = 0.0,
    decay:              float = 0.94
) -> IVResult:
    S = rolling_avg
    K = odds.line

    fair_prob      = extract_fair_prob(odds)
    sigma_implied  = extract_implied_vol(S, K, fair_prob)
    sigma_realized = compute_realized_vol(past_performances, decay)

    spread = sigma_implied - sigma_realized
    signal, confidence = detect_edge(sigma_implied, sigma_realized, sentiment_score)

    return IVResult(
        player_id         = player_id,
        player_name       = player_name,
        stat              = stat,
        spot              = round(S, 2),
        strike            = K,
        moneyness         = round(S / K, 3),
        prob_over_implied = round(fair_prob, 3),
        sigma_implied     = round(sigma_implied, 3),
        sigma_realized    = round(sigma_realized, 3),
        spread            = round(spread, 3),
        signal            = signal,
        confidence        = confidence
    )


# ─────────────────────────────────────────────
# Quick demo
# ─────────────────────────────────────────────

if __name__ == "__main__":
    import json

    past_pts = np.array([31, 24, 28, 35, 22, 29, 33, 26, 30, 28,
                         25, 32, 27, 24, 31], dtype=float)

    result = analyze_player(
        player_id         = 2544,
        player_name       = "LeBron James",
        stat              = "PTS",
        rolling_avg       = float(np.mean(past_pts[-10:])),
        past_performances = past_pts,
        odds              = OddsLine(over_american=-130, under_american=+108, line=27.5),
        sentiment_score   = 0.72,
        decay             = 0.94
    )

    print(json.dumps(result.__dict__, indent=2))