//! Options/derivatives overlay for prediction markets.
//!
//! Builds synthetic option chains, volatility surfaces, term structures,
//! and spread strategies from prediction market prices.

use pyo3::prelude::*;

/// A synthetic option derived from prediction market prices.
#[pyclass(frozen, get_all)]
#[derive(Debug, Clone)]
pub struct SyntheticOption {
    pub market_id: String,
    pub strike: f64,
    pub expiry_days: f64,
    pub implied_vol: f64,
    pub delta: f64,
    pub gamma: f64,
    pub theta: f64,
    pub vega: f64,
    pub price: f64,
}

#[pymethods]
impl SyntheticOption {
    fn __repr__(&self) -> String {
        format!(
            "SyntheticOption(strike={:.2}, iv={:.4}, delta={:.4}, price={:.4})",
            self.strike, self.implied_vol, self.delta, self.price
        )
    }
}

/// A point on the volatility surface.
#[pyclass(frozen, get_all)]
#[derive(Debug, Clone)]
pub struct VolSurfacePoint {
    pub strike: f64,
    pub expiry_days: f64,
    pub implied_vol: f64,
}

#[pymethods]
impl VolSurfacePoint {
    #[new]
    fn new(strike: f64, expiry_days: f64, implied_vol: f64) -> Self {
        Self { strike, expiry_days, implied_vol }
    }

    fn __repr__(&self) -> String {
        format!("VolSurfacePoint(K={:.2}, T={:.0}d, iv={:.4})", self.strike, self.expiry_days, self.implied_vol)
    }
}

/// A point on the term structure.
#[pyclass(frozen, get_all)]
#[derive(Debug, Clone)]
pub struct TermStructurePoint {
    pub expiry_days: f64,
    pub implied_vol: f64,
    pub price: f64,
}

#[pymethods]
impl TermStructurePoint {
    #[new]
    fn new(expiry_days: f64, implied_vol: f64, price: f64) -> Self {
        Self { expiry_days, implied_vol, price }
    }

    fn __repr__(&self) -> String {
        format!("TermStructurePoint(T={:.0}d, iv={:.4}, price={:.4})", self.expiry_days, self.implied_vol, self.price)
    }
}

/// Spread strategy type.
#[pyclass(eq, eq_int, frozen, hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpreadStrategy {
    Butterfly = 0,
    Straddle = 1,
    CalendarSpread = 2,
}

#[pymethods]
impl SpreadStrategy {
    fn __repr__(&self) -> String {
        match self {
            Self::Butterfly => "SpreadStrategy.Butterfly",
            Self::Straddle => "SpreadStrategy.Straddle",
            Self::CalendarSpread => "SpreadStrategy.CalendarSpread",
        }
        .to_string()
    }
    fn __str__(&self) -> String { self.__repr__() }
}

#[inline]
fn finite_or_zero(v: f64) -> f64 {
    if v.is_finite() { v } else { 0.0 }
}

/// Build a synthetic options chain from prediction market prices.
///
/// Treats each market price as a binary option and computes implied Greeks.
#[pyfunction]
pub fn build_options_chain(
    market_ids: Vec<String>,
    prices: Vec<f64>,
    expiry_days: Vec<f64>,
) -> Vec<SyntheticOption> {
    let n = market_ids.len().min(prices.len()).min(expiry_days.len());
    let mut chain = Vec::with_capacity(n);

    for i in 0..n {
        let p = prices[i].clamp(0.001, 0.999);
        let t = expiry_days[i].max(0.01);
        let t_years = t / 365.0;

        // Implied vol from binary option pricing: p = N(d2)
        let iv = implied_vol_binary(p, t_years);

        // Binary option Greeks using d2 = Phi^{-1}(p)
        //
        // For a binary (digital) call paying $1 if S > K at expiry:
        //   price = N(d2), so d2 = N^{-1}(p)
        //   phi(d2) = standard normal PDF evaluated at d2
        //
        // Delta = phi(d2) / (S * sigma * sqrt(T))
        //   In prediction markets we normalize S=1, so delta = phi(d2) / (sigma * sqrt(T))
        // Gamma = -phi(d2) * d2 / (S^2 * sigma^2 * T)
        //   Normalized: gamma = -phi(d2) * d2 / (sigma^2 * T)
        // Theta = phi(d2) * d2 / (2*T)  (simplified with r=0)
        // Vega  = -phi(d2) * d2 * sqrt(T)
        //   (sensitivity of binary price to vol; negative d2 means positive vega for OTM calls)

        let d2 = norm_inv(p);
        let phi_d2 = norm_pdf(d2);
        let iv_sqrt_t = iv * t_years.sqrt();

        let delta = if iv_sqrt_t > 1e-10 {
            finite_or_zero(phi_d2 / iv_sqrt_t)
        } else {
            0.0
        };

        let gamma = if iv > 1e-10 && t_years > 1e-10 {
            finite_or_zero(-phi_d2 * d2 / (iv * iv * t_years))
        } else {
            0.0
        };

        let theta = if t_years > 1e-10 {
            finite_or_zero(phi_d2 * d2 / (2.0 * t_years))
        } else {
            0.0
        };

        let vega = finite_or_zero(-phi_d2 * d2 * t_years.sqrt());

        chain.push(SyntheticOption {
            market_id: market_ids[i].clone(),
            strike: 0.5, // binary options have effective strike at 0.5
            expiry_days: t,
            implied_vol: iv,
            delta: delta.clamp(0.0, 100.0),
            gamma: gamma.clamp(-100.0, 100.0),
            theta: theta.clamp(-100.0, 100.0),
            vega: vega.clamp(-100.0, 100.0),
            price: p,
        });
    }

    chain
}

