//! OHLCV bar aggregation (resampling) for multi-timeframe analysis.
//!
//! Aggregates 1-minute OHLCV bars into higher timeframes (5m, 15m, 1h).
//! Uses correct OHLC aggregation: open=first, high=max, low=min, close=last, volume=sum.

use crate::OHLCVBar;

/// Aggregates 1-minute OHLCV bars into 5m, 15m, and 1h bars.
///
/// Buffers incoming 1-minute bars and emits resampled bars when the
/// required number of constituent bars has been collected.
///
/// # Aggregation rules
/// - **open**: first bar's open
/// - **high**: max of all highs
/// - **low**: min of all lows
/// - **close**: last bar's close
/// - **volume**: sum of all volumes
/// - **timestamp**: first bar's timestamp
#[derive(Debug, Clone)]
pub struct BarResampler {
    buffer_5m: Vec<OHLCVBar>,
    buffer_15m: Vec<OHLCVBar>,
    buffer_1h: Vec<OHLCVBar>,
}

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

impl BarResampler {
    /// Create a new resampler with empty buffers.
    pub fn new() -> Self {
        Self {
            buffer_5m: Vec::with_capacity(5),
            buffer_15m: Vec::with_capacity(15),
            buffer_1h: Vec::with_capacity(60),
        }
    }

    /// Feed a 1-minute bar into the resampler.
    ///
    /// Returns `(Option<5m_bar>, Option<15m_bar>, Option<1h_bar>)` --
    /// each is `Some` when the corresponding period has been completed.
    pub fn push(
        &mut self,
        bar: OHLCVBar,
    ) -> (Option<OHLCVBar>, Option<OHLCVBar>, Option<OHLCVBar>) {
        self.buffer_5m.push(bar);
        self.buffer_15m.push(bar);
        self.buffer_1h.push(bar);

        let bar_5m = Self::try_aggregate(&mut self.buffer_5m, 5);
        let bar_15m = Self::try_aggregate(&mut self.buffer_15m, 15);
        let bar_1h = Self::try_aggregate(&mut self.buffer_1h, 60);

        (bar_5m, bar_15m, bar_1h)
    }

    /// Clear all internal buffers.
    pub fn reset(&mut self) {
        self.buffer_5m.clear();
        self.buffer_15m.clear();
        self.buffer_1h.clear();
    }

    /// If `buf` has reached `period` bars, aggregate and drain it.
    fn try_aggregate(buf: &mut Vec<OHLCVBar>, period: usize) -> Option<OHLCVBar> {
        (buf.len() >= period).then(|| {
            let agg = Self::aggregate(buf);
            buf.clear();
            agg
        })
    }

