//! Dynamic slippage models for realistic trade execution simulation.
//!
//! Provides pluggable slippage estimation that accounts for order size,
//! market liquidity, bid-ask spreads, and volatility regimes.

use serde::{Deserialize, Serialize};

// ---------------------------------------------------------------------------
// Core types
// ---------------------------------------------------------------------------

/// Volatility regime classification used to scale slippage estimates.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VolatilityRegime {
    /// Calm markets — tighter spreads, lower impact.
    Low,
    /// Typical trading conditions.
    Normal,
    /// Stressed / crisis markets — wide spreads, high impact.
    Crisis,
}

/// Contextual information passed to a [`SlippageModel`] for each fill.
#[derive(Debug, Clone)]
pub struct SlippageContext {
    /// Size of the order being filled (contracts / shares).
    pub order_size: f64,
    /// Average daily volume for the instrument.
    pub average_daily_volume: f64,
    /// Current bid-ask spread (price units).
    pub current_spread: f64,
    /// Current volatility regime.
    pub volatility_regime: VolatilityRegime,
}

/// Spread characteristics for a specific instrument.
#[derive(Debug, Clone)]
pub struct SpreadProfile {
    /// Human-readable instrument name.
    pub name: String,
    /// Minimum price increment.
    pub tick_size: f64,
    /// Typical spread expressed in ticks.
    pub typical_spread_ticks: f64,
}

impl SpreadProfile {
    /// E-mini S&P 500 futures (ES) — tick = 0.25, typical spread = 1 tick.
    #[must_use]
    pub fn es_futures() -> Self {
        Self {
            name: "ES".to_string(),
            tick_size: 0.25,
            typical_spread_ticks: 1.0,
        }
    }

    /// 10-Year Treasury Note futures (ZN) — tick = 1/64, typical spread = 1 tick.
    #[must_use]
    pub fn zn_futures() -> Self {
        Self {
            name: "ZN".to_string(),
            tick_size: 1.0 / 64.0,
            typical_spread_ticks: 1.0,
        }
    }

    /// USD/CHF (6S / SIXE) futures — tick = 0.0005 (half-pip), typical spread = 2 ticks.
    #[must_use]
    pub fn sixe_futures() -> Self {
        Self {
            name: "6S".to_string(),
            tick_size: 0.0005,
            typical_spread_ticks: 2.0,
        }
    }

    /// Typical spread in price units (`tick_size * typical_spread_ticks`).
    #[must_use]
    pub fn typical_spread(&self) -> f64 {
        self.tick_size * self.typical_spread_ticks
    }
}

// ---------------------------------------------------------------------------
// Trait
// ---------------------------------------------------------------------------

/// A model that estimates execution slippage (in price units) for a given
/// order context.  Implementations must be `Send + Sync` so they can be
/// shared across async tasks.
pub trait SlippageModel: Send + Sync {
    /// Compute the estimated slippage for the given context.
    ///
    /// Returns a non-negative value in price units that should be *added* to
    /// the execution cost (subtracted from fills on buys, added on sells).
    fn compute(&self, ctx: &SlippageContext) -> f64;
}

// ---------------------------------------------------------------------------
// Implementations
// ---------------------------------------------------------------------------

/// Returns a constant slippage regardless of context.
///
/// This is the simplest model and serves as a backward-compatible default.
#[derive(Debug, Clone)]
pub struct FixedSlippage {
    /// Constant slippage value (price units).
    pub slippage: f64,
}

impl FixedSlippage {
    /// Create a new fixed slippage model.
    #[must_use]
    pub fn new(slippage: f64) -> Self {
        Self { slippage }
    }
}

impl SlippageModel for FixedSlippage {
    fn compute(&self, _ctx: &SlippageContext) -> f64 {
        self.slippage
    }
}

/// Square-root market-impact model: `slippage = k * sqrt(order_size / ADV)`.
///
/// This is the standard Almgren-Chriss style temporary impact model used
/// widely in execution analytics.
#[derive(Debug, Clone)]
pub struct VolumeImpactSlippage {
    /// Impact coefficient (scales the sqrt term).
    pub k: f64,
}

impl VolumeImpactSlippage {
    /// Create a new volume-impact model with the given coefficient.
    #[must_use]
    pub fn new(k: f64) -> Self {
        Self { k }
    }
}