/// Build a volatility surface from prices at different strikes and expiries.
#[pyfunction]
pub fn vol_surface(
    strikes: Vec<f64>,
    expiry_days: Vec<f64>,
    prices: Vec<f64>,
) -> Vec<VolSurfacePoint> {
    let n = strikes.len().min(expiry_days.len()).min(prices.len());
    let mut surface = Vec::with_capacity(n);

    for i in 0..n {
        let p = prices[i].clamp(0.001, 0.999);
        let t = expiry_days[i].max(0.01) / 365.0;
        let iv = implied_vol_binary(p, t);
        surface.push(VolSurfacePoint {
            strike: strikes[i],
            expiry_days: expiry_days[i],
            implied_vol: iv,
        });
    }

    surface
}

/// Build a term structure from prices at different expiries.
#[pyfunction]
pub fn term_structure(
    expiry_days: Vec<f64>,
    prices: Vec<f64>,
) -> Vec<TermStructurePoint> {
    let n = expiry_days.len().min(prices.len());
    let mut ts = Vec::with_capacity(n);

    for i in 0..n {
        let p = prices[i].clamp(0.001, 0.999);
        let t = expiry_days[i].max(0.01) / 365.0;
        let iv = implied_vol_binary(p, t);
        ts.push(TermStructurePoint {
            expiry_days: expiry_days[i],
            implied_vol: iv,
            price: prices[i],
        });
    }

    // Sort by expiry
    ts.sort_by(|a, b| a.expiry_days.partial_cmp(&b.expiry_days).unwrap_or(std::cmp::Ordering::Equal));
    ts
}

/// Build synthetic butterfly spread legs.
///
/// Returns (lower_buy, center_sell_2x, upper_buy) as (market_id, side, size) tuples.
#[pyfunction]
pub fn synthetic_butterfly(
    lower_market: String,
    center_market: String,
    upper_market: String,
    size: f64,
) -> Vec<(String, String, f64)> {
    vec![
        (lower_market, "buy".to_string(), size),
        (center_market, "sell".to_string(), size * 2.0),
        (upper_market, "buy".to_string(), size),
    ]
}

/// Build synthetic straddle legs.
///
/// Returns (yes_buy, no_buy) legs for the same market.
#[pyfunction]
pub fn synthetic_straddle(
    market_id: String,
    size: f64,
) -> Vec<(String, String, f64)> {
    vec![
        (market_id.clone(), "buy_yes".to_string(), size),
        (market_id, "buy_no".to_string(), size),
    ]
}

/// Build calendar spread legs between near and far expiry.
#[pyfunction]
pub fn calendar_spread_legs(
    near_market: String,
    far_market: String,
    size: f64,
) -> Vec<(String, String, f64)> {
    vec![
        (near_market, "sell".to_string(), size),
        (far_market, "buy".to_string(), size),
    ]
}

// ---- helpers ----

fn implied_vol_binary(price: f64, t_years: f64) -> f64 {
    // For a binary call: price = N(d2) where d2 = (ln(S/K) + (r - sigma^2/2)*T) / (sigma*sqrt(T))
    // At ATM (S=K, r=0): d2 = -sigma*sqrt(T)/2, so sigma = -2*d2/sqrt(T) = -2*N_inv(p)/sqrt(T)
    if t_years < 1e-10 || price <= 0.0 || price >= 1.0 {
        return 0.0;
    }
    let z = norm_inv(price);
    let iv = (-2.0 * z / t_years.sqrt()).abs();
    finite_or_zero(iv.min(5.0)) // cap at 500% vol
}

fn norm_pdf(x: f64) -> f64 {
    const INV_SQRT_2PI: f64 = 0.398_942_280_401_432_7;
    INV_SQRT_2PI * (-0.5 * x * x).exp()
}

