"""
=========================================
CV6 AI Trading OS — Options Pricing Engine
=========================================
Black-Scholes model:
  • Fair value (theoretical price)
  • Delta  — price sensitivity
  • Gamma  — delta rate of change
  • Theta  — time decay per day (₹ lost per day)
  • Vega   — volatility sensitivity
  • IV calculation from market price

Usage:
    from app.fno.options_pricing import price_option, OptionGreeks
=========================================
"""
from __future__ import annotations

import math
from dataclasses import dataclass
from typing import Optional


# ── Standard Normal helpers ───────────────────────────────────────────────────

def _norm_cdf(x: float) -> float:
    """Cumulative standard normal distribution."""
    return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0)))


def _norm_pdf(x: float) -> float:
    """Standard normal probability density function."""
    return math.exp(-0.5 * x * x) / math.sqrt(2.0 * math.pi)


# ── Greeks dataclass ──────────────────────────────────────────────────────────

@dataclass
class OptionGreeks:
    fair_value:  float   # Theoretical price ₹
    delta:       float   # +0 to +1 for CE, -1 to 0 for PE
    gamma:       float   # Change in delta per ₹1 move
    theta:       float   # ₹ lost per day (always negative)
    vega:        float   # ₹ change per 1% IV change
    iv:          float   # Implied Volatility used (%)
    intrinsic:   float   # Max(0, S-K) for CE, Max(0, K-S) for PE
    time_value:  float   # fair_value - intrinsic
    moneyness:   str     # ITM | ATM | OTM
    days_to_exp: float


# ── Black-Scholes ─────────────────────────────────────────────────────────────

def price_option(
    spot:        float,    # Current index/stock price
    strike:      float,    # Option strike price
    days_to_exp: float,    # Days remaining to expiry
    iv_pct:      float,    # Implied Volatility in % (e.g. 15.0 for 15%)
    option_type: str,      # "CE" or "PE"
    risk_free:   float = 6.5,   # India risk-free rate % (repo rate approx)
) -> OptionGreeks:
    """
    Black-Scholes option pricing for Indian index options.

    Returns OptionGreeks with fair value + all greeks.
    """
    ot  = option_type.upper()
    S   = float(spot)
    K   = float(strike)
    T   = max(days_to_exp, 0.001) / 365.0   # time in years
    r   = risk_free / 100.0
    sig = iv_pct / 100.0

    if sig <= 0 or S <= 0 or K <= 0:
        return _zero_greeks(option_type, days_to_exp)

    # d1, d2
    d1 = (math.log(S / K) + (r + 0.5 * sig * sig) * T) / (sig * math.sqrt(T))
    d2 = d1 - sig * math.sqrt(T)

    # Fair value
    if ot == "CE":
        fair_value  = S * _norm_cdf(d1) - K * math.exp(-r * T) * _norm_cdf(d2)
        delta       = _norm_cdf(d1)
        intrinsic   = max(0.0, S - K)
    else:
        fair_value  = K * math.exp(-r * T) * _norm_cdf(-d2) - S * _norm_cdf(-d1)
        delta       = _norm_cdf(d1) - 1.0
        intrinsic   = max(0.0, K - S)

    # Greeks
    gamma = _norm_pdf(d1) / (S * sig * math.sqrt(T))

    # Theta per calendar day
    theta_annual = (
        -(S * _norm_pdf(d1) * sig) / (2 * math.sqrt(T))
        - r * K * math.exp(-r * T) * (_norm_cdf(d2) if ot == "CE" else _norm_cdf(-d2))
    )
    theta = theta_annual / 365.0   # per day

    # Vega per 1% IV change
    vega = S * _norm_pdf(d1) * math.sqrt(T) / 100.0

    time_value = max(0.0, fair_value - intrinsic)

    # Moneyness
    if abs(S - K) / K < 0.005:
        moneyness = "ATM"
    elif (ot == "CE" and S > K) or (ot == "PE" and S < K):
        moneyness = "ITM"
    else:
        moneyness = "OTM"

    return OptionGreeks(
        fair_value  = round(fair_value,  2),
        delta       = round(delta,        4),
        gamma       = round(gamma,        6),
        theta       = round(theta,        2),
        vega        = round(vega,         2),
        iv          = round(iv_pct,       2),
        intrinsic   = round(intrinsic,    2),
        time_value  = round(time_value,   2),
        moneyness   = moneyness,
        days_to_exp = days_to_exp,
    )


