//! Yes/No parity monitoring and cross-platform arbitrage detection.
//!
//! In prediction markets, YES + NO prices should sum to ~1.0.
//! Deviations indicate mispricing or arbitrage opportunities.
//! All computations are branchless where possible for hot-path performance.

use pyo3::prelude::*;

/// Result of a YES/NO parity check for a single market.
#[pyclass]
#[derive(Debug, Clone)]
pub struct ParityResult {
    #[pyo3(get)]
    pub market_id: String,
    /// Best YES price (ask for buying YES, or feed price).
    #[pyo3(get)]
    pub yes_price: f64,
    /// Best NO price (1.0 - yes_price, or actual NO price if available).
    #[pyo3(get)]
    pub no_price: f64,
    /// Deviation from parity: |yes + no - 1.0|.
    #[pyo3(get)]
    pub deviation: f64,
    /// True if deviation exceeds threshold.
    #[pyo3(get)]
    pub is_violation: bool,
    /// The exchange this data is from.
    #[pyo3(get)]
    pub exchange: String,
}

#[pymethods]
impl ParityResult {
    fn __repr__(&self) -> String {
        format!(
            "ParityResult({} yes={:.4} no={:.4} dev={:.4} violation={})",
            self.market_id, self.yes_price, self.no_price, self.deviation, self.is_violation
        )
    }
}

/// A detected arbitrage opportunity across exchanges.
#[pyclass]
#[derive(Debug, Clone)]
pub struct ArbitrageOpportunity {
    #[pyo3(get)]
    pub market_id: String,
    /// Exchange where you would buy.
    #[pyo3(get)]
    pub buy_exchange: String,
    /// Exchange where you would sell.
    #[pyo3(get)]
    pub sell_exchange: String,
    /// Price to buy at (ask on buy exchange).
    #[pyo3(get)]
    pub buy_price: f64,
    /// Price to sell at (bid on sell exchange).
    #[pyo3(get)]
    pub sell_price: f64,
    /// Raw edge before fees: sell_price - buy_price.
    #[pyo3(get)]
    pub raw_edge: f64,
    /// Net edge after estimated fees.
    #[pyo3(get)]
    pub net_edge: f64,
    /// Maximum executable size (min of available liquidity on both sides).
    #[pyo3(get)]
    pub max_size: f64,
    /// Whether this opportunity is executable (net_edge > 0 and max_size > 0).
    #[pyo3(get)]
    pub is_executable: bool,
}

#[pymethods]
impl ArbitrageOpportunity {
    fn __repr__(&self) -> String {
        format!(
            "ArbitrageOpportunity({} buy@{}={:.4} sell@{}={:.4} edge={:.4} net={:.4} size={:.1})",
            self.market_id,
            self.buy_exchange, self.buy_price,
            self.sell_exchange, self.sell_price,
            self.raw_edge, self.net_edge, self.max_size,
        )
    }
}

/// Check YES/NO parity for a market given two prices.
///
/// `#[inline]` — hot path, called every tick per market.
#[inline]
pub fn check_parity(
    market_id: &str,
    yes_price: f64,
    no_price: f64,
    threshold: f64,
    exchange: &str,
) -> ParityResult {
    if !yes_price.is_finite() || !no_price.is_finite() {
        return ParityResult {
            market_id: market_id.to_string(),
            yes_price,
            no_price,
            deviation: f64::NAN,
            is_violation: false,
            exchange: exchange.to_string(),
        };
    }
    let sum = yes_price + no_price;
    let deviation = (sum - 1.0).abs();
    ParityResult {
        market_id: market_id.to_string(),
        yes_price,
        no_price,
        deviation,
        is_violation: deviation > threshold,
        exchange: exchange.to_string(),
    }
}