fn norm_inv(p: f64) -> f64 {
    // Rational approximation (Abramowitz & Stegun)
    if p <= 0.0 || p >= 1.0 {
        return 0.0;
    }
    let t = if p < 0.5 {
        (-2.0 * p.ln()).sqrt()
    } else {
        (-2.0 * (1.0 - p).ln()).sqrt()
    };

    let c0 = 2.515517;
    let c1 = 0.802853;
    let c2 = 0.010328;
    let d1 = 1.432788;
    let d2 = 0.189269;
    let d3 = 0.001308;

    let z = t - (c0 + c1 * t + c2 * t * t) / (1.0 + d1 * t + d2 * t * t + d3 * t * t * t);

    if p < 0.5 { -z } else { z }
}

pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<SyntheticOption>()?;
    m.add_class::<VolSurfacePoint>()?;
    m.add_class::<TermStructurePoint>()?;
    m.add_class::<SpreadStrategy>()?;
    m.add_function(wrap_pyfunction!(build_options_chain, m)?)?;
    m.add_function(wrap_pyfunction!(vol_surface, m)?)?;
    m.add_function(wrap_pyfunction!(term_structure, m)?)?;
    m.add_function(wrap_pyfunction!(synthetic_butterfly, m)?)?;
    m.add_function(wrap_pyfunction!(synthetic_straddle, m)?)?;
    m.add_function(wrap_pyfunction!(calendar_spread_legs, m)?)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_build_options_chain() {
        let chain = build_options_chain(
            vec!["M1".into(), "M2".into()],
            vec![0.6, 0.3],
            vec![30.0, 60.0],
        );
        assert_eq!(chain.len(), 2);
        assert!(chain[0].implied_vol > 0.0);
        assert!(chain[0].delta > 0.0);
    }

    #[test]
    fn test_build_options_chain_edge_prices() {
        let chain = build_options_chain(
            vec!["M1".into()],
            vec![0.001], // extreme
            vec![30.0],
        );
        assert_eq!(chain.len(), 1);
        assert!(chain[0].implied_vol.is_finite());
    }

    #[test]
    fn test_vol_surface() {
        let surface = vol_surface(
            vec![0.3, 0.5, 0.7],
            vec![30.0, 30.0, 30.0],
            vec![0.3, 0.5, 0.7],
        );
        assert_eq!(surface.len(), 3);
        // ATM (0.5) should have lower IV than wings
        assert!(surface[1].implied_vol < surface[0].implied_vol.max(surface[2].implied_vol) + 1.0);
    }

    #[test]
    fn test_term_structure() {
        let ts = term_structure(
            vec![30.0, 60.0, 90.0],
            vec![0.5, 0.5, 0.5],
        );
        assert_eq!(ts.len(), 3);
        // Should be sorted by expiry
        assert!(ts[0].expiry_days <= ts[1].expiry_days);
        assert!(ts[1].expiry_days <= ts[2].expiry_days);
    }

    #[test]
    fn test_synthetic_butterfly() {
        let legs = synthetic_butterfly("A".into(), "B".into(), "C".into(), 10.0);
        assert_eq!(legs.len(), 3);
        assert_eq!(legs[0].1, "buy");
        assert!((legs[0].2 - 10.0).abs() < 1e-10);
        assert_eq!(legs[1].1, "sell");
        assert!((legs[1].2 - 20.0).abs() < 1e-10);
        assert_eq!(legs[2].1, "buy");
    }

    #[test]
    fn test_synthetic_straddle() {
        let legs = synthetic_straddle("M1".into(), 5.0);
        assert_eq!(legs.len(), 2);
        assert_eq!(legs[0].1, "buy_yes");
        assert_eq!(legs[1].1, "buy_no");
    }

    #[test]
    fn test_calendar_spread() {
        let legs = calendar_spread_legs("NEAR".into(), "FAR".into(), 10.0);
        assert_eq!(legs.len(), 2);
        assert_eq!(legs[0].1, "sell");
        assert_eq!(legs[1].1, "buy");
    }

    #[test]
    fn test_implied_vol_binary() {
        let iv = implied_vol_binary(0.5, 30.0 / 365.0);
        assert!(iv.is_finite());
        // ATM binary at 0.5 should have ~0 IV (norm_inv(0.5) = 0)
        assert!(iv < 0.1);
    }

    #[test]
    fn test_implied_vol_binary_otm() {
        let iv = implied_vol_binary(0.1, 30.0 / 365.0);
        assert!(iv > 0.0);
        assert!(iv.is_finite());
    }

    #[test]
    fn test_norm_pdf() {
        assert!((norm_pdf(0.0) - 0.3989).abs() < 0.001);
    }

    #[test]
    fn test_norm_inv() {
        assert!(norm_inv(0.5).abs() < 0.01);
        assert!(norm_inv(0.975) > 1.9);
        assert!(norm_inv(0.025) < -1.9);
    }
}
