//! # Slippage and Market Impact Models
//!
//! Volume-dependent transaction cost modeling for realistic backtesting
//! and RL reward computation. Replaces fixed bps constants with
//! square-root market impact models calibrated per instrument.

use serde::{Deserialize, Serialize};

use ml_core::common::action::OrderType;

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

/// Estimate total transaction cost including market impact.
///
/// Implementations range from simple fixed-cost wrappers (backward compat)
/// to full square-root impact models with temporary decay.
pub trait MarketImpactModel: Send + Sync {
    /// Estimate total transaction cost including market impact.
    ///
    /// Returns cost as a **decimal fraction** (e.g., 0.0015 = 15 bps).
    ///
    /// # Arguments
    ///
    /// * `position_change` - Signed change in position (lots / contracts).
    ///   Only the absolute value matters for cost estimation.
    /// * `volume` - Number of lots/contracts traded in this order.
    /// * `adv` - Average daily volume (20-day rolling), used to normalize
    ///   participation rate.
    /// * `order_type` - Execution strategy (Market / LimitMaker / IoC).
    fn estimate_cost(
        &self,
        position_change: f64,
        volume: f64,
        adv: f64,
        order_type: OrderType,
    ) -> f64;
}

// ---------------------------------------------------------------------------
// FixedCostModel  (backward-compat wrapper)
// ---------------------------------------------------------------------------

/// Backward-compatible fixed-cost model that delegates to
/// [`OrderType::transaction_cost()`].
///
/// Ignores volume and ADV entirely -- useful for unit tests and as a
/// migration shim while callers are updated to supply volume data.
#[derive(Debug, Clone, Copy, Default)]
pub struct FixedCostModel;

impl MarketImpactModel for FixedCostModel {
    fn estimate_cost(
        &self,
        _position_change: f64,
        _volume: f64,
        _adv: f64,
        order_type: OrderType,
    ) -> f64 {
        order_type.transaction_cost()
    }
}

// ---------------------------------------------------------------------------
// LinearImpactConfig
// ---------------------------------------------------------------------------

/// Configuration for the [`LinearImpactModel`].
///
/// All costs are expressed in **decimal fractions** (1 bp = 0.0001).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinearImpactConfig {
    /// Square-root impact coefficient (bps per sqrt-lot participation).
    ///
    /// Typical values:
    /// - ES futures: ~0.1 bps / sqrt(lot)
    /// - 6E futures: ~0.3 bps / sqrt(lot)
    pub sqrt_impact_bps: f64,

    /// Half-spread estimate.  When OHLCV data is available the caller
    /// should compute `0.5 * (high - low) / close * normalization_factor`
    /// and pass it here.  Defaults to 1 bp (0.0001).
    pub half_spread: f64,

    /// Order-type premium/rebate in decimal:
    /// - Market:     +0 bps  (0.0)
    /// - IoC:        +2 bps  (0.0002)
    /// - LimitMaker: -5 bps  (-0.0005)  (maker rebate)
    pub market_premium: f64,
    pub ioc_premium: f64,
    pub limit_rebate: f64,

}

impl Default for LinearImpactConfig {
    fn default() -> Self {
        Self {
            sqrt_impact_bps: 0.1, // ES-like default
            half_spread: 0.0001,  // 1 bp
            market_premium: 0.0,
            ioc_premium: 0.0002,    // +2 bps
            limit_rebate: -0.0005,  // -5 bps (rebate)
        }
    }
}

// ---------------------------------------------------------------------------
// LinearImpactModel
// ---------------------------------------------------------------------------

/// Square-root market impact model.
///
/// ```text
/// cost = half_spread + sqrt_impact * sqrt(|volume| / adv) + order_premium
/// ```
///
/// The model is additive: base spread + market impact + order-type premium.
/// All components are in decimal fractions (e.g., 0.0001 = 1 bp).
#[derive(Debug, Clone)]
pub struct LinearImpactModel {
    config: LinearImpactConfig,
}