impl SlippageModel for VolumeImpactSlippage {
    fn compute(&self, ctx: &SlippageContext) -> f64 {
        if ctx.average_daily_volume <= 0.0 {
            return 0.0;
        }
        let participation = ctx.order_size / ctx.average_daily_volume;
        self.k * participation.sqrt()
    }
}

/// Wraps an inner [`SlippageModel`] and multiplies its output by a factor
/// determined by the current [`VolatilityRegime`].
///
/// Regime multipliers:
/// - `Low`    — 0.5
/// - `Normal` — 1.0
/// - `Crisis` — 3.0
pub struct RegimeAwareSlippage {
    inner: Box<dyn SlippageModel>,
    low_factor: f64,
    normal_factor: f64,
    crisis_factor: f64,
}

impl std::fmt::Debug for RegimeAwareSlippage {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("RegimeAwareSlippage")
            .field("low_factor", &self.low_factor)
            .field("normal_factor", &self.normal_factor)
            .field("crisis_factor", &self.crisis_factor)
            .finish_non_exhaustive()
    }
}

impl RegimeAwareSlippage {
    /// Wrap `inner` with default regime multipliers (0.5 / 1.0 / 3.0).
    pub fn new(inner: Box<dyn SlippageModel>) -> Self {
        Self {
            inner,
            low_factor: 0.5,
            normal_factor: 1.0,
            crisis_factor: 3.0,
        }
    }

    /// Return the multiplier for the given regime.
    fn regime_factor(&self, regime: VolatilityRegime) -> f64 {
        match regime {
            VolatilityRegime::Low => self.low_factor,
            VolatilityRegime::Normal => self.normal_factor,
            VolatilityRegime::Crisis => self.crisis_factor,
        }
    }
}

impl SlippageModel for RegimeAwareSlippage {
    fn compute(&self, ctx: &SlippageContext) -> f64 {
        let base = self.inner.compute(ctx);
        base * self.regime_factor(ctx.volatility_regime)
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn fixed_slippage_returns_constant() {
        let model = FixedSlippage::new(0.5);
        let ctx = SlippageContext {
            order_size: 100.0,
            average_daily_volume: 1_000_000.0,
            current_spread: 0.25,
            volatility_regime: VolatilityRegime::Normal,
        };
        let result = model.compute(&ctx);
        assert!(
            (result - 0.5).abs() < f64::EPSILON,
            "Fixed slippage should return the constant value, got {result}"
        );
    }

    #[test]
    fn volume_impact_scales_with_order_size() {
        let model = VolumeImpactSlippage::new(1.0);
        let small_ctx = SlippageContext {
            order_size: 100.0,
            average_daily_volume: 1_000_000.0,
            current_spread: 0.25,
            volatility_regime: VolatilityRegime::Normal,
        };
        let large_ctx = SlippageContext {
            order_size: 10_000.0,
            average_daily_volume: 1_000_000.0,
            current_spread: 0.25,
            volatility_regime: VolatilityRegime::Normal,
        };
        let small_slip = model.compute(&small_ctx);
        let large_slip = model.compute(&large_ctx);
        assert!(
            large_slip > small_slip,
            "Larger orders should produce more slippage: large={large_slip}, small={small_slip}"
        );
    }

    #[test]
    fn regime_aware_crisis_exceeds_normal() {
        let inner = Box::new(FixedSlippage::new(1.0));
        let model = RegimeAwareSlippage::new(inner);

        let normal_ctx = SlippageContext {
            order_size: 100.0,
            average_daily_volume: 1_000_000.0,
            current_spread: 0.25,
            volatility_regime: VolatilityRegime::Normal,
        };
        let crisis_ctx = SlippageContext {
            volatility_regime: VolatilityRegime::Crisis,
            ..normal_ctx.clone()
        };

        let normal_slip = model.compute(&normal_ctx);
        let crisis_slip = model.compute(&crisis_ctx);
        assert!(
            crisis_slip > normal_slip,
            "Crisis slippage ({crisis_slip}) should exceed normal ({normal_slip})"
        );
    }

    #[test]
    fn es_spread_profile_tick_size() {
        let es = SpreadProfile::es_futures();
        assert!(
            (es.tick_size - 0.25).abs() < f64::EPSILON,
            "ES tick_size should be 0.25, got {}",
            es.tick_size
        );
    }
}
