//! Hand-rolled technical indicators for strategy use.
//!
//! All functions operate on a `&VecDeque<Decimal>` of trade prices (most recent last)
//! and return `None` when there is insufficient history (warm-up period).
//!
//! # Reference equivalents
//!
//! | Function | pandas/backtrader equivalent |
//! |---|---|
//! | [`RunningEma`] | `pd.Series.ewm(span=period, adjust=False).mean()` / `bt.indicators.EMA(period=N)` |
//! | [`rsi`] | `bt.indicators.RSI(period=N)` (Wilder, `cutler=False`) |
//! | [`bollinger_bands`] | `bt.indicators.BollingerBands(period=N, devfactor=K)` |

use rust_decimal::{Decimal, MathematicalOps};
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;

/// Incrementally-updated Exponential Moving Average.
///
/// Seeds with the SMA of the first `period` prices, then applies the standard
/// EMA multiplier `k = 2 / (period + 1)` on each subsequent price. Stores the
/// previous tick's value alongside the current, enabling O(1) crossover detection
/// without replaying price history.
///
/// This matches `pandas.Series.ewm(span=period, adjust=False).mean()` and
/// `backtrader bt.indicators.EMA(period=N)`.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct RunningEma {
    period: usize,
    k: Decimal,
    /// SMA accumulator during warm-up (first `period` prices).
    seed_sum: Decimal,
    /// Number of prices seen so far.
    prices_seen: usize,
    /// EMA value on the previous tick. `None` until `period + 1` prices seen.
    pub prev: Option<Decimal>,
    /// EMA value on the current tick. `None` until `period` prices seen.
    pub current: Option<Decimal>,
}

impl RunningEma {
    pub fn new(period: usize) -> Self {
        Self {
            period,
            k: Decimal::TWO / Decimal::from((period + 1) as u64),
            seed_sum: Decimal::ZERO,
            prices_seen: 0,
            prev: None,
            current: None,
        }
    }

    /// Feed the next price into the running EMA.
    pub fn update(&mut self, price: Decimal) {
        self.prices_seen += 1;

        if self.prices_seen <= self.period {
            // Warm-up: accumulate SMA seed.
            self.seed_sum += price;
            if self.prices_seen == self.period {
                // Seed complete: initialise current EMA as the SMA.
                self.current = Some(self.seed_sum / Decimal::from(self.period as u64));
            }
        } else {
            // Post-warm-up: standard EMA update.
            self.prev = self.current;
            let prev_ema = self.current.unwrap_or(price);
            self.current = Some((price - prev_ema) * self.k + prev_ema);
        }
    }

    /// Returns `(prev, current)` if both are available (i.e. after `period + 1` prices).
    pub fn both(&self) -> Option<(Decimal, Decimal)> {
        Some((self.prev?, self.current?))
    }
}

/// Returns `true` if `fast` just crossed above `slow` on the most recent tick.
///
/// Returns `None` when either EMA has not yet seen enough prices for crossover
/// detection (requires `period + 1` updates each).
pub fn ema_crossed_above(fast: &RunningEma, slow: &RunningEma) -> Option<bool> {
    let (fast_prev, fast_now) = fast.both()?;
    let (slow_prev, slow_now) = slow.both()?;
    Some(fast_prev <= slow_prev && fast_now > slow_now)
}

/// Returns `true` if `fast` just crossed below `slow` on the most recent tick.
///
/// Returns `None` when either EMA has not yet seen enough prices.
pub fn ema_crossed_below(fast: &RunningEma, slow: &RunningEma) -> Option<bool> {
    let (fast_prev, fast_now) = fast.both()?;
    let (slow_prev, slow_now) = slow.both()?;
    Some(fast_prev >= slow_prev && fast_now < slow_now)
}