impl Default for LinearImpactModel {
    fn default() -> Self {
        Self {
            config: LinearImpactConfig::default(),
        }
    }
}

impl LinearImpactModel {
    /// Create a new model from the given configuration.
    pub fn new(config: LinearImpactConfig) -> Self {
        Self { config }
    }

    /// Create with a specific sqrt-impact coefficient (bps per sqrt-lot).
    /// Other parameters use defaults.
    pub fn with_sqrt_impact(sqrt_impact_bps: f64) -> Self {
        Self {
            config: LinearImpactConfig {
                sqrt_impact_bps,
                ..Default::default()
            },
        }
    }

    /// Return the order-type premium/rebate for the given order type.
    fn order_premium(&self, order_type: OrderType) -> f64 {
        match order_type {
            OrderType::Market => self.config.market_premium,
            OrderType::IoC => self.config.ioc_premium,
            OrderType::LimitMaker => self.config.limit_rebate,
        }
    }
}

impl MarketImpactModel for LinearImpactModel {
    fn estimate_cost(
        &self,
        _position_change: f64,
        volume: f64,
        adv: f64,
        order_type: OrderType,
    ) -> f64 {
        let abs_volume = volume.abs();

        // Guard: if ADV is zero or negative, fall back to spread + premium only
        // to avoid division by zero / NaN.
        let impact = if adv > 0.0 {
            // Convert bps coefficient to decimal: 0.1 bps = 0.00001
            let sqrt_impact_decimal = self.config.sqrt_impact_bps * 0.0001;
            sqrt_impact_decimal * (abs_volume / adv).sqrt()
        } else {
            0.0
        };

        let premium = self.order_premium(order_type);

        // Total cost floored at zero -- a maker rebate cannot make cost negative
        // when spread + impact are small enough.
        let total = self.config.half_spread + impact + premium;
        total.max(0.0)
    }
}

