//! Streaming-optimized statistics for open deviation bar analysis
//!
//! Clean implementation using production-proven streaming statistics crates:
//! - tdigests: t-digest algorithm for streaming percentiles
//! - rolling-stats: Welford's algorithm for numerically stable variance
//! - online-statistics: Comprehensive streaming statistics with serialization

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use opendeviationbar_core::{AggTrade, OpenDeviationBar};

/// Core streaming statistics engine optimized for open deviation bar processing
pub struct StreamingStatsEngine {
    /// Configuration for statistical computation
    _config: StreamingConfig,

    /// Trade-level streaming statistics
    trade_stats: TradeStats,

    /// Open deviation bar streaming statistics
    bar_stats: BarStats,
}

/// Configuration for streaming statistics computation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamingConfig {
    /// Enable percentile computation (uses t-digest)
    pub enable_percentiles: bool,

    /// Enable rolling statistics (uses Welford's algorithm)
    pub enable_rolling_stats: bool,

    /// Window size for rolling statistics
    pub rolling_window_size: usize,

    /// T-digest compression parameter (higher = more accurate, more memory)
    pub tdigest_compression: f64,
}

impl Default for StreamingConfig {
    fn default() -> Self {
        Self {
            enable_percentiles: true,
            enable_rolling_stats: true,
            rolling_window_size: 1000,
            tdigest_compression: 100.0, // Good balance of accuracy/memory
        }
    }
}

/// Trade-level streaming statistics
pub struct TradeStats {
    count: u64,

    #[cfg(feature = "stats")]
    price_values: Vec<f64>,

    #[cfg(feature = "stats")]
    volume_values: Vec<f64>,

    #[cfg(feature = "stats")]
    rolling_price: rolling_stats::Stats<f64>,

    #[cfg(feature = "stats")]
    rolling_volume: rolling_stats::Stats<f64>,
}

/// OHLC field discriminant for fixed-size array indexing.
/// Memory v2: Replaces HashMap<String, _> with [_; 4] for 4 fixed OHLC keys.
#[derive(Clone, Copy)]
enum OhlcField {
    Open = 0,
    High = 1,
    Low = 2,
    Close = 3,
}

/// Open deviation bar streaming statistics
pub struct BarStats {
    count: u64,

    #[cfg(feature = "stats")]
    ohlc_values: [Vec<f64>; 4],

    #[cfg(feature = "stats")]
    rolling_ohlc: [rolling_stats::Stats<f64>; 4],
}

/// Serializable statistics snapshot
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatisticsSnapshot {
    /// Trade count processed
    pub trade_count: u64,

    /// Bar count generated
    pub bar_count: u64,

    /// Price statistics
    pub price_stats: PriceStatistics,

    /// Volume statistics
    pub volume_stats: VolumeStatistics,

    /// OHLC statistics
    pub ohlc_stats: OhlcStatistics,

    /// Timestamp of snapshot
    pub timestamp: String,
}

/// Price streaming statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceStatistics {
    /// Percentiles (P50, P75, P90, P95, P99)
    pub percentiles: HashMap<String, f64>,

    /// Rolling statistics
    pub rolling: RollingStats,

    /// Price range (min/max)
    pub range: (f64, f64),
}

/// Volume streaming statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VolumeStatistics {
    /// Percentiles (P50, P75, P90, P95, P99)
    pub percentiles: HashMap<String, f64>,

    /// Rolling statistics
    pub rolling: RollingStats,

    /// Volume range (min/max)
    pub range: (f64, f64),
}

/// OHLC streaming statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OhlcStatistics {
    /// Open price statistics
    pub open: PriceStatistics,

    /// High price statistics
    pub high: PriceStatistics,

    /// Low price statistics
    pub low: PriceStatistics,

    /// Close price statistics
    pub close: PriceStatistics,
}

/// Rolling window statistics (Welford's algorithm)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RollingStats {
    /// Mean
    pub mean: f64,

    /// Variance (sample)
    pub variance: f64,

    /// Standard deviation
    pub std_dev: f64,

    /// Count in rolling window
    pub count: u64,
}

impl StreamingStatsEngine {
    /// Create new streaming statistics engine
    pub fn new() -> Self {
        Self::with_config(StreamingConfig::default())
    }

