use uforia_ms_types::BboQuote;

/// Ring-buffer tracker that stores the most recent BBO quotes.
///
/// Default capacity is 600 entries (60 seconds at 100 ms cadence).
#[derive(Debug, Clone)]
pub struct BboTracker {
    buf: Vec<Option<BboQuote>>,
    /// Next write position (wraps around).
    head: usize,
    /// Number of items currently stored (up to capacity).
    len: usize,
}

impl BboTracker {
    /// Create a new tracker with the given capacity.
    pub fn new(capacity: usize) -> Self {
        assert!(capacity > 0, "BboTracker capacity must be > 0");
        Self {
            buf: (0..capacity).map(|_| None).collect(),
            head: 0,
            len: 0,
        }
    }

    /// Push a new BBO quote into the ring buffer.
    pub fn push(&mut self, quote: BboQuote) {
        self.buf[self.head] = Some(quote);
        self.head = (self.head + 1) % self.capacity();
        if self.len < self.capacity() {
            self.len += 1;
        }
    }

    /// The most recently pushed quote.
    pub fn latest(&self) -> Option<&BboQuote> {
        if self.len == 0 {
            return None;
        }
        let idx = if self.head == 0 {
            self.capacity() - 1
        } else {
            self.head - 1
        };
        self.buf[idx].as_ref()
    }

    /// Number of stored quotes.
    pub fn len(&self) -> usize {
        self.len
    }

    /// Whether the buffer is empty.
    pub fn is_empty(&self) -> bool {
        self.len == 0
    }

    /// Maximum capacity.
    pub fn capacity(&self) -> usize {
        self.buf.len()
    }

    /// Change in spread over the last `lookback` quotes.
    ///
    /// Returns `latest_spread - oldest_spread` where oldest is `lookback` entries
    /// before the latest. Returns `None` if fewer than `lookback` quotes are stored.
    pub fn spread_change(&self, lookback: usize) -> Option<f64> {
        if lookback == 0 || lookback > self.len {
            return None;
        }
        let latest = self.latest()?;
        let older = self.nth_from_latest(lookback - 1)?;
        Some(latest.spread() - older.spread())
    }

    /// Change in total depth (bid_size + ask_size) over the last `lookback` quotes.
    ///
    /// Returns `None` if fewer than `lookback` quotes are stored.
    pub fn depth_change(&self, lookback: usize) -> Option<f64> {
        if lookback == 0 || lookback > self.len {
            return None;
        }
        let latest = self.latest()?;
        let older = self.nth_from_latest(lookback - 1)?;
        let latest_depth = latest.bid_size + latest.ask_size;
        let older_depth = older.bid_size + older.ask_size;
        Some(latest_depth - older_depth)
    }

    /// Get the quote that is `n` positions before the latest (0 = latest itself).
    fn nth_from_latest(&self, n: usize) -> Option<&BboQuote> {
        if n >= self.len {
            return None;
        }
        let cap = self.capacity();
        // latest is at head - 1; n before that is head - 1 - n
        let idx = (self.head + cap - 1 - n) % cap;
        self.buf[idx].as_ref()
    }
}

