//! Alpha Blocks N + V — Microprice features (Stoikov 2014 + Cartea-Jaimungal 2017
//! + multi-level variants).
//!
//! ## Background
//!
//! The standard mid-price `(P_b + P_a) / 2` ignores depth: an order book with
//! `(P_b, P_a) = (100.00, 100.04)` has mid 100.02 regardless of whether
//! bid_size is 1 contract or 100,000. The **microprice** (Stoikov 2014)
//! corrects this by weighting each side by the OPPOSITE side's volume:
//!
//! ```text
//!   M = (P_b · V_a + P_a · V_b) / (V_a + V_b)
//! ```
//!
//! When `V_a >> V_b` (heavy ask depth, thin bid), the microprice tilts toward
//! `P_b` — reflecting the higher probability that the next trade will hit the
//! bid. This is a **directional price expectation**, not just a quote midpoint.
//!
//! ## Cartea-Jaimungal 2017 adjusted microprice
//!
//! Extends Stoikov by incorporating L2 information. The full continuous-time
//! correction involves a martingale projection on the joint depth-imbalance
//! state. The discrete approximation we use:
//!
//! ```text
//!   M_adjusted = M_stoikov + λ · imb_L2 · (mid_L2 − mid_L1)
//! ```
//!
//! where `λ = (V_b^{L2} + V_a^{L2}) / (V_b^{L1} + V_a^{L1})` weights the
//! correction by L2 / L1 depth ratio. Captures **directional pressure at
//! depth beyond L1**.
//!
//! ## Multi-level microprice
//!
//! Generalized Stoikov over the top-K levels of the book. Each level's price
//! is weighted by the OPPOSITE side's depth at that level. Captures
//! consensus across multiple book levels.
//!
//! ## Block N (5 features) — stateful Stoikov derivatives
//! 1. `stoikov_microprice` — raw microprice
//! 2. `microprice_change` — first difference `M_t − M_{t-1}`
//! 3. `microprice_acceleration` — second difference `M_t − 2·M_{t-1} + M_{t-2}`
//! 4. `microprice_residual` — signed gap `M_t − mid_t`
//! 5. `microprice_residual_sign` — sign of (4), useful for tree models
//!
//! ## Block V (3 features) — multi-level + Cartea variants
//! 6. `microprice_top3` — multi-level microprice over L1-L3
//! 7. `microprice_top5` — multi-level microprice over L1-L5
//! 8. `microprice_cartea` — Cartea-Jaimungal adjusted microprice

use data::providers::databento::mbp10::{BidAskPair, Mbp10Snapshot};

/// Alpha Blocks N + V — stateful microprice aggregator.
///
/// Holds rolling history of recent Stoikov microprices to compute first and
/// second differences (Block N features 2-3). All other features are pure
/// functions of the current snapshot.
#[derive(Debug, Clone, Default)]
pub struct MicropriceFeatures {
    /// Previous Stoikov microprice (one step back).
    prev_stoikov: Option<f64>,
    /// Two steps back (for second-difference / acceleration).
    prev_prev_stoikov: Option<f64>,
}

impl MicropriceFeatures {
    pub const fn new() -> Self {
        Self {
            prev_stoikov: None,
            prev_prev_stoikov: None,
        }
    }

    /// Compute all 8 N+V features from `snapshot`.
    ///
    /// Returns `[stoikov, Δstoikov, Δ²stoikov, residual, residual_sign,
    /// mp_top3, mp_top5, cartea]`.
    ///
    /// The first/second-difference features are 0 on the first/second call
    /// respectively (no prior state). Stoikov gracefully degrades to mid when
    /// L1 has zero depth on either side.
    pub fn update(&mut self, snapshot: &Mbp10Snapshot) -> [f64; 8] {
        let stoikov = stoikov_microprice(snapshot);
        let mid = snapshot.mid_price();
        let residual = stoikov - mid;
        let residual_sign = residual.signum();
        let mp_top3 = multilevel_microprice(snapshot, 3);
        let mp_top5 = multilevel_microprice(snapshot, 5);
        let cartea = cartea_adjusted_microprice(snapshot, stoikov);

        // First difference: Δ = M_t − M_{t-1}
        let dmp = match self.prev_stoikov {
            Some(p) => stoikov - p,
            None => 0.0,
        };
        // Second difference: Δ² = M_t − 2·M_{t-1} + M_{t-2}
        let ddmp = match (self.prev_stoikov, self.prev_prev_stoikov) {
            (Some(p1), Some(p0)) => stoikov - 2.0 * p1 + p0,
            _ => 0.0,
        };

        // Shift the state forward
        self.prev_prev_stoikov = self.prev_stoikov;
        self.prev_stoikov = Some(stoikov);

        [stoikov, dmp, ddmp, residual, residual_sign, mp_top3, mp_top5, cartea]
    }