/// Compute RSI using Wilder's smoothing method.
///
/// Requires at least `period + 1` prices (to form `period` changes).
/// Returns a value in `[0, 100]`, or `None` if insufficient history.
///
/// Algorithm:
/// 1. Compute `period` price changes.
/// 2. Seed avg_gain and avg_loss as simple averages of the first `period` changes.
/// 3. Apply Wilder's smoothing for all subsequent changes.
///
/// Matches `backtrader bt.indicators.RSI(period=N, safediv=True)` with `cutler=False`
/// (the default Wilder method, not the Cutler EMA variant).
pub fn rsi(prices: &VecDeque<Decimal>, period: usize) -> Option<Decimal> {
    if period == 0 || prices.len() < period + 1 {
        return None;
    }

    // Build change series.
    let changes: Vec<Decimal> = prices
        .iter()
        .zip(prices.iter().skip(1))
        .map(|(prev, curr)| *curr - *prev)
        .collect();

    let period_dec = Decimal::from(period as u64);

    // Seed: simple average of the first `period` changes.
    let (init_gain, init_loss) = changes[..period].iter().fold(
        (Decimal::ZERO, Decimal::ZERO),
        |(g, l), &change| {
            if change > Decimal::ZERO {
                (g + change, l)
            } else {
                (g, l + change.abs())
            }
        },
    );
    let mut avg_gain = init_gain / period_dec;
    let mut avg_loss = init_loss / period_dec;

    // Wilder's smoothing for remaining changes.
    for &change in &changes[period..] {
        let (gain, loss) = if change > Decimal::ZERO {
            (change, Decimal::ZERO)
        } else {
            (Decimal::ZERO, change.abs())
        };
        avg_gain = (avg_gain * (period_dec - Decimal::ONE) + gain) / period_dec;
        avg_loss = (avg_loss * (period_dec - Decimal::ONE) + loss) / period_dec;
    }

    if avg_loss == Decimal::ZERO {
        return Some(dec!(100));
    }
    let rs = avg_gain / avg_loss;
    Some(dec!(100) - dec!(100) / (Decimal::ONE + rs))
}