    /// Create with custom configuration
    pub fn with_config(config: StreamingConfig) -> Self {
        Self {
            _config: config.clone(),
            trade_stats: TradeStats::new(&config),
            bar_stats: BarStats::new(&config),
        }
    }

    /// Process single trade for streaming statistics
    pub fn process_trade(&mut self, trade: &AggTrade) {
        self.trade_stats.update(trade);
    }

    /// Process single open deviation bar for streaming statistics
    pub fn process_bar(&mut self, bar: &OpenDeviationBar) {
        self.bar_stats.update(bar);
    }

    /// Get current statistics snapshot (serializable)
    pub fn snapshot(&self) -> StatisticsSnapshot {
        StatisticsSnapshot {
            trade_count: self.trade_stats.count,
            bar_count: self.bar_stats.count,
            price_stats: self.trade_stats.price_statistics(),
            volume_stats: self.trade_stats.volume_statistics(),
            ohlc_stats: self.bar_stats.ohlc_statistics(),
            timestamp: chrono::Utc::now().to_rfc3339(),
        }
    }
}

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

impl TradeStats {
    #[cfg(feature = "stats")]
    fn new(_config: &StreamingConfig) -> Self {
        Self {
            count: 0,
            price_values: Vec::new(),
            volume_values: Vec::new(),
            rolling_price: rolling_stats::Stats::new(),
            rolling_volume: rolling_stats::Stats::new(),
        }
    }

    #[cfg(not(feature = "stats"))]
    fn new(_config: &StreamingConfig) -> Self {
        Self { count: 0 }
    }

    fn update(&mut self, _trade: &AggTrade) {
        self.count += 1;

        #[cfg(feature = "stats")]
        {
            let price = _trade.price.to_f64();
            let volume = _trade.volume.to_f64();

            self.price_values.push(price);
            self.volume_values.push(volume);

            self.rolling_price.update(price);
            self.rolling_volume.update(volume);
        }
    }

    fn price_statistics(&self) -> PriceStatistics {
        #[cfg(feature = "stats")]
        {
            let percentiles = if !self.price_values.is_empty() {
                let mut tdigest = tdigests::TDigest::from_values(self.price_values.clone());
                tdigest.compress(100);

                [
                    ("P50", 0.5),
                    ("P75", 0.75),
                    ("P90", 0.9),
                    ("P95", 0.95),
                    ("P99", 0.99),
                ]
                .iter()
                .map(|(name, quantile)| (name.to_string(), tdigest.estimate_quantile(*quantile)))
                .collect()
            } else {
                HashMap::new()
            };

            let range = if !self.price_values.is_empty() {
                let min = self
                    .price_values
                    .iter()
                    .fold(f64::INFINITY, |a, &b| a.min(b));
                let max = self
                    .price_values
                    .iter()
                    .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
                (min, max)
            } else {
                (0.0, 0.0)
            };

            PriceStatistics {
                percentiles,
                rolling: RollingStats {
                    mean: self.rolling_price.mean,
                    variance: 0.0, // Not available in rolling-stats
                    std_dev: self.rolling_price.std_dev,
                    count: self.rolling_price.count as u64,
                },
                range,
            }
        }

        #[cfg(not(feature = "stats"))]
        {
            PriceStatistics {
                percentiles: HashMap::new(),
                rolling: RollingStats {
                    mean: 0.0,
                    variance: 0.0,
                    std_dev: 0.0,
                    count: self.count,
                },
                range: (0.0, 0.0),
            }
        }
    }