    pub fn reset(&mut self) {
        self.prev_stoikov = None;
        self.prev_prev_stoikov = None;
    }

    /// Whether enough history has accumulated to produce non-zero acceleration.
    pub fn is_ready(&self) -> bool {
        self.prev_prev_stoikov.is_some()
    }
}

/// **Stoikov 2014 microprice**: `M = (P_b · V_a + P_a · V_b) / (V_a + V_b)`.
///
/// Edge cases:
/// - If either side has zero depth at L1: falls back to `mid_price()` (the
///   weight denominator would be the other side's size only).
/// - If both sides have zero depth: returns 0 (degenerate book).
pub fn stoikov_microprice(snapshot: &Mbp10Snapshot) -> f64 {
    if snapshot.levels.is_empty() {
        return 0.0;
    }
    let l1 = &snapshot.levels[0];
    let p_b = BidAskPair::price_to_f64(l1.bid_px);
    let p_a = BidAskPair::price_to_f64(l1.ask_px);
    let v_b = l1.bid_sz as f64;
    let v_a = l1.ask_sz as f64;
    let total = v_a + v_b;
    if total < 1.0 {
        return snapshot.mid_price();
    }
    (p_b * v_a + p_a * v_b) / total
}

/// **Multi-level microprice** over the top-K levels.
///
/// Generalizes Stoikov to K depth levels. For each level l ∈ [0, K), price
/// is weighted by the OPPOSITE side's depth (size_ask weights bid_price,
/// size_bid weights ask_price). Sums across levels for both numerator and
/// denominator:
///
/// ```text
///   M_K = Σ_l (P_b^l · V_a^l + P_a^l · V_b^l)  /  Σ_l (V_a^l + V_b^l)
/// ```
///
/// Falls back to `mid_price()` when total weight is degenerate.
pub fn multilevel_microprice(snapshot: &Mbp10Snapshot, top_k: usize) -> f64 {
    let n = snapshot.levels.len().min(top_k);
    if n == 0 {
        return 0.0;
    }
    let mut num = 0.0_f64;
    let mut den = 0.0_f64;
    for i in 0..n {
        let lvl = &snapshot.levels[i];
        let p_b = BidAskPair::price_to_f64(lvl.bid_px);
        let p_a = BidAskPair::price_to_f64(lvl.ask_px);
        let v_b = lvl.bid_sz as f64;
        let v_a = lvl.ask_sz as f64;
        num += p_b * v_a + p_a * v_b;
        den += v_a + v_b;
    }
    if den < 1.0 {
        return snapshot.mid_price();
    }
    num / den
}