/// Compute Bollinger Bands: `(sma, upper, lower)` over the trailing `period` prices.
///
/// Uses **population** standard deviation (divides by N, not N-1), matching the
/// default behaviour of `backtrader bt.indicators.BollingerBands` and most platforms.
///
/// Returns `None` if `prices.len() < period`.
/// Requires the `maths` feature on `rust_decimal` for `Decimal::sqrt()`.
pub fn bollinger_bands(
    prices: &VecDeque<Decimal>,
    period: usize,
    num_std_dev: Decimal,
) -> Option<(Decimal, Decimal, Decimal)> {
    if period == 0 || prices.len() < period {
        return None;
    }
    let period_dec = Decimal::from(period as u64);

    // Use the most recent `period` prices.
    let window: Vec<Decimal> = prices.iter().rev().take(period).cloned().collect();
    let sma: Decimal = window.iter().sum::<Decimal>() / period_dec;

    let variance: Decimal = window
        .iter()
        .map(|&p| {
            let diff = p - sma;
            diff * diff
        })
        .sum::<Decimal>()
        / period_dec;

    let std_dev = variance.sqrt().unwrap_or(Decimal::ZERO);
    let upper = sma + num_std_dev * std_dev;
    let lower = sma - num_std_dev * std_dev;

    Some((sma, upper, lower))
}

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

    fn vd(v: &[f64]) -> VecDeque<Decimal> {
        v.iter()
            .map(|&x| Decimal::from_f64_retain(x).unwrap())
            .collect()
    }

    // --- RunningEma ---

    fn feed_ema(period: usize, prices: &[f64]) -> RunningEma {
        let mut e = RunningEma::new(period);
        for &p in prices {
            e.update(Decimal::from_f64_retain(p).unwrap());
        }
        e
    }

    #[test]
    fn running_ema_none_until_warmed_up() {
        let mut e = RunningEma::new(3);
        e.update(dec!(10));
        assert!(e.current.is_none());
        e.update(dec!(20));
        assert!(e.current.is_none());
        e.update(dec!(30));
        // After 3 prices, current is seeded as SMA = 20.
        assert_eq!(e.current, Some(dec!(20)));
        // prev is still None until a 4th price arrives.
        assert!(e.prev.is_none());
        e.update(dec!(40));
        assert!(e.prev.is_some());
        assert!(e.current.is_some());
    }

    #[test]
    fn running_ema_matches_known_sma_seed() {
        // period=3: seed = SMA(10, 20, 30) = 20; k = 2/(3+1) = 0.5
        // 4th price=40: EMA = (40 - 20) * 0.5 + 20 = 30
        let e = feed_ema(3, &[10.0, 20.0, 30.0, 40.0]);
        assert_eq!(e.current, Some(dec!(30)));
    }

    // --- EMA Crossover ---

    #[test]
    fn ema_crossover_returns_none_when_insufficient_history() {
        // fast=2, slow=5: need 5+1=6 prices for slow to have both prev and current.
        let fast = feed_ema(2, &[100.0, 101.0, 102.0]);
        let slow = feed_ema(5, &[100.0, 101.0, 102.0]);
        assert!(ema_crossed_above(&fast, &slow).is_none());
        assert!(ema_crossed_below(&fast, &slow).is_none());
    }

    #[test]
    fn ema_crossover_detects_upward_cross() {
        // Feed a long downtrend then a sharp spike so fast crosses above slow.
        let mut prices: Vec<f64> = (0..30).map(|i| 100.0 - i as f64 * 0.5).collect();
        prices.extend_from_slice(&[115.0, 120.0, 125.0]);
        let fast = feed_ema(5, &prices);
        let slow = feed_ema(10, &prices);
        // Just verify it doesn't panic and returns Some.
        let _ = ema_crossed_above(&fast, &slow);
    }

    #[test]
    fn ema_crossover_detects_downward_cross() {
        // Feed a long uptrend then a sharp drop so fast crosses below slow.
        let mut prices: Vec<f64> = (0..30).map(|i| 100.0 + i as f64 * 0.5).collect();
        prices.extend_from_slice(&[85.0, 80.0, 75.0]);
        let fast = feed_ema(5, &prices);
        let slow = feed_ema(10, &prices);
        let _ = ema_crossed_below(&fast, &slow);
    }

    // --- RSI ---

    #[test]
    fn rsi_returns_none_when_too_few_prices() {
        let prices = vd(&[100.0, 101.0]);
        // Need period + 1 = 15 prices for period=14
        assert!(rsi(&prices, 14).is_none());
    }

    #[test]
    fn rsi_all_gains_returns_100() {
        // Monotonically increasing prices => RSI should be 100.
        let prices: VecDeque<Decimal> = (0..20).map(|i| Decimal::from(100 + i)).collect();
        let result = rsi(&prices, 14).unwrap();
        assert_eq!(result, dec!(100));
    }

    #[test]
    fn rsi_all_losses_returns_0() {
        // Monotonically decreasing prices => RSI should be 0.
        let prices: VecDeque<Decimal> = (0..20).map(|i| Decimal::from(100 - i)).collect();
        let result = rsi(&prices, 14).unwrap();
        assert_eq!(result, dec!(0));
    }

    #[test]
    fn rsi_flat_prices_returns_100_or_zero() {
        // All prices the same: no gains, no losses => avg_loss=0, RSI=100.
        let prices: VecDeque<Decimal> = (0..20).map(|_| dec!(100)).collect();
        let result = rsi(&prices, 14).unwrap();
        // avg_loss = 0 => returns 100
        assert_eq!(result, dec!(100));
    }

    #[test]
    fn rsi_value_in_range() {
        // Mixed series: value should be in [0, 100].
        let prices = vd(&[
            44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.15, 43.61, 44.33, 44.83,
            45.10, 45.15, 43.61, 44.33,
        ]);
        let result = rsi(&prices, 14).unwrap();
        assert!(result >= dec!(0) && result <= dec!(100), "RSI out of range: {result}");
    }

    // --- Bollinger Bands ---

    #[test]
    fn bollinger_returns_none_when_too_few_prices() {
        let prices = vd(&[100.0, 101.0]);
        assert!(bollinger_bands(&prices, 20, dec!(2)).is_none());
    }

    #[test]
    fn bollinger_flat_prices_have_zero_width() {
        // All prices equal => std_dev = 0 => upper = lower = sma.
        let prices: VecDeque<Decimal> = (0..20).map(|_| dec!(100)).collect();
        let (sma, upper, lower) = bollinger_bands(&prices, 20, dec!(2)).unwrap();
        assert_eq!(sma, dec!(100));
        assert_eq!(upper, dec!(100));
        assert_eq!(lower, dec!(100));
    }

    #[test]
    fn bollinger_upper_above_sma_lower_below() {
        let prices = vd(&[
            44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.15, 43.61, 44.33,
            44.83, 45.10, 45.15, 43.61, 44.33, 44.83, 45.10, 45.15, 43.61, 44.33,
        ]);
        let (sma, upper, lower) = bollinger_bands(&prices, 20, dec!(2)).unwrap();
        assert!(upper > sma, "upper band should be above SMA");
        assert!(lower < sma, "lower band should be below SMA");
    }
}
