//! Adversarial simulation for strategy robustness testing.
//!
//! Adversary types that inject realistic market manipulation scenarios.
//! Pure functions: market state in, actions out.

use pyo3::prelude::*;

/// Type of adversarial agent.
#[pyclass(eq, eq_int, frozen, hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AdversaryType {
    Whale = 0,
    FrontRunner = 1,
    Spoofing = 2,
    LiquidityDrainer = 3,
}

#[pymethods]
impl AdversaryType {
    fn __repr__(&self) -> String {
        match self {
            Self::Whale => "AdversaryType.Whale",
            Self::FrontRunner => "AdversaryType.FrontRunner",
            Self::Spoofing => "AdversaryType.Spoofing",
            Self::LiquidityDrainer => "AdversaryType.LiquidityDrainer",
        }
        .to_string()
    }
    fn __str__(&self) -> String { self.__repr__() }
}

/// Configuration for an adversary agent.
#[pyclass(frozen, get_all)]
#[derive(Debug, Clone)]
pub struct AdversaryConfig {
    pub adversary_type: AdversaryType,
    pub intensity: f64,
    pub market_id: String,
    pub size_multiplier: f64,
}

#[pymethods]
impl AdversaryConfig {
    #[new]
    #[pyo3(signature = (adversary_type, market_id, intensity=1.0, size_multiplier=5.0))]
    fn new(adversary_type: AdversaryType, market_id: String, intensity: f64, size_multiplier: f64) -> Self {
        Self { adversary_type, intensity, market_id, size_multiplier }
    }

    fn __repr__(&self) -> String {
        format!("AdversaryConfig(type={:?}, market='{}', intensity={:.2})",
            self.adversary_type, self.market_id, self.intensity)
    }
}

/// An action generated by an adversary.
#[pyclass(frozen, get_all)]
#[derive(Debug, Clone)]
pub struct AdversaryAction {
    pub adversary_type: AdversaryType,
    pub market_id: String,
    pub side: String,
    pub size: f64,
    pub price: f64,
    pub is_cancel: bool,
    pub description: String,
}

#[pymethods]
impl AdversaryAction {
    fn __repr__(&self) -> String {
        format!("AdversaryAction({:?}, {}, size={:.2}, price={:.4})",
            self.adversary_type, self.side, self.size, self.price)
    }
}

/// Metrics from an adversarial simulation run.
#[pyclass(frozen, get_all)]
#[derive(Debug, Clone)]
pub struct SimulationMetrics {
    pub total_pnl: f64,
    pub max_drawdown: f64,
    pub sharpe_ratio: f64,
    pub num_trades: u64,
    pub win_rate: f64,
    pub avg_slippage: f64,
}

#[pymethods]
impl SimulationMetrics {
    #[new]
    #[pyo3(signature = (total_pnl=0.0, max_drawdown=0.0, sharpe_ratio=0.0, num_trades=0, win_rate=0.0, avg_slippage=0.0))]
    fn new(total_pnl: f64, max_drawdown: f64, sharpe_ratio: f64, num_trades: u64, win_rate: f64, avg_slippage: f64) -> Self {
        Self { total_pnl, max_drawdown, sharpe_ratio, num_trades, win_rate, avg_slippage }
    }

    fn __repr__(&self) -> String {
        format!("SimulationMetrics(pnl={:.4}, dd={:.4}, sharpe={:.4})",
            self.total_pnl, self.max_drawdown, self.sharpe_ratio)
    }
}

/// Result of adversarial simulation.
#[pyclass(frozen, get_all)]
#[derive(Debug, Clone)]
pub struct AdversarialResult {
    pub baseline: SimulationMetrics,
    pub adversarial: SimulationMetrics,
    pub pnl_impact: f64,
    pub drawdown_impact: f64,
    pub sharpe_impact: f64,
    pub vulnerability_score: f64,
}

#[pymethods]
impl AdversarialResult {
    fn __repr__(&self) -> String {
        format!("AdversarialResult(pnl_impact={:.4}, vuln_score={:.4})",
            self.pnl_impact, self.vulnerability_score)
    }
}

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

/// Generate whale actions: large market-moving orders.
#[pyfunction]
#[pyo3(signature = (market_id, current_price, avg_size, intensity=1.0, seed=42))]
pub fn whale_action(
    market_id: String,
    current_price: f64,
    avg_size: f64,
    intensity: f64,
    seed: u64,
) -> AdversaryAction {
    let mut rng = SimpleRng(seed);
    let size = avg_size * intensity * (3.0 + rng.next_f64() * 7.0); // 3-10x average
    let side = if rng.next_f64() > 0.5 { "buy" } else { "sell" };
    let impact = if side == "buy" { 0.02 } else { -0.02 };
    let price = (current_price + impact * intensity).clamp(0.01, 0.99);

    AdversaryAction {
        adversary_type: AdversaryType::Whale,
        market_id,
        side: side.to_string(),
        size,
        price,
        is_cancel: false,
        description: format!("Whale {} {:.0} @ {:.4}", side, size, price),
    }
}