/// Detect cross-exchange arbitrage for a single market.
///
/// Compares bid/ask across two exchanges. An arb exists when
/// one exchange's bid > another's ask (you can buy low, sell high).
///
/// `fee_rate_a` and `fee_rate_b`: round-trip fee rate per exchange (e.g., 0.002 for 20bps).
#[inline]
pub fn detect_arbitrage(
    market_id: &str,
    exchange_a: &str,
    bid_a: f64,
    ask_a: f64,
    exchange_b: &str,
    bid_b: f64,
    ask_b: f64,
    fee_rate_a: f64,
    fee_rate_b: f64,
    max_liquidity: f64,
) -> Option<ArbitrageOpportunity> {
    // Direction 1: buy on A (at ask_a), sell on B (at bid_b)
    let edge_ab = bid_b - ask_a;
    // Direction 2: buy on B (at ask_b), sell on A (at bid_a)
    let edge_ba = bid_a - ask_b;

    if edge_ab > edge_ba && edge_ab > 0.0 {
        let total_fees = ask_a * fee_rate_a + bid_b * fee_rate_b;
        let net = edge_ab - total_fees;
        Some(ArbitrageOpportunity {
            market_id: market_id.to_string(),
            buy_exchange: exchange_a.to_string(),
            sell_exchange: exchange_b.to_string(),
            buy_price: ask_a,
            sell_price: bid_b,
            raw_edge: edge_ab,
            net_edge: net,
            max_size: max_liquidity,
            is_executable: net > 0.0 && max_liquidity > 0.0,
        })
    } else if edge_ba > 0.0 {
        let total_fees = ask_b * fee_rate_b + bid_a * fee_rate_a;
        let net = edge_ba - total_fees;
        Some(ArbitrageOpportunity {
            market_id: market_id.to_string(),
            buy_exchange: exchange_b.to_string(),
            sell_exchange: exchange_a.to_string(),
            buy_price: ask_b,
            sell_price: bid_a,
            raw_edge: edge_ba,
            net_edge: net,
            max_size: max_liquidity,
            is_executable: net > 0.0 && max_liquidity > 0.0,
        })
    } else {
        None
    }
}

// ---------------------------------------------------------------------------
// Multi-outcome event parity and arbitrage
// ---------------------------------------------------------------------------

/// A detected arbitrage opportunity within a multi-outcome event.
#[pyclass]
#[derive(Debug, Clone)]
pub struct EventArbitrageOpportunity {
    #[pyo3(get)]
    pub event_id: String,
    #[pyo3(get)]
    pub exchange: String,
    /// Sum of all outcome YES prices.
    #[pyo3(get)]
    pub price_sum: f64,
    /// Overround: sum - 1.0.
    #[pyo3(get)]
    pub overround: f64,
    /// "buy_all" if sum < 1 (outcomes underpriced), "sell_all" if sum > 1.
    #[pyo3(get)]
    pub direction: String,
    #[pyo3(get)]
    pub net_edge: f64,
    #[pyo3(get)]
    pub is_executable: bool,
}

#[pymethods]
impl EventArbitrageOpportunity {
    fn __repr__(&self) -> String {
        format!(
            "EventArbitrageOpportunity(event='{}' sum={:.4} overround={:.4} dir={} edge={:.4})",
            self.event_id, self.price_sum, self.overround, self.direction, self.net_edge,
        )
    }
}

/// Check parity across all outcomes in a multi-outcome event.
/// Sum of YES prices should be ~1.0.
#[inline]
pub fn check_event_parity(
    event_id: &str,
    outcome_prices: &[f64],
    threshold: f64,
    exchange: &str,
) -> ParityResult {
    if outcome_prices.iter().any(|p| !p.is_finite()) {
        return ParityResult {
            market_id: event_id.to_string(),
            yes_price: f64::NAN,
            no_price: f64::NAN,
            deviation: f64::NAN,
            is_violation: false,
            exchange: exchange.to_string(),
        };
    }
    let sum: f64 = outcome_prices.iter().sum();
    let deviation = (sum - 1.0).abs();
    ParityResult {
        market_id: event_id.to_string(),
        yes_price: sum,
        no_price: 1.0 - sum,
        deviation,
        is_violation: deviation > threshold,
        exchange: exchange.to_string(),
    }
}