impl Default for BboTracker {
    fn default() -> Self {
        Self::new(600)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Utc;
    use uforia_ms_types::{Asset, Venue};

    fn make_quote(bid_price: f64, bid_size: f64, ask_price: f64, ask_size: f64) -> BboQuote {
        BboQuote {
            timestamp: Utc::now(),
            asset: Asset::Btc,
            venue: Venue::BinanceSpot,
            bid_price,
            bid_size,
            ask_price,
            ask_size,
        }
    }

    #[test]
    fn test_empty_tracker() {
        let tracker = BboTracker::new(10);
        assert!(tracker.is_empty());
        assert_eq!(tracker.len(), 0);
        assert!(tracker.latest().is_none());
        assert!(tracker.spread_change(1).is_none());
        assert!(tracker.depth_change(1).is_none());
    }

    #[test]
    fn test_push_and_latest() {
        let mut tracker = BboTracker::new(10);
        tracker.push(make_quote(100.0, 1.0, 101.0, 1.0));
        assert_eq!(tracker.len(), 1);
        assert!(!tracker.is_empty());

        let latest = tracker.latest().unwrap();
        assert!((latest.bid_price - 100.0).abs() < 1e-10);
        assert!((latest.ask_price - 101.0).abs() < 1e-10);
    }

    #[test]
    fn test_spread_change() {
        let mut tracker = BboTracker::new(10);

        // Quote 0: spread = 1.0
        tracker.push(make_quote(100.0, 1.0, 101.0, 1.0));
        // Quote 1: spread = 2.0
        tracker.push(make_quote(100.0, 1.0, 102.0, 1.0));
        // Quote 2: spread = 3.0
        tracker.push(make_quote(100.0, 1.0, 103.0, 1.0));

        // lookback=1: compare latest (spread=3) with itself -> 0
        // Actually lookback=1 means: latest vs (lookback-1=0) = latest itself
        // spread_change(1) = latest.spread - nth_from_latest(0).spread = 3 - 3 = 0
        assert!((tracker.spread_change(1).unwrap() - 0.0).abs() < 1e-10);

        // lookback=2: latest (spread=3) vs 1-before (spread=2) -> 1.0
        assert!((tracker.spread_change(2).unwrap() - 1.0).abs() < 1e-10);

        // lookback=3: latest (spread=3) vs 2-before (spread=1) -> 2.0
        assert!((tracker.spread_change(3).unwrap() - 2.0).abs() < 1e-10);

        // lookback=4: not enough data
        assert!(tracker.spread_change(4).is_none());
    }

    #[test]
    fn test_depth_change() {
        let mut tracker = BboTracker::new(10);

        // Quote 0: depth = 1+1 = 2
        tracker.push(make_quote(100.0, 1.0, 101.0, 1.0));
        // Quote 1: depth = 3+2 = 5
        tracker.push(make_quote(100.0, 3.0, 101.0, 2.0));

        // lookback=2: latest depth(5) - oldest depth(2) = 3
        assert!((tracker.depth_change(2).unwrap() - 3.0).abs() < 1e-10);
    }

    #[test]
    fn test_ring_buffer_wraps() {
        let mut tracker = BboTracker::new(3);

        tracker.push(make_quote(100.0, 1.0, 101.0, 1.0)); // idx 0
        tracker.push(make_quote(200.0, 1.0, 201.0, 1.0)); // idx 1
        tracker.push(make_quote(300.0, 1.0, 301.0, 1.0)); // idx 2, buffer full
        assert_eq!(tracker.len(), 3);

        // Push another - should overwrite idx 0
        tracker.push(make_quote(400.0, 1.0, 401.0, 1.0)); // idx 0, overwrites first
        assert_eq!(tracker.len(), 3); // still 3

        let latest = tracker.latest().unwrap();
        assert!((latest.bid_price - 400.0).abs() < 1e-10);

        // Oldest remaining should be quote at idx 1 (bid=200)
        let oldest = tracker.nth_from_latest(2).unwrap();
        assert!((oldest.bid_price - 200.0).abs() < 1e-10);
    }

    #[test]
    fn test_default_capacity() {
        let tracker = BboTracker::default();
        assert_eq!(tracker.capacity(), 600);
    }

    #[test]
    fn test_spread_change_zero_lookback() {
        let mut tracker = BboTracker::new(10);
        tracker.push(make_quote(100.0, 1.0, 101.0, 1.0));
        assert!(tracker.spread_change(0).is_none());
    }

    #[test]
    fn test_synthetic_sequence() {
        let mut tracker = BboTracker::new(100);

        // Simulate a tightening spread scenario
        for i in 0..20 {
            let spread = 10.0 - (i as f64) * 0.4; // 10.0 down to 2.4
            let bid = 50000.0;
            let ask = bid + spread;
            tracker.push(make_quote(bid, 1.0 + i as f64, ask, 1.0 + i as f64));
        }

        assert_eq!(tracker.len(), 20);

        // Spread shrank by 0.4 * 19 = 7.6 total
        let change = tracker.spread_change(20).unwrap();
        assert!((change - (-7.6)).abs() < 1e-10);

        // Depth grew: latest depth = (1+19)*2 = 40, oldest = 1*2 = 2 -> +38
        let depth_ch = tracker.depth_change(20).unwrap();
        assert!((depth_ch - 38.0).abs() < 1e-10);
    }
}