def _zero_greeks(option_type: str, days_to_exp: float) -> OptionGreeks:
    return OptionGreeks(
        fair_value=0.0, delta=0.0, gamma=0.0,
        theta=0.0, vega=0.0, iv=0.0,
        intrinsic=0.0, time_value=0.0,
        moneyness="N/A", days_to_exp=days_to_exp,
    )


# ── Implied Volatility solver (Newton-Raphson) ────────────────────────────────

def calc_iv(
    market_price: float,
    spot:         float,
    strike:       float,
    days_to_exp:  float,
    option_type:  str,
    risk_free:    float = 6.5,
    max_iter:     int   = 100,
) -> Optional[float]:
    """
    Reverse-solve IV from market price using Newton-Raphson.
    Returns IV in % or None if failed.
    """
    ot  = option_type.upper()
    low, high = 0.1, 500.0   # IV search range 0.1% to 500%
    iv  = 30.0                # initial guess

    for _ in range(max_iter):
        g = price_option(spot, strike, days_to_exp, iv, ot, risk_free)
        price_diff = g.fair_value - market_price

        if abs(price_diff) < 0.01:
            return round(iv, 2)

        # Use vega (∂price/∂iv) for Newton step
        vega_raw = g.vega * 100   # convert back to per 100% IV
        if abs(vega_raw) < 1e-8:
            break

        iv -= price_diff / vega_raw

        if iv < low:
            iv = low
        if iv > high:
            iv = high

    # Bisection fallback
    for _ in range(50):
        mid = (low + high) / 2.0
        g   = price_option(spot, strike, days_to_exp, mid, ot, risk_free)
        if abs(g.fair_value - market_price) < 0.01:
            return round(mid, 2)
        if g.fair_value < market_price:
            low = mid
        else:
            high = mid

    return None


# ── Max Pain calculator ───────────────────────────────────────────────────────

def calc_max_pain(oi_data: dict) -> Optional[float]:
    """
    Max Pain = strike where option writers lose least money.
    oi_data: { strike: { "call_oi": int, "put_oi": int } }
    Returns max pain strike.
    """
    if not oi_data:
        return None

    strikes = sorted(oi_data.keys())
    min_pain = float("inf")
    max_pain_strike = None

    for test_strike in strikes:
        total_loss = 0
        for s in strikes:
            c_oi = oi_data[s].get("call_oi", 0)
            p_oi = oi_data[s].get("put_oi",  0)
            # Call writers lose when test_strike > s
            if test_strike > s:
                total_loss += c_oi * (test_strike - s)
            # Put writers lose when test_strike < s
            if test_strike < s:
                total_loss += p_oi * (s - test_strike)

        if total_loss < min_pain:
            min_pain         = total_loss
            max_pain_strike  = test_strike

    return max_pain_strike


# ── Quick summary for AI prompt ───────────────────────────────────────────────

def greeks_summary(g: OptionGreeks, lot_size: int = 75) -> str:
    """One-line summary for AI context."""
    return (
        f"Fair Value=₹{g.fair_value:.2f} | "
        f"Delta={g.delta:+.3f} | "
        f"Theta=₹{g.theta:.2f}/day | "
        f"Vega=₹{g.vega:.2f}/1%IV | "
        f"IV={g.iv:.1f}% | "
        f"{g.moneyness} | "
        f"Theta/lot=₹{g.theta * lot_size:.0f}/day"
    )