/// Generate front-runner actions: copy detected order ahead of execution.
#[pyfunction]
pub fn front_runner_action(
    market_id: String,
    detected_side: &str,
    detected_price: f64,
    detected_size: f64,
    edge_bps: f64,
) -> AdversaryAction {
    let edge = edge_bps / 10000.0;
    let price = if detected_side == "buy" {
        (detected_price - edge).max(0.01)
    } else {
        (detected_price + edge).min(0.99)
    };
    let size = detected_size * 0.5; // front-run half the size

    AdversaryAction {
        adversary_type: AdversaryType::FrontRunner,
        market_id,
        side: detected_side.to_string(),
        size,
        price,
        is_cancel: false,
        description: format!("Front-run {} {:.0} @ {:.4}", detected_side, size, price),
    }
}

/// Generate spoofing actions: place and cancel large orders to move price.
#[pyfunction]
#[pyo3(signature = (market_id, current_price, spoof_size, levels=3))]
pub fn spoofing_action(
    market_id: String,
    current_price: f64,
    spoof_size: f64,
    levels: u32,
) -> Vec<AdversaryAction> {
    let mut actions = Vec::new();
    for i in 0..levels {
        let offset = 0.01 * (i + 1) as f64;
        // Place large bids below to push price up
        actions.push(AdversaryAction {
            adversary_type: AdversaryType::Spoofing,
            market_id: market_id.clone(),
            side: "buy".to_string(),
            size: spoof_size,
            price: (current_price - offset).max(0.01),
            is_cancel: true, // will be canceled
            description: format!("Spoof bid level {}", i + 1),
        });
    }
    actions
}

/// Generate liquidity drainer actions: aggressively take all resting orders.
#[pyfunction]
pub fn liquidity_drainer_action(
    market_id: String,
    bid_price: f64,
    ask_price: f64,
    available_size: f64,
) -> Vec<AdversaryAction> {
    let mut actions = Vec::new();

    if available_size > 0.0 && bid_price > 0.0 {
        actions.push(AdversaryAction {
            adversary_type: AdversaryType::LiquidityDrainer,
            market_id: market_id.clone(),
            side: "sell".to_string(),
            size: available_size,
            price: bid_price,
            is_cancel: false,
            description: format!("Drain bids: sell {:.0} @ {:.4}", available_size, bid_price),
        });
    }

    if available_size > 0.0 && ask_price > 0.0 {
        actions.push(AdversaryAction {
            adversary_type: AdversaryType::LiquidityDrainer,
            market_id,
            side: "buy".to_string(),
            size: available_size,
            price: ask_price,
            is_cancel: false,
            description: format!("Drain asks: buy {:.0} @ {:.4}", available_size, ask_price),
        });
    }

    actions
}

/// Compute metrics comparing baseline vs adversarial simulation.
#[pyfunction]
pub fn compute_simulation_metrics(
    baseline_pnls: Vec<f64>,
    adversarial_pnls: Vec<f64>,
) -> AdversarialResult {
    let base = compute_metrics_from_pnls(&baseline_pnls);
    let adv = compute_metrics_from_pnls(&adversarial_pnls);

    let pnl_impact = finite_or_zero(adv.total_pnl - base.total_pnl);
    let dd_impact = finite_or_zero(adv.max_drawdown - base.max_drawdown);
    let sharpe_impact = finite_or_zero(adv.sharpe_ratio - base.sharpe_ratio);

    // Vulnerability: how much worse under adversarial conditions (0-1 scale)
    let vuln = if base.total_pnl.abs() > 1e-10 {
        finite_or_zero((-pnl_impact / base.total_pnl.abs()).clamp(0.0, 1.0))
    } else if adv.total_pnl < 0.0 {
        1.0
    } else {
        0.0
    };

    AdversarialResult {
        baseline: base,
        adversarial: adv,
        pnl_impact,
        drawdown_impact: dd_impact,
        sharpe_impact,
        vulnerability_score: vuln,
    }
}

/// Compute vulnerability score from baseline and adversarial metrics.
#[pyfunction]
pub fn vulnerability_score(
    baseline_sharpe: f64,
    adversarial_sharpe: f64,
    baseline_pnl: f64,
    adversarial_pnl: f64,
) -> f64 {
    let sharpe_ratio_loss = if baseline_sharpe > 0.0 {
        ((baseline_sharpe - adversarial_sharpe) / baseline_sharpe).clamp(0.0, 1.0)
    } else {
        0.0
    };
    let pnl_loss = if baseline_pnl.abs() > 1e-10 {
        ((baseline_pnl - adversarial_pnl) / baseline_pnl.abs()).clamp(0.0, 1.0)
    } else {
        0.0
    };
    finite_or_zero((0.5 * sharpe_ratio_loss + 0.5 * pnl_loss).clamp(0.0, 1.0))
}

// ---- helpers ----