/// Detect arbitrage in a multi-outcome event.
/// If sum < 1.0 - fees: buy all outcomes = guaranteed profit.
/// If sum > 1.0 + fees: sell all (buy all NOs) = guaranteed profit.
#[inline]
pub fn detect_event_arbitrage(
    event_id: &str,
    outcome_prices: &[(String, f64)],
    fee_rate: f64,
    exchange: &str,
) -> Option<EventArbitrageOpportunity> {
    let sum: f64 = outcome_prices.iter().map(|(_, p)| p).sum();
    let overround = sum - 1.0;
    let n = outcome_prices.len() as f64;
    // Total fees for trading all outcomes (buy or sell each)
    let total_fees = fee_rate * n;

    if overround.abs() <= total_fees {
        // Edge consumed by fees
        return None;
    }

    let (direction, net_edge) = if overround < 0.0 {
        // Sum < 1: buy all outcomes for guaranteed profit
        ("buy_all".to_string(), overround.abs() - total_fees)
    } else {
        // Sum > 1: sell all outcomes (buy all NOs) for guaranteed profit
        ("sell_all".to_string(), overround - total_fees)
    };

    if net_edge <= 0.0 {
        return None;
    }

    Some(EventArbitrageOpportunity {
        event_id: event_id.to_string(),
        exchange: exchange.to_string(),
        price_sum: sum,
        overround,
        direction,
        net_edge,
        is_executable: true,
    })
}

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

    #[test]
    fn parity_no_violation() {
        let r = check_parity("mkt_1", 0.55, 0.45, 0.02, "paper");
        assert!((r.deviation).abs() < 1e-10);
        assert!(!r.is_violation);
    }

    #[test]
    fn parity_violation() {
        let r = check_parity("mkt_1", 0.55, 0.50, 0.02, "paper");
        assert!((r.deviation - 0.05).abs() < 1e-10);
        assert!(r.is_violation);
    }

    #[test]
    fn arb_buy_a_sell_b() {
        let opp = detect_arbitrage(
            "mkt_1", "poly", 0.50, 0.52, "kalshi", 0.56, 0.58, 0.002, 0.002, 100.0,
        );
        let opp = opp.unwrap();
        assert_eq!(opp.buy_exchange, "poly");
        assert_eq!(opp.sell_exchange, "kalshi");
        assert!((opp.raw_edge - 0.04).abs() < 1e-10); // 0.56 - 0.52
        assert!(opp.is_executable);
    }

    #[test]
    fn arb_buy_b_sell_a() {
        let opp = detect_arbitrage(
            "mkt_1", "poly", 0.58, 0.60, "kalshi", 0.50, 0.52, 0.002, 0.002, 100.0,
        );
        let opp = opp.unwrap();
        assert_eq!(opp.buy_exchange, "kalshi");
        assert_eq!(opp.sell_exchange, "poly");
    }

    #[test]
    fn no_arb_when_spread_overlaps() {
        let opp = detect_arbitrage(
            "mkt_1", "poly", 0.50, 0.55, "kalshi", 0.50, 0.55, 0.002, 0.002, 100.0,
        );
        assert!(opp.is_none());
    }

    #[test]
    fn arb_not_executable_after_fees() {
        // Tiny edge eaten by fees
        let opp = detect_arbitrage(
            "mkt_1", "poly", 0.50, 0.500, "kalshi", 0.501, 0.55, 0.01, 0.01, 100.0,
        );
        if let Some(opp) = opp {
            assert!(!opp.is_executable);
        }
    }

    // Event parity tests

    #[test]
    fn event_parity_balanced() {
        let r = check_event_parity(
            "election",
            &[0.25, 0.25, 0.25, 0.25],
            0.02,
            "polymarket",
        );
        assert!((r.deviation).abs() < 1e-10);
        assert!(!r.is_violation);
    }

    #[test]
    fn event_parity_violation() {
        let r = check_event_parity(
            "election",
            &[0.30, 0.30, 0.25, 0.20],
            0.02,
            "polymarket",
        );
        assert!((r.deviation - 0.05).abs() < 1e-10);
        assert!(r.is_violation);
    }

    #[test]
    fn event_arb_buy_all() {
        // Sum = 0.90, underpriced
        let opp = detect_event_arbitrage(
            "election",
            &[
                ("Trump".into(), 0.30),
                ("Biden".into(), 0.25),
                ("DeSantis".into(), 0.20),
                ("Other".into(), 0.15),
            ],
            0.002,
            "polymarket",
        );
        let opp = opp.unwrap();
        assert_eq!(opp.direction, "buy_all");
        assert!(opp.net_edge > 0.0);
        assert!(opp.is_executable);
    }

    #[test]
    fn event_arb_sell_all() {
        // Sum = 1.10, overpriced
        let opp = detect_event_arbitrage(
            "election",
            &[
                ("Trump".into(), 0.40),
                ("Biden".into(), 0.35),
                ("DeSantis".into(), 0.20),
                ("Other".into(), 0.15),
            ],
            0.002,
            "polymarket",
        );
        let opp = opp.unwrap();
        assert_eq!(opp.direction, "sell_all");
        assert!(opp.net_edge > 0.0);
    }

    #[test]
    fn event_arb_fees_consume_edge() {
        // Sum = 1.01, tiny overround eaten by fees (4 outcomes * 0.01 fee = 0.04)
        let opp = detect_event_arbitrage(
            "election",
            &[
                ("A".into(), 0.26),
                ("B".into(), 0.25),
                ("C".into(), 0.25),
                ("D".into(), 0.25),
            ],
            0.01,
            "polymarket",
        );
        assert!(opp.is_none());
    }
}