    fn volume_statistics(&self) -> VolumeStatistics {
        #[cfg(feature = "stats")]
        {
            let percentiles = if !self.volume_values.is_empty() {
                let mut tdigest = tdigests::TDigest::from_values(self.volume_values.clone());
                tdigest.compress(100);

                [
                    ("P50", 0.5),
                    ("P75", 0.75),
                    ("P90", 0.9),
                    ("P95", 0.95),
                    ("P99", 0.99),
                ]
                .iter()
                .map(|(name, quantile)| (name.to_string(), tdigest.estimate_quantile(*quantile)))
                .collect()
            } else {
                HashMap::new()
            };

            let range = if !self.volume_values.is_empty() {
                let min = self
                    .volume_values
                    .iter()
                    .fold(f64::INFINITY, |a, &b| a.min(b));
                let max = self
                    .volume_values
                    .iter()
                    .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
                (min, max)
            } else {
                (0.0, 0.0)
            };

            VolumeStatistics {
                percentiles,
                rolling: RollingStats {
                    mean: self.rolling_volume.mean,
                    variance: 0.0, // Not available in rolling-stats
                    std_dev: self.rolling_volume.std_dev,
                    count: self.rolling_volume.count as u64,
                },
                range,
            }
        }

        #[cfg(not(feature = "stats"))]
        {
            VolumeStatistics {
                percentiles: HashMap::new(),
                rolling: RollingStats {
                    mean: 0.0,
                    variance: 0.0,
                    std_dev: 0.0,
                    count: self.count,
                },
                range: (0.0, 0.0),
            }
        }
    }
}

impl BarStats {
    #[cfg(feature = "stats")]
    fn new(_config: &StreamingConfig) -> Self {
        // Issue #96 Memory v2: Fixed-size arrays instead of HashMap<String, _>
        Self {
            count: 0,
            ohlc_values: [Vec::new(), Vec::new(), Vec::new(), Vec::new()],
            rolling_ohlc: [
                rolling_stats::Stats::new(),
                rolling_stats::Stats::new(),
                rolling_stats::Stats::new(),
                rolling_stats::Stats::new(),
            ],
        }
    }

    #[cfg(not(feature = "stats"))]
    fn new(_config: &StreamingConfig) -> Self {
        Self { count: 0 }
    }

    fn update(&mut self, _bar: &OpenDeviationBar) {
        self.count += 1;

        #[cfg(feature = "stats")]
        {
            let values = [
                (OhlcField::Open, _bar.open.to_f64()),
                (OhlcField::High, _bar.high.to_f64()),
                (OhlcField::Low, _bar.low.to_f64()),
                (OhlcField::Close, _bar.close.to_f64()),
            ];

            for (field, value) in values {
                self.ohlc_values[field as usize].push(value);
                self.rolling_ohlc[field as usize].update(value);
            }
        }
    }