// ===========================================================================
// Tests
// ===========================================================================

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

    // -----------------------------------------------------------------------
    // FixedCostModel
    // -----------------------------------------------------------------------

    #[test]
    fn test_fixed_cost_model_matches_order_type() {
        let model = FixedCostModel;
        let adv = 100_000.0;
        let vol = 10.0;
        let pos = 5.0;

        assert!(
            (model.estimate_cost(pos, vol, adv, OrderType::Market)
                - OrderType::Market.transaction_cost())
            .abs()
                < f64::EPSILON
        );
        assert!(
            (model.estimate_cost(pos, vol, adv, OrderType::LimitMaker)
                - OrderType::LimitMaker.transaction_cost())
            .abs()
                < f64::EPSILON
        );
        assert!(
            (model.estimate_cost(pos, vol, adv, OrderType::IoC)
                - OrderType::IoC.transaction_cost())
            .abs()
                < f64::EPSILON
        );
    }

    // -----------------------------------------------------------------------
    // LinearImpactModel - config defaults
    // -----------------------------------------------------------------------

    #[test]
    fn test_linear_impact_config_defaults() {
        let cfg = LinearImpactConfig::default();
        // sqrt_impact should be positive and small
        assert!(cfg.sqrt_impact_bps > 0.0);
        assert!(cfg.sqrt_impact_bps < 10.0); // reasonable upper bound
        // half_spread should be ~ 1bp
        assert!((cfg.half_spread - 0.0001).abs() < 1e-8);
        // market premium is zero
        assert!((cfg.market_premium - 0.0).abs() < 1e-8);
        // ioc premium is positive
        assert!(cfg.ioc_premium > 0.0);
        // limit rebate is negative
        assert!(cfg.limit_rebate < 0.0);
    }

    // -----------------------------------------------------------------------
    // LinearImpactModel - core behavior
    // -----------------------------------------------------------------------

    #[test]
    fn test_linear_impact_higher_volume() {
        let model = LinearImpactModel::default();
        let adv = 100_000.0;

        let cost_low = model.estimate_cost(10.0, 100.0, adv, OrderType::Market);
        let cost_high = model.estimate_cost(10.0, 10_000.0, adv, OrderType::Market);

        assert!(
            cost_high > cost_low,
            "Higher volume should yield higher cost: {} vs {}",
            cost_high,
            cost_low
        );
    }

    #[test]
    fn test_linear_impact_zero_volume() {
        let model = LinearImpactModel::default();
        let adv = 100_000.0;

        let cost = model.estimate_cost(0.0, 0.0, adv, OrderType::Market);
        // With zero volume, impact term is zero.
        // Cost should equal half_spread + market_premium = 0.0001 + 0.0
        assert!(
            (cost - 0.0001).abs() < 1e-8,
            "Zero volume should give base spread cost only, got {}",
            cost
        );
    }

    #[test]
    fn test_linear_impact_order_type_premium() {
        let model = LinearImpactModel::default();
        let adv = 100_000.0;
        let volume = 500.0;
        let pos = 10.0;

        let cost_market = model.estimate_cost(pos, volume, adv, OrderType::Market);
        let cost_ioc = model.estimate_cost(pos, volume, adv, OrderType::IoC);
        let cost_limit = model.estimate_cost(pos, volume, adv, OrderType::LimitMaker);

        // IoC is more expensive than Market (IoC has +2bps premium)
        assert!(
            cost_ioc > cost_market,
            "IoC should cost more than Market: {} vs {}",
            cost_ioc,
            cost_market
        );
        // Market is more expensive than LimitMaker (LimitMaker gets -5bps rebate)
        assert!(
            cost_market > cost_limit,
            "Market should cost more than LimitMaker: {} vs {}",
            cost_market,
            cost_limit
        );
    }

    #[test]
    fn test_linear_impact_sqrt_scaling() {
        let model = LinearImpactModel::default();
        let adv = 100_000.0;

        // If we 4x the volume, the sqrt impact should roughly 2x.
        let cost_v = model.estimate_cost(10.0, 1_000.0, adv, OrderType::Market);
        let cost_4v = model.estimate_cost(10.0, 4_000.0, adv, OrderType::Market);

        // Impact component: sqrt(V/ADV) vs sqrt(4V/ADV) = 2 * sqrt(V/ADV)
        // The spread and premium are identical, so:
        //   cost_4v - spread = 2 * (cost_v - spread)
        let spread = model.config.half_spread + model.config.market_premium;
        let impact_v = cost_v - spread;
        let impact_4v = cost_4v - spread;

        let ratio = impact_4v / impact_v;
        assert!(
            (ratio - 2.0).abs() < 1e-6,
            "sqrt scaling: 4x volume should give 2x impact, ratio = {}",
            ratio
        );
    }

    #[test]
    fn test_linear_impact_negative_position_change() {
        let model = LinearImpactModel::default();
        let adv = 100_000.0;
        let volume = 500.0;

        let cost_pos = model.estimate_cost(10.0, volume, adv, OrderType::Market);
        let cost_neg = model.estimate_cost(-10.0, volume, adv, OrderType::Market);

        // Signed position_change should not affect cost (volume is the driver).
        assert!(
            (cost_pos - cost_neg).abs() < f64::EPSILON,
            "Cost should be identical for +/- position change: {} vs {}",
            cost_pos,
            cost_neg
        );
    }

    #[test]
    fn test_linear_impact_negative_volume_uses_abs() {
        let model = LinearImpactModel::default();
        let adv = 100_000.0;

        let cost_pos = model.estimate_cost(10.0, 500.0, adv, OrderType::Market);
        let cost_neg = model.estimate_cost(10.0, -500.0, adv, OrderType::Market);

        assert!(
            (cost_pos - cost_neg).abs() < f64::EPSILON,
            "Negative volume should be treated as abs: {} vs {}",
            cost_pos,
            cost_neg
        );
    }

    #[test]
    fn test_linear_impact_zero_adv_no_panic() {
        let model = LinearImpactModel::default();
        // ADV = 0 should not panic or produce NaN
        let cost = model.estimate_cost(10.0, 500.0, 0.0, OrderType::Market);
        assert!(cost.is_finite(), "Cost should be finite with zero ADV");
        assert!(cost >= 0.0, "Cost should be non-negative");
    }

    #[test]
    fn test_linear_impact_negative_adv_no_panic() {
        let model = LinearImpactModel::default();
        let cost = model.estimate_cost(10.0, 500.0, -100_000.0, OrderType::Market);
        assert!(cost.is_finite(), "Cost should be finite with negative ADV");
        assert!(cost >= 0.0, "Cost should be non-negative");
    }

    #[test]
    fn test_linear_impact_cost_non_negative() {
        // Even with a large LimitMaker rebate, cost is floored at 0.
        let cfg = LinearImpactConfig {
            sqrt_impact_bps: 0.01,
            half_spread: 0.0001,
            limit_rebate: -0.01, // huge rebate: -100 bps
            ..Default::default()
        };
        let model = LinearImpactModel::new(cfg);
        let cost = model.estimate_cost(1.0, 1.0, 100_000.0, OrderType::LimitMaker);
        assert!(cost >= 0.0, "Cost must never be negative, got {}", cost);
    }

    // -----------------------------------------------------------------------
    // Trait object dispatch
    // -----------------------------------------------------------------------

    #[test]
    fn test_trait_object_dispatch() {
        let fixed: Box<dyn MarketImpactModel> = Box::new(FixedCostModel);
        let linear: Box<dyn MarketImpactModel> = Box::new(LinearImpactModel::default());

        let adv = 100_000.0;
        let volume = 500.0;
        let pos = 10.0;

        let cost_fixed = fixed.estimate_cost(pos, volume, adv, OrderType::Market);
        let cost_linear = linear.estimate_cost(pos, volume, adv, OrderType::Market);

        // Both should return finite, positive values
        assert!(cost_fixed.is_finite() && cost_fixed > 0.0);
        assert!(cost_linear.is_finite() && cost_linear > 0.0);

        // They should differ (different models)
        assert!(
            (cost_fixed - cost_linear).abs() > 1e-10,
            "Fixed and linear models should produce different costs"
        );
    }

    // -----------------------------------------------------------------------
    // Custom configuration
    // -----------------------------------------------------------------------

    #[test]
    fn test_with_sqrt_impact_constructor() {
        let model = LinearImpactModel::with_sqrt_impact(0.3);
        assert!(
            (model.config.sqrt_impact_bps - 0.3).abs() < 1e-8,
            "sqrt_impact_bps should be 0.3"
        );
        // Other defaults preserved
        assert!(
            (model.config.half_spread - 0.0001).abs() < 1e-8,
            "half_spread should still be default"
        );
    }

    #[test]
    fn test_higher_sqrt_impact_means_higher_cost() {
        let model_low = LinearImpactModel::with_sqrt_impact(0.1);
        let model_high = LinearImpactModel::with_sqrt_impact(0.5);

        let adv = 100_000.0;
        let volume = 5_000.0;

        let cost_low = model_low.estimate_cost(10.0, volume, adv, OrderType::Market);
        let cost_high = model_high.estimate_cost(10.0, volume, adv, OrderType::Market);

        assert!(
            cost_high > cost_low,
            "Higher sqrt_impact should yield higher cost: {} vs {}",
            cost_high,
            cost_low
        );
    }

    #[test]
    fn test_default_model_impl() {
        // Ensure Default trait works for LinearImpactModel
        let model: LinearImpactModel = Default::default();
        let cost = model.estimate_cost(1.0, 100.0, 50_000.0, OrderType::Market);
        assert!(cost.is_finite() && cost > 0.0);
    }
}