/// **Cartea-Jaimungal 2017 adjusted microprice** (discrete approximation).
///
/// Adds an L2 directional-pressure correction to the L1 Stoikov microprice:
///
/// ```text
///   M_adj = M_stoikov + λ · imb_L2 · (mid_L2 − mid_L1)
///   imb_L2 = (V_b^{L2} − V_a^{L2}) / (V_b^{L2} + V_a^{L2})
///   λ = depth_L2 / depth_L1
/// ```
///
/// The correction adds: "if L2 has bid-heavy imbalance AND L2 mid is above
/// L1 mid, the L1 microprice underestimates the upward pressure". The `λ`
/// weight scales the correction by relative L2 depth.
///
/// Falls back to `stoikov` if fewer than 2 levels or if L2 has zero depth.
pub fn cartea_adjusted_microprice(snapshot: &Mbp10Snapshot, stoikov: f64) -> f64 {
    if snapshot.levels.len() < 2 {
        return stoikov;
    }
    let l1 = &snapshot.levels[0];
    let l2 = &snapshot.levels[1];

    let v_b1 = l1.bid_sz as f64;
    let v_a1 = l1.ask_sz as f64;
    let v_b2 = l2.bid_sz as f64;
    let v_a2 = l2.ask_sz as f64;

    let depth_l1 = v_b1 + v_a1;
    let depth_l2 = v_b2 + v_a2;
    if depth_l1 < 1.0 || depth_l2 < 1.0 {
        return stoikov;
    }

    let imb_l2 = (v_b2 - v_a2) / depth_l2;
    let mid_l1 = (BidAskPair::price_to_f64(l1.bid_px) + BidAskPair::price_to_f64(l1.ask_px)) / 2.0;
    let mid_l2 = (BidAskPair::price_to_f64(l2.bid_px) + BidAskPair::price_to_f64(l2.ask_px)) / 2.0;

    let lambda = depth_l2 / depth_l1;
    stoikov + lambda * imb_l2 * (mid_l2 - mid_l1)
}

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

    fn snap_with_l1(bid_px: i64, ask_px: i64, bid_sz: u32, ask_sz: u32) -> Mbp10Snapshot {
        let levels = vec![BidAskPair {
            bid_px,
            bid_sz,
            bid_ct: 5,
            ask_px,
            ask_sz,
            ask_ct: 5,
        }];
        Mbp10Snapshot::new("ES.FUT".to_owned(), 0, levels, 0, 0)
    }

    fn snap_with_2levels(
        bid_px_l1: i64,
        ask_px_l1: i64,
        bid_sz_l1: u32,
        ask_sz_l1: u32,
        bid_px_l2: i64,
        ask_px_l2: i64,
        bid_sz_l2: u32,
        ask_sz_l2: u32,
    ) -> Mbp10Snapshot {
        let levels = vec![
            BidAskPair {
                bid_px: bid_px_l1,
                bid_sz: bid_sz_l1,
                bid_ct: 5,
                ask_px: ask_px_l1,
                ask_sz: ask_sz_l1,
                ask_ct: 5,
            },
            BidAskPair {
                bid_px: bid_px_l2,
                bid_sz: bid_sz_l2,
                bid_ct: 5,
                ask_px: ask_px_l2,
                ask_sz: ask_sz_l2,
                ask_ct: 5,
            },
        ];
        Mbp10Snapshot::new("ES.FUT".to_owned(), 0, levels, 0, 0)
    }

    #[test]
    fn test_stoikov_balanced_book_equals_midprice() {
        // bid_sz = ask_sz → microprice = mid (no skew)
        let snap = snap_with_l1(100_000_000_000, 100_040_000_000, 100, 100);
        let mp = stoikov_microprice(&snap);
        let mid = snap.mid_price();
        assert!((mp - mid).abs() < 1e-6, "balanced book: mp={mp}, mid={mid}");
    }

    #[test]
    fn test_stoikov_heavy_ask_tilts_toward_bid() {
        // V_a >> V_b → microprice closer to P_b (heavy ask = next trade hits bid)
        let snap = snap_with_l1(100_000_000_000, 100_040_000_000, 10, 1000);
        let mp = stoikov_microprice(&snap);
        let mid = snap.mid_price();
        // Should be much closer to bid (100.0) than to ask (100.04)
        assert!(mp < mid, "heavy ask should tilt mp below mid");
        let bid_px = BidAskPair::price_to_f64(100_000_000_000);
        let dist_to_bid = (mp - bid_px).abs();
        let dist_to_mid = (mp - mid).abs();
        assert!(
            dist_to_bid < dist_to_mid,
            "heavy ask should put mp closer to bid than to mid (bid_dist={dist_to_bid}, mid_dist={dist_to_mid})"
        );
    }

    #[test]
    fn test_stoikov_heavy_bid_tilts_toward_ask() {
        let snap = snap_with_l1(100_000_000_000, 100_040_000_000, 1000, 10);
        let mp = stoikov_microprice(&snap);
        let mid = snap.mid_price();
        assert!(mp > mid, "heavy bid should tilt mp above mid");
    }

    #[test]
    fn test_stoikov_zero_depth_falls_back_to_mid() {
        let snap = snap_with_l1(100_000_000_000, 100_040_000_000, 0, 0);
        let mp = stoikov_microprice(&snap);
        let mid = snap.mid_price();
        assert!(
            (mp - mid).abs() < 1e-6,
            "zero depth must fall back to mid, got mp={mp} mid={mid}"
        );
    }

    #[test]
    fn test_multilevel_microprice_one_level_equals_stoikov() {
        let snap = snap_with_l1(100_000_000_000, 100_040_000_000, 100, 200);
        let stoikov = stoikov_microprice(&snap);
        let multi = multilevel_microprice(&snap, 1);
        assert!(
            (stoikov - multi).abs() < 1e-6,
            "K=1 multi-level should equal Stoikov, got {stoikov} vs {multi}"
        );
    }

    #[test]
    fn test_multilevel_microprice_includes_deeper_levels() {
        // L1: tight balanced
        // L2: bid-heavy (suggests upward pressure deeper in book)
        // Multi-level over both should tilt toward ask (averaging in L2's heavy-bid signal)
        let snap = snap_with_2levels(
            100_000_000_000, 100_040_000_000, 100, 100, // L1 balanced
            99_960_000_000,  100_080_000_000, 5000, 50,  // L2 heavy-bid
        );
        let stoikov_l1 = stoikov_microprice(&snap);
        let multi_top2 = multilevel_microprice(&snap, 2);
        // L1 alone: balanced → mid
        // Multi-level: heavy L2 bid → tilts upward
        assert!(
            multi_top2 > stoikov_l1,
            "L2 bid-heavy info should pull multi-level above L1-only stoikov ({stoikov_l1} → {multi_top2})"
        );
    }

    #[test]
    fn test_cartea_falls_back_to_stoikov_with_fewer_than_2_levels() {
        let snap = snap_with_l1(100_000_000_000, 100_040_000_000, 100, 100);
        let stoikov = stoikov_microprice(&snap);
        let cartea = cartea_adjusted_microprice(&snap, stoikov);
        assert!((stoikov - cartea).abs() < 1e-6, "<2 levels: Cartea = Stoikov");
    }

    #[test]
    fn test_cartea_l2_bid_heavy_higher_mid_l2_adds_upward_correction() {
        // Stoikov on balanced L1 = mid_L1.
        // L2 bid-heavy + mid_L2 > mid_L1 → Cartea > Stoikov.
        let snap = snap_with_2levels(
            100_000_000_000, 100_040_000_000, 100, 100,
            99_990_000_000,  100_060_000_000, 1000, 100,  // L2: bid-heavy
        );
        let mid_l2 = (BidAskPair::price_to_f64(99_990_000_000)
            + BidAskPair::price_to_f64(100_060_000_000))
            / 2.0;
        let mid_l1 = (BidAskPair::price_to_f64(100_000_000_000)
            + BidAskPair::price_to_f64(100_040_000_000))
            / 2.0;
        assert!(mid_l2 > mid_l1, "test precondition: mid_L2 must be > mid_L1");

        let stoikov = stoikov_microprice(&snap);
        let cartea = cartea_adjusted_microprice(&snap, stoikov);
        assert!(
            cartea > stoikov,
            "L2 bid-heavy with mid_L2 > mid_L1 → Cartea > Stoikov (got {cartea} vs {stoikov})"
        );
    }

    #[test]
    fn test_microprice_features_update_first_call_no_state() {
        let mut mp = MicropriceFeatures::new();
        let snap = snap_with_l1(100_000_000_000, 100_040_000_000, 100, 100);
        let result = mp.update(&snap);
        // First call: Δ and Δ² must be exactly 0
        assert_eq!(result[1], 0.0, "first call: Δ stoikov = 0");
        assert_eq!(result[2], 0.0, "first call: Δ² stoikov = 0");
        assert!(!mp.is_ready(), "after 1 call: not ready for acceleration");
    }

    #[test]
    fn test_microprice_features_acceleration_requires_three_calls() {
        let mut mp = MicropriceFeatures::new();
        let snap1 = snap_with_l1(100_000_000_000, 100_040_000_000, 100, 100);
        let snap2 = snap_with_l1(100_010_000_000, 100_050_000_000, 100, 100);
        let snap3 = snap_with_l1(100_020_000_000, 100_060_000_000, 100, 100);
        let r1 = mp.update(&snap1);
        let r2 = mp.update(&snap2);
        let r3 = mp.update(&snap3);
        // Call 1: Δ=0, Δ²=0
        assert_eq!(r1[1], 0.0);
        assert_eq!(r1[2], 0.0);
        // Call 2: Δ non-zero, Δ²=0
        assert!(r2[1] > 0.0, "rising microprice → Δ > 0");
        assert_eq!(r2[2], 0.0, "still no second-back state for Δ²");
        // Call 3: both non-zero (linear ramp → Δ constant, Δ² = 0 exactly)
        assert!(r3[1] > 0.0);
        assert!(r3[2].abs() < 1e-6, "linear ramp → Δ² ≈ 0, got {}", r3[2]);
        assert!(mp.is_ready());
    }

    #[test]
    fn test_microprice_residual_sign_matches_imbalance() {
        let mut mp = MicropriceFeatures::new();
        // Heavy ask → microprice < mid → residual < 0
        let snap = snap_with_l1(100_000_000_000, 100_040_000_000, 10, 1000);
        let result = mp.update(&snap);
        assert!(result[3] < 0.0, "residual should be negative: got {}", result[3]);
        assert_eq!(result[4], -1.0, "sign should be -1");
    }
}