fn compute_metrics_from_pnls(pnls: &[f64]) -> SimulationMetrics {
    if pnls.is_empty() {
        return SimulationMetrics {
            total_pnl: 0.0, max_drawdown: 0.0, sharpe_ratio: 0.0,
            num_trades: 0, win_rate: 0.0, avg_slippage: 0.0,
        };
    }

    let total_pnl: f64 = pnls.iter().sum();
    let n = pnls.len() as f64;
    let mean = total_pnl / n;
    let var = pnls.iter().map(|p| (p - mean).powi(2)).sum::<f64>() / (n - 1.0).max(1.0);
    let std = var.sqrt();
    let sharpe = if std > 1e-10 { finite_or_zero(mean / std) } else { 0.0 };

    // Max drawdown from cumulative PnL
    let mut peak = 0.0f64;
    let mut max_dd = 0.0f64;
    let mut cum = 0.0;
    for p in pnls {
        cum += p;
        peak = peak.max(cum);
        max_dd = max_dd.max(peak - cum);
    }

    let wins = pnls.iter().filter(|p| **p > 0.0).count();
    let win_rate = wins as f64 / n;

    SimulationMetrics {
        total_pnl: finite_or_zero(total_pnl),
        max_drawdown: finite_or_zero(max_dd),
        sharpe_ratio: sharpe,
        num_trades: pnls.len() as u64,
        win_rate: finite_or_zero(win_rate),
        avg_slippage: 0.0,
    }
}

struct SimpleRng(u64);
impl SimpleRng {
    fn next_f64(&mut self) -> f64 {
        self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
        (self.0 >> 11) as f64 / (1u64 << 53) as f64
    }
}

pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<AdversaryType>()?;
    m.add_class::<AdversaryConfig>()?;
    m.add_class::<AdversaryAction>()?;
    m.add_class::<SimulationMetrics>()?;
    m.add_class::<AdversarialResult>()?;
    m.add_function(wrap_pyfunction!(whale_action, m)?)?;
    m.add_function(wrap_pyfunction!(front_runner_action, m)?)?;
    m.add_function(wrap_pyfunction!(spoofing_action, m)?)?;
    m.add_function(wrap_pyfunction!(liquidity_drainer_action, m)?)?;
    m.add_function(wrap_pyfunction!(compute_simulation_metrics, m)?)?;
    m.add_function(wrap_pyfunction!(vulnerability_score, m)?)?;
    Ok(())
}

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

    #[test]
    fn test_whale_action() {
        let action = whale_action("M1".into(), 0.50, 100.0, 1.0, 42);
        assert_eq!(action.adversary_type, AdversaryType::Whale);
        assert!(action.size > 100.0); // should be amplified
        assert!(action.price > 0.0 && action.price < 1.0);
    }

    #[test]
    fn test_front_runner_buy() {
        let action = front_runner_action("M1".into(), "buy", 0.50, 100.0, 50.0);
        assert_eq!(action.side, "buy");
        assert!(action.price < 0.50); // front-runs by buying cheaper
        assert!((action.size - 50.0).abs() < 1e-10);
    }

    #[test]
    fn test_front_runner_sell() {
        let action = front_runner_action("M1".into(), "sell", 0.50, 100.0, 50.0);
        assert_eq!(action.side, "sell");
        assert!(action.price > 0.50);
    }

    #[test]
    fn test_spoofing_action() {
        let actions = spoofing_action("M1".into(), 0.50, 1000.0, 3);
        assert_eq!(actions.len(), 3);
        for a in &actions {
            assert!(a.is_cancel);
            assert_eq!(a.adversary_type, AdversaryType::Spoofing);
        }
    }

    #[test]
    fn test_liquidity_drainer() {
        let actions = liquidity_drainer_action("M1".into(), 0.48, 0.52, 500.0);
        assert_eq!(actions.len(), 2);
        assert_eq!(actions[0].side, "sell");
        assert_eq!(actions[1].side, "buy");
    }

    #[test]
    fn test_compute_simulation_metrics() {
        let baseline = vec![10.0, -5.0, 15.0, -2.0, 8.0];
        let adversarial = vec![5.0, -10.0, 8.0, -5.0, 3.0];
        let result = compute_simulation_metrics(baseline, adversarial);
        assert!(result.pnl_impact < 0.0); // adversarial is worse
        assert!(result.vulnerability_score > 0.0);
    }

    #[test]
    fn test_compute_simulation_metrics_empty() {
        let result = compute_simulation_metrics(vec![], vec![]);
        assert!((result.pnl_impact).abs() < 1e-10);
    }

    #[test]
    fn test_vulnerability_score() {
        let score = vulnerability_score(1.5, 0.5, 1000.0, 200.0);
        assert!(score > 0.0 && score <= 1.0);
    }

    #[test]
    fn test_vulnerability_score_no_baseline() {
        let score = vulnerability_score(0.0, -0.5, 0.0, -100.0);
        assert!((score).abs() < 1e-10 || score >= 0.0);
    }

    #[test]
    fn test_compute_metrics_from_pnls() {
        let pnls = vec![10.0, -5.0, 15.0, -2.0, 8.0];
        let m = compute_metrics_from_pnls(&pnls);
        assert!((m.total_pnl - 26.0).abs() < 1e-10);
        assert_eq!(m.num_trades, 5);
        assert!((m.win_rate - 0.6).abs() < 1e-10);
        assert!(m.max_drawdown >= 0.0);
    }
}