    /// Aggregate a non-empty slice of bars into a single OHLCV bar.
    ///
    /// Returns `OHLCVBar::default()` if `bars` is empty (defensive).
    fn aggregate(bars: &[OHLCVBar]) -> OHLCVBar {
        let Some(first) = bars.first() else {
            return OHLCVBar::default();
        };
        let Some(last) = bars.last() else {
            return OHLCVBar::default();
        };

        let mut high = first.high;
        let mut low = first.low;
        let mut volume = 0.0_f64;

        for bar in bars {
            if bar.high > high {
                high = bar.high;
            }
            if bar.low < low {
                low = bar.low;
            }
            volume += bar.volume;
        }

        OHLCVBar {
            timestamp: first.timestamp,
            open: first.open,
            high,
            low,
            close: last.close,
            volume,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::{TimeZone, Timelike, Utc};

    fn make_bar(minute: u32, open: f64, high: f64, low: f64, close: f64, volume: f64) -> OHLCVBar {
        OHLCVBar {
            timestamp: Utc
                .with_ymd_and_hms(2026, 1, 1, 10, minute, 0)
                .single()
                .unwrap_or_else(Utc::now),
            open,
            high,
            low,
            close,
            volume,
        }
    }

    #[test]
    fn test_5m_aggregation() {
        let mut resampler = BarResampler::new();

        // Push 4 bars -- nothing emitted yet
        for i in 0..4 {
            let (b5, _, _) = resampler.push(make_bar(i, 100.0, 105.0, 95.0, 102.0, 1000.0));
            assert!(b5.is_none(), "should not emit 5m bar before 5 bars");
        }

        // 5th bar with distinct OHLC to verify aggregation
        let (b5, _, _) = resampler.push(make_bar(4, 103.0, 110.0, 90.0, 108.0, 2000.0));
        let bar = b5.expect("should emit 5m bar after 5 bars");

        // open = first bar's open
        assert!((bar.open - 100.0).abs() < f64::EPSILON);
        // high = max of all highs (110.0)
        assert!((bar.high - 110.0).abs() < f64::EPSILON);
        // low = min of all lows (90.0)
        assert!((bar.low - 90.0).abs() < f64::EPSILON);
        // close = last bar's close
        assert!((bar.close - 108.0).abs() < f64::EPSILON);
        // timestamp = first bar's timestamp
        assert_eq!(bar.timestamp.minute(), 0);
    }

    #[test]
    fn test_15m_aggregation() {
        let mut resampler = BarResampler::new();

        for i in 0..14 {
            let (_, b15, _) = resampler.push(make_bar(i, 100.0, 105.0, 95.0, 102.0, 500.0));
            assert!(b15.is_none());
        }

        let (_, b15, _) = resampler.push(make_bar(14, 103.0, 120.0, 85.0, 115.0, 1500.0));
        let bar = b15.expect("should emit 15m bar after 15 bars");

        assert!((bar.open - 100.0).abs() < f64::EPSILON);
        assert!((bar.high - 120.0).abs() < f64::EPSILON);
        assert!((bar.low - 85.0).abs() < f64::EPSILON);
        assert!((bar.close - 115.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_1h_aggregation() {
        let mut resampler = BarResampler::new();

        for i in 0..59 {
            let m = i % 60;
            let (_, _, b1h) = resampler.push(make_bar(m, 100.0, 105.0, 95.0, 102.0, 100.0));
            assert!(b1h.is_none());
        }

        let (_, _, b1h) = resampler.push(make_bar(59, 103.0, 130.0, 80.0, 125.0, 200.0));
        let bar = b1h.expect("should emit 1h bar after 60 bars");

        assert!((bar.open - 100.0).abs() < f64::EPSILON);
        assert!((bar.high - 130.0).abs() < f64::EPSILON);
        assert!((bar.low - 80.0).abs() < f64::EPSILON);
        assert!((bar.close - 125.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_partial_period() {
        let mut resampler = BarResampler::new();

        // Push 3 bars -- no period completes
        for i in 0..3 {
            let (b5, b15, b1h) = resampler.push(make_bar(i, 100.0, 105.0, 95.0, 102.0, 100.0));
            assert!(b5.is_none());
            assert!(b15.is_none());
            assert!(b1h.is_none());
        }
    }

    #[test]
    fn test_volume_summed() {
        let mut resampler = BarResampler::new();

        for i in 0..5 {
            resampler.push(make_bar(i, 100.0, 105.0, 95.0, 102.0, 300.0 + f64::from(i)));
        }

        // Reset and do it again to check volume sum precisely
        resampler.reset();

        let volumes = [100.0, 200.0, 300.0, 400.0, 500.0];
        for (i, &v) in volumes.iter().enumerate() {
            let i_u32 = i as u32;
            resampler.push(make_bar(i_u32, 100.0, 105.0, 95.0, 102.0, v));
        }

        // The 5m buffer already emitted on the 5th push above.
        // So we need a fresh run. Let's reset and push again.
        resampler.reset();

        let mut last_result = (None, None, None);
        for (i, &v) in volumes.iter().enumerate() {
            let i_u32 = i as u32;
            last_result = resampler.push(make_bar(i_u32, 100.0, 105.0, 95.0, 102.0, v));
        }
        let bar = last_result.0.expect("should emit 5m bar");
        let expected_volume: f64 = volumes.iter().sum();
        assert!(
            (bar.volume - expected_volume).abs() < f64::EPSILON,
            "volume should be sum: expected {expected_volume}, got {}",
            bar.volume
        );
    }

    #[test]
    fn test_reset() {
        let mut resampler = BarResampler::new();

        // Push 3 bars
        for i in 0..3 {
            resampler.push(make_bar(i, 100.0, 105.0, 95.0, 102.0, 100.0));
        }

        resampler.reset();

        // After reset, need full 5 bars again for 5m emission
        for i in 0..4 {
            let (b5, _, _) = resampler.push(make_bar(i, 100.0, 105.0, 95.0, 102.0, 100.0));
            assert!(b5.is_none(), "should not emit 5m bar before 5 bars post-reset");
        }

        let (b5, _, _) = resampler.push(make_bar(4, 100.0, 105.0, 95.0, 102.0, 100.0));
        assert!(b5.is_some(), "should emit 5m bar after 5 bars post-reset");
    }

    #[test]
    fn test_multiple_periods() {
        let mut resampler = BarResampler::new();
        let mut count_5m = 0;

        // Push 10 bars -- should get exactly 2 five-minute bars
        for i in 0..10 {
            let (b5, _, _) = resampler.push(make_bar(i, 100.0, 105.0, 95.0, 102.0, 100.0));
            if b5.is_some() {
                count_5m += 1;
            }
        }

        assert_eq!(count_5m, 2, "10 one-minute bars should produce 2 five-minute bars");
    }
}