    fn ohlc_statistics(&self) -> OhlcStatistics {
        #[cfg(feature = "stats")]
        {
            let create_price_stats = |field: OhlcField| -> PriceStatistics {
                let values = &self.ohlc_values[field as usize];
                let rolling = &self.rolling_ohlc[field as usize];

                let percentiles = if !values.is_empty() {
                    let mut tdigest = tdigests::TDigest::from_values(values.clone());
                    tdigest.compress(100);

                    [
                        ("P50", 0.5),
                        ("P75", 0.75),
                        ("P90", 0.9),
                        ("P95", 0.95),
                        ("P99", 0.99),
                    ]
                    .iter()
                    .map(|(name, quantile)| {
                        (name.to_string(), tdigest.estimate_quantile(*quantile))
                    })
                    .collect()
                } else {
                    HashMap::new()
                };

                let range = if !values.is_empty() {
                    let min = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
                    let max = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
                    (min, max)
                } else {
                    (0.0, 0.0)
                };

                PriceStatistics {
                    percentiles,
                    rolling: RollingStats {
                        mean: rolling.mean,
                        variance: 0.0, // Not available in rolling-stats
                        std_dev: rolling.std_dev,
                        count: rolling.count as u64,
                    },
                    range,
                }
            };

            OhlcStatistics {
                open: create_price_stats(OhlcField::Open),
                high: create_price_stats(OhlcField::High),
                low: create_price_stats(OhlcField::Low),
                close: create_price_stats(OhlcField::Close),
            }
        }

        #[cfg(not(feature = "stats"))]
        {
            let empty_stats = PriceStatistics {
                percentiles: HashMap::new(),
                rolling: RollingStats {
                    mean: 0.0,
                    variance: 0.0,
                    std_dev: 0.0,
                    count: self.count,
                },
                range: (0.0, 0.0),
            };

            OhlcStatistics {
                open: empty_stats.clone(),
                high: empty_stats.clone(),
                low: empty_stats.clone(),
                close: empty_stats,
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use opendeviationbar_core::{DataSource, FixedPoint};

    #[test]
    fn test_streaming_stats_engine_creation() {
        let engine = StreamingStatsEngine::new();
        let snapshot = engine.snapshot();

        assert_eq!(snapshot.trade_count, 0);
        assert_eq!(snapshot.bar_count, 0);
    }

    #[test]
    fn test_trade_processing() {
        let mut engine = StreamingStatsEngine::new();

        let trade = AggTrade {
            agg_trade_id: 1,
            price: FixedPoint::from_str("50000.0").unwrap(),
            volume: FixedPoint::from_str("1.5").unwrap(),
            first_trade_id: 1,
            last_trade_id: 1,
            timestamp: 1609459200000,
            is_buyer_maker: false,
            is_best_match: None,
        };

        engine.process_trade(&trade);
        let snapshot = engine.snapshot();

        assert_eq!(snapshot.trade_count, 1);
    }

    #[test]
    fn test_bar_processing() {
        let mut engine = StreamingStatsEngine::new();

        let bar = OpenDeviationBar {
            open_time: 1609459200000,
            close_time: 1609459200000,
            open: FixedPoint::from_str("50000.0").unwrap(),
            high: FixedPoint::from_str("50100.0").unwrap(),
            low: FixedPoint::from_str("49900.0").unwrap(),
            close: FixedPoint::from_str("50050.0").unwrap(),
            // Issue #88: volume fields are i128
            volume: FixedPoint::from_str("10.5").unwrap().0 as i128,
            turnover: 0,
            individual_trade_count: 42,
            agg_record_count: 1,
            first_trade_id: 1,
            last_trade_id: 42,
            first_agg_trade_id: 0, // Issue #72
            last_agg_trade_id: 0,  // Issue #72
            data_source: DataSource::BinanceFuturesUM,
            buy_volume: FixedPoint::from_str("5.0").unwrap().0 as i128,
            buy_turnover: 0,
            sell_volume: FixedPoint::from_str("5.5").unwrap().0 as i128,
            sell_turnover: 0,
            buy_trade_count: 20,
            sell_trade_count: 22,
            vwap: FixedPoint::from_str("50025.0").unwrap(),
            // Microstructure features (Issue #25)
            duration_us: 0,
            ofi: 0.0,
            vwap_close_deviation: 0.0,
            price_impact: 0.0,
            kyle_lambda_proxy: 0.0,
            trade_intensity: 0.0,
            volume_per_trade: 0.0,
            aggression_ratio: 0.0,
            aggregation_density_f64: 0.0,
            turnover_imbalance: 0.0,
            // Inter-bar features (Issue #59)
            lookback_trade_count: None,
            lookback_ofi: None,
            lookback_duration_us: None,
            lookback_intensity: None,
            lookback_vwap_raw: None,
            lookback_vwap_position: None,
            lookback_count_imbalance: None,
            lookback_kyle_lambda: None,
            lookback_burstiness: None,
            lookback_volume_skew: None,
            lookback_volume_kurt: None,
            lookback_price_range: None,
            lookback_kaufman_er: None,
            lookback_garman_klass_vol: None,
            lookback_hurst: None,
            lookback_permutation_entropy: None,
            // Intra-bar features (Issue #59)
            intra_bull_epoch_density: None,
            intra_bear_epoch_density: None,
            intra_bull_excess_gain: None,
            intra_bear_excess_gain: None,
            intra_bull_cv: None,
            intra_bear_cv: None,
            intra_max_drawdown: None,
            intra_max_runup: None,
            intra_trade_count: None,
            intra_ofi: None,
            intra_duration_us: None,
            intra_intensity: None,
            intra_vwap_position: None,
            intra_count_imbalance: None,
            intra_kyle_lambda: None,
            intra_burstiness: None,
            intra_volume_skew: None,
            intra_volume_kurt: None,
            intra_kaufman_er: None,
            intra_garman_klass_vol: None,
            intra_hurst: None,
            intra_permutation_entropy: None,
        };

        engine.process_bar(&bar);
        let snapshot = engine.snapshot();

        assert_eq!(snapshot.bar_count, 1);
    }
}
