//! ML-Based Anomaly Detection
//!
//! Detect price manipulation, wash trading, and abnormal patterns
//! using statistical and machine learning techniques.

use crate::SynorDecimal;
use chrono::{DateTime, Duration, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};

/// Types of anomalies that can be detected
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AnomalyType {
    /// Price moved outside expected range
    PriceOutlier,
    /// Sudden volume spike
    VolumeSpike,
    /// Suspected wash trading (self-trades)
    WashTrading,
    /// Coordinated trading pattern
    CoordinatedTrading,
    /// Pump and dump pattern
    PumpAndDump,
    /// Flash loan attack signature
    FlashLoanAttack,
    /// Abnormal order book imbalance
    OrderBookImbalance,
    /// Unusual trading frequency
    FrequencyAnomaly,
    /// Statistical outlier (general)
    StatisticalOutlier,
}

/// Detected anomaly with details
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Anomaly {
    /// Type of anomaly
    pub anomaly_type: AnomalyType,
    /// Affected trading pair
    pub pair: String,
    /// Detection timestamp
    pub detected_at: DateTime<Utc>,
    /// Severity score (0.0 - 1.0)
    pub severity: f64,
    /// Confidence in detection (0.0 - 1.0)
    pub confidence: f64,
    /// Detailed description
    pub description: String,
    /// Associated data points
    pub data: AnomalyData,
    /// Recommended action
    pub recommended_action: RecommendedAction,
}

/// Data associated with an anomaly
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyData {
    /// Current value
    pub current_value: SynorDecimal,
    /// Expected value
    pub expected_value: SynorDecimal,
    /// Standard deviations from mean
    pub z_score: f64,
    /// Relevant timestamps
    pub timestamps: Vec<DateTime<Utc>>,
    /// Additional context
    pub context: HashMap<String, String>,
}

/// Recommended action for detected anomaly
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RecommendedAction {
    /// No action needed
    Monitor,
    /// Alert administrators
    Alert,
    /// Temporarily pause trading
    PauseTrading,
    /// Trigger circuit breaker
    TriggerCircuitBreaker,
    /// Investigate manually
    Investigate,
    /// Block suspicious addresses
    BlockAddresses(Vec<String>),
}

/// Configuration for anomaly detection
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyDetectorConfig {
    /// Z-score threshold for outlier detection
    pub z_score_threshold: f64,
    /// Minimum data points for statistical analysis
    pub min_data_points: usize,
    /// Volume spike multiplier threshold
    pub volume_spike_multiplier: f64,
    /// Wash trading detection window (seconds)
    pub wash_trading_window: i64,
    /// Pump and dump detection window (minutes)
    pub pump_dump_window: i64,
    /// Flash loan detection window (seconds)
    pub flash_loan_window: i64,
    /// Enable ML-based detection (vs pure statistical)
    pub ml_enabled: bool,
    /// Learning rate for online updates
    pub learning_rate: f64,
}

impl Default for AnomalyDetectorConfig {
    fn default() -> Self {
        Self {
            z_score_threshold: 3.0,
            min_data_points: 30,
            volume_spike_multiplier: 5.0,
            wash_trading_window: 60,
            pump_dump_window: 60,
            flash_loan_window: 15,
            ml_enabled: true,
            learning_rate: 0.01,
        }
    }
}

/// Price/volume data point
#[derive(Debug, Clone)]
pub struct MarketDataPoint {
    pub price: SynorDecimal,
    pub volume: SynorDecimal,
    pub timestamp: DateTime<Utc>,
    pub bid_volume: Option<SynorDecimal>,
    pub ask_volume: Option<SynorDecimal>,
    pub trade_count: Option<u64>,
    pub addresses: Vec<String>, // Participating addresses
}

/// Running statistics for a time series
#[derive(Debug, Clone)]
struct RunningStats {
    count: usize,
    mean: f64,
    m2: f64, // For variance calculation
    min: f64,
    max: f64,
}

impl RunningStats {
    fn new() -> Self {
        Self {
            count: 0,
            mean: 0.0,
            m2: 0.0,
            min: f64::MAX,
            max: f64::MIN,
        }
    }

    /// Welford's online algorithm for mean and variance
    fn update(&mut self, value: f64) {
        self.count += 1;
        let delta = value - self.mean;
        self.mean += delta / self.count as f64;
        let delta2 = value - self.mean;
        self.m2 += delta * delta2;
        self.min = self.min.min(value);
        self.max = self.max.max(value);
    }

    fn variance(&self) -> f64 {
        if self.count < 2 {
            0.0
        } else {
            self.m2 / (self.count - 1) as f64
        }
    }

    fn std_dev(&self) -> f64 {
        self.variance().sqrt()
    }

    fn z_score(&self, value: f64) -> f64 {
        let std = self.std_dev();
        if std < f64::EPSILON {
            0.0
        } else {
            (value - self.mean) / std
        }
    }
}

/// Isolation Forest for anomaly detection
#[derive(Debug, Clone)]
struct IsolationTree {
    split_feature: usize,
    split_value: f64,
    left: Option<Box<IsolationTree>>,
    right: Option<Box<IsolationTree>>,
    size: usize,
}

impl IsolationTree {
    /// Build tree from data
    fn build(data: &[Vec<f64>], max_depth: usize, current_depth: usize) -> Option<Self> {
        if data.is_empty() || current_depth >= max_depth || data.len() <= 1 {
            return None;
        }

        let n_features = data[0].len();
        if n_features == 0 {
            return None;
        }

        // Random feature selection (deterministic for reproducibility)
        let feature = (current_depth * 7 + data.len()) % n_features;

        // Find min/max for this feature
        let values: Vec<f64> = data.iter().map(|row| row[feature]).collect();
        let min_val = values.iter().copied().fold(f64::MAX, f64::min);
        let max_val = values.iter().copied().fold(f64::MIN, f64::max);

        if (max_val - min_val).abs() < f64::EPSILON {
            return None;
        }

        // Random split point
        let split = min_val + (max_val - min_val) * 0.5;

        let (left_data, right_data): (Vec<_>, Vec<_>) = data.iter()
            .cloned()
            .partition(|row| row[feature] < split);

        Some(Self {
            split_feature: feature,
            split_value: split,
            left: Self::build(&left_data, max_depth, current_depth + 1).map(Box::new),
            right: Self::build(&right_data, max_depth, current_depth + 1).map(Box::new),
            size: data.len(),
        })
    }

    /// Get path length for a point
    fn path_length(&self, point: &[f64], current_length: f64) -> f64 {
        if self.left.is_none() && self.right.is_none() {
            return current_length + c_factor(self.size);
        }

        if point[self.split_feature] < self.split_value {
            if let Some(ref left) = self.left {
                return left.path_length(point, current_length + 1.0);
            }
        } else if let Some(ref right) = self.right {
            return right.path_length(point, current_length + 1.0);
        }

        current_length + c_factor(self.size)
    }
}

/// Expected path length for random BST
fn c_factor(n: usize) -> f64 {
    if n <= 1 {
        0.0
    } else if n == 2 {
        1.0
    } else {
        let n = n as f64;
        2.0 * ((n - 1.0).ln() + 0.5772156649) - (2.0 * (n - 1.0) / n)
    }
}

/// Isolation Forest ensemble
struct IsolationForest {
    trees: Vec<IsolationTree>,
    sample_size: usize,
}

impl IsolationForest {
    fn new(data: &[Vec<f64>], n_trees: usize, sample_size: usize) -> Self {
        let max_depth = (sample_size as f64).log2().ceil() as usize;

        let trees: Vec<_> = (0..n_trees)
            .filter_map(|i| {
                // Subsample with deterministic "randomness" based on tree index
                let sample: Vec<_> = data.iter()
                    .enumerate()
                    .filter(|(j, _)| (i + j) % 3 != 0)
                    .take(sample_size)
                    .map(|(_, row)| row.clone())
                    .collect();

                IsolationTree::build(&sample, max_depth, 0)
            })
            .collect();

        Self { trees, sample_size }
    }

    fn anomaly_score(&self, point: &[f64]) -> f64 {
        if self.trees.is_empty() {
            return 0.5;
        }

        let avg_path: f64 = self.trees.iter()
            .map(|tree| tree.path_length(point, 0.0))
            .sum::<f64>() / self.trees.len() as f64;

        let c = c_factor(self.sample_size);
        if c < f64::EPSILON {
            return 0.5;
        }

        2.0_f64.powf(-avg_path / c)
    }
}

/// Per-pair anomaly detection state
struct PairDetector {
    /// Price history
    price_history: VecDeque<MarketDataPoint>,
    /// Price statistics
    price_stats: RunningStats,
    /// Volume statistics
    volume_stats: RunningStats,
    /// Return statistics (price changes)
    return_stats: RunningStats,
    /// Recent trade addresses for wash trading detection
    recent_addresses: VecDeque<(DateTime<Utc>, Vec<String>)>,
    /// Detected anomalies
    anomalies: Vec<Anomaly>,
    /// Isolation forest (rebuilt periodically)
    isolation_forest: Option<IsolationForest>,
    /// Last model rebuild time
    last_rebuild: DateTime<Utc>,
}

impl PairDetector {
    fn new() -> Self {
        Self {
            price_history: VecDeque::with_capacity(10000),
            price_stats: RunningStats::new(),
            volume_stats: RunningStats::new(),
            return_stats: RunningStats::new(),
            recent_addresses: VecDeque::with_capacity(1000),
            anomalies: Vec::new(),
            isolation_forest: None,
            last_rebuild: Utc::now() - Duration::hours(24), // Force initial build
        }
    }

    fn add_data_point(&mut self, point: MarketDataPoint) {
        // Update return stats
        if let Some(prev) = self.price_history.back() {
            let price_f64 = point.price.to_string().parse::<f64>().unwrap_or(0.0);
            let prev_f64 = prev.price.to_string().parse::<f64>().unwrap_or(0.0);
            if prev_f64 > 0.0 {
                let return_pct = (price_f64 - prev_f64) / prev_f64 * 100.0;
                self.return_stats.update(return_pct);
            }
        }

        // Update statistics
        let price_f64 = point.price.to_string().parse::<f64>().unwrap_or(0.0);
        let volume_f64 = point.volume.to_string().parse::<f64>().unwrap_or(0.0);
        self.price_stats.update(price_f64);
        self.volume_stats.update(volume_f64);

        // Track addresses
        if !point.addresses.is_empty() {
            self.recent_addresses.push_back((point.timestamp, point.addresses.clone()));
        }

        self.price_history.push_back(point);

        // Cleanup old data
        let cutoff = Utc::now() - Duration::hours(24);
        while self.price_history.front().map(|p| p.timestamp < cutoff).unwrap_or(false) {
            self.price_history.pop_front();
        }
        while self.recent_addresses.front().map(|(t, _)| *t < cutoff).unwrap_or(false) {
            self.recent_addresses.pop_front();
        }
    }

    fn rebuild_model(&mut self) {
        if self.price_history.len() < 100 {
            return;
        }

        // Build feature vectors: [price, volume, return, bid/ask ratio]
        let data: Vec<Vec<f64>> = self.price_history.iter()
            .skip(1)
            .zip(self.price_history.iter())
            .map(|(curr, prev)| {
                let price = curr.price.to_string().parse::<f64>().unwrap_or(0.0);
                let volume = curr.volume.to_string().parse::<f64>().unwrap_or(0.0);
                let prev_price = prev.price.to_string().parse::<f64>().unwrap_or(1.0);
                let return_pct = if prev_price > 0.0 {
                    (price - prev_price) / prev_price * 100.0
                } else {
                    0.0
                };

                let bid_ask_ratio = match (curr.bid_volume, curr.ask_volume) {
                    (Some(bid), Some(ask)) => {
                        let bid_f = bid.to_string().parse::<f64>().unwrap_or(0.0);
                        let ask_f = ask.to_string().parse::<f64>().unwrap_or(1.0);
                        if ask_f > 0.0 { bid_f / ask_f } else { 1.0 }
                    }
                    _ => 1.0,
                };

                vec![price, volume, return_pct, bid_ask_ratio]
            })
            .collect();

        self.isolation_forest = Some(IsolationForest::new(&data, 100, 256.min(data.len())));
        self.last_rebuild = Utc::now();
    }
}

/// Main anomaly detector
pub struct AnomalyDetector {
    config: AnomalyDetectorConfig,
    detectors: HashMap<String, PairDetector>,
}

impl AnomalyDetector {
    pub fn new() -> Self {
        Self::with_config(AnomalyDetectorConfig::default())
    }

    pub fn with_config(config: AnomalyDetectorConfig) -> Self {
        Self {
            config,
            detectors: HashMap::new(),
        }
    }

    /// Process a new market data point and detect anomalies
    pub fn process(&mut self, pair: &str, data: MarketDataPoint) -> Vec<Anomaly> {
        // Ensure detector exists
        if !self.detectors.contains_key(pair) {
            self.detectors.insert(pair.to_string(), PairDetector::new());
        }

        // Rebuild model periodically
        let ml_enabled = self.config.ml_enabled;
        if let Some(detector) = self.detectors.get_mut(pair) {
            if (Utc::now() - detector.last_rebuild).num_hours() >= 1 && ml_enabled {
                detector.rebuild_model();
            }
        }

        // Copy config values we need
        let min_data_points = self.config.min_data_points;
        let z_score_threshold = self.config.z_score_threshold;
        let volume_spike_multiplier = self.config.volume_spike_multiplier;
        let wash_trading_window = self.config.wash_trading_window;
        let pump_dump_window = self.config.pump_dump_window;
        let flash_loan_window = self.config.flash_loan_window;

        let mut anomalies = Vec::new();

        // Run all detectors using the immutable reference first
        if let Some(detector) = self.detectors.get(pair) {
            if let Some(a) = Self::detect_price_outlier_impl(pair, &data, detector, min_data_points, z_score_threshold) {
                anomalies.push(a);
            }
            if let Some(a) = Self::detect_volume_spike_impl(pair, &data, detector, min_data_points, volume_spike_multiplier) {
                anomalies.push(a);
            }
            if let Some(a) = Self::detect_wash_trading_impl(pair, &data, detector, wash_trading_window) {
                anomalies.push(a);
            }
            if let Some(a) = Self::detect_pump_dump_impl(pair, detector, pump_dump_window) {
                anomalies.push(a);
            }
            if let Some(a) = Self::detect_flash_loan_impl(pair, &data, detector, flash_loan_window) {
                anomalies.push(a);
            }
            if ml_enabled {
                if let Some(a) = Self::detect_ml_anomaly_impl(pair, &data, detector) {
                    anomalies.push(a);
                }
            }
        }

        // Now mutably access detector to add data and anomalies
        if let Some(detector) = self.detectors.get_mut(pair) {
            detector.add_data_point(data);
            detector.anomalies.extend(anomalies.clone());
        }

        anomalies
    }

    fn detect_price_outlier_impl(pair: &str, data: &MarketDataPoint, detector: &PairDetector, min_data_points: usize, z_score_threshold: f64) -> Option<Anomaly> {
        if detector.price_stats.count < min_data_points {
            return None;
        }

        let price_f64 = data.price.to_string().parse::<f64>().ok()?;
        let z_score = detector.price_stats.z_score(price_f64);

        if z_score.abs() > z_score_threshold {
            let expected = Decimal::from_f64_retain(detector.price_stats.mean)?;
            Some(Anomaly {
                anomaly_type: AnomalyType::PriceOutlier,
                pair: pair.to_string(),
                detected_at: Utc::now(),
                severity: (z_score.abs() / 10.0).min(1.0),
                confidence: 0.8,
                description: format!(
                    "Price {} is {:.2} standard deviations from mean {:.4}",
                    data.price, z_score, detector.price_stats.mean
                ),
                data: AnomalyData {
                    current_value: data.price,
                    expected_value: expected,
                    z_score,
                    timestamps: vec![data.timestamp],
                    context: HashMap::new(),
                },
                recommended_action: if z_score.abs() > 5.0 {
                    RecommendedAction::TriggerCircuitBreaker
                } else {
                    RecommendedAction::Alert
                },
            })
        } else {
            None
        }
    }

    fn detect_volume_spike_impl(pair: &str, data: &MarketDataPoint, detector: &PairDetector, min_data_points: usize, volume_spike_multiplier: f64) -> Option<Anomaly> {
        if detector.volume_stats.count < min_data_points {
            return None;
        }

        let volume_f64 = data.volume.to_string().parse::<f64>().ok()?;
        let mean = detector.volume_stats.mean;

        if volume_f64 > mean * volume_spike_multiplier {
            let expected = Decimal::from_f64_retain(mean)?;
            let z_score = detector.volume_stats.z_score(volume_f64);
            Some(Anomaly {
                anomaly_type: AnomalyType::VolumeSpike,
                pair: pair.to_string(),
                detected_at: Utc::now(),
                severity: ((volume_f64 / mean) / 20.0).min(1.0),
                confidence: 0.75,
                description: format!(
                    "Volume {} is {:.1}x the average {:.2}",
                    data.volume, volume_f64 / mean, mean
                ),
                data: AnomalyData {
                    current_value: data.volume,
                    expected_value: expected,
                    z_score,
                    timestamps: vec![data.timestamp],
                    context: HashMap::new(),
                },
                recommended_action: RecommendedAction::Investigate,
            })
        } else {
            None
        }
    }

    fn detect_wash_trading_impl(pair: &str, data: &MarketDataPoint, detector: &PairDetector, wash_trading_window: i64) -> Option<Anomaly> {
        if data.addresses.is_empty() {
            return None;
        }

        let window_start = Utc::now() - Duration::seconds(wash_trading_window);

        // Count address appearances in recent window
        let mut address_counts: HashMap<&str, usize> = HashMap::new();
        for (ts, addrs) in &detector.recent_addresses {
            if *ts >= window_start {
                for addr in addrs {
                    *address_counts.entry(addr.as_str()).or_insert(0) += 1;
                }
            }
        }

        // Check current trade addresses
        for addr in &data.addresses {
            if let Some(&count) = address_counts.get(addr.as_str()) {
                if count >= 3 {
                    let mut context = HashMap::new();
                    context.insert("address".to_string(), addr.clone());
                    context.insert("trade_count".to_string(), count.to_string());

                    return Some(Anomaly {
                        anomaly_type: AnomalyType::WashTrading,
                        pair: pair.to_string(),
                        detected_at: Utc::now(),
                        severity: ((count as f64) / 10.0).min(1.0),
                        confidence: 0.6,
                        description: format!(
                            "Address {} appears in {} trades within {}s window",
                            addr, count, wash_trading_window
                        ),
                        data: AnomalyData {
                            current_value: data.volume,
                            expected_value: Decimal::ZERO,
                            z_score: 0.0,
                            timestamps: vec![data.timestamp],
                            context,
                        },
                        recommended_action: RecommendedAction::BlockAddresses(vec![addr.clone()]),
                    });
                }
            }
        }

        None
    }

    fn detect_pump_dump_impl(pair: &str, detector: &PairDetector, pump_dump_window: i64) -> Option<Anomaly> {
        // Need enough history
        if detector.price_history.len() < 10 {
            return None;
        }

        let window_start = Utc::now() - Duration::minutes(pump_dump_window);
        let prices: Vec<_> = detector.price_history.iter()
            .filter(|p| p.timestamp >= window_start)
            .map(|p| p.price.to_string().parse::<f64>().unwrap_or(0.0))
            .collect();

        if prices.len() < 5 {
            return None;
        }

        // Find max and check for reversal
        let max_price = prices.iter().copied().fold(f64::MIN, f64::max);
        let max_idx = prices.iter().position(|&p| (p - max_price).abs() < f64::EPSILON)?;
        let first_price = prices.first()?;
        let last_price = prices.last()?;

        // Pump: >30% rise, Dump: >20% drop from peak
        let pump_pct = (max_price - first_price) / first_price * 100.0;
        let dump_pct = (max_price - last_price) / max_price * 100.0;

        if pump_pct > 30.0 && dump_pct > 20.0 && max_idx < prices.len() - 2 {
            let mut context = HashMap::new();
            context.insert("pump_percent".to_string(), format!("{:.1}", pump_pct));
            context.insert("dump_percent".to_string(), format!("{:.1}", dump_pct));

            return Some(Anomaly {
                anomaly_type: AnomalyType::PumpAndDump,
                pair: pair.to_string(),
                detected_at: Utc::now(),
                severity: ((pump_pct + dump_pct) / 100.0).min(1.0),
                confidence: 0.7,
                description: format!(
                    "Potential pump and dump: +{:.1}% pump, -{:.1}% dump",
                    pump_pct, dump_pct
                ),
                data: AnomalyData {
                    current_value: Decimal::from_f64_retain(*last_price)?,
                    expected_value: Decimal::from_f64_retain(*first_price)?,
                    z_score: pump_pct,
                    timestamps: vec![Utc::now()],
                    context,
                },
                recommended_action: RecommendedAction::PauseTrading,
            });
        }

        None
    }

    fn detect_flash_loan_impl(pair: &str, data: &MarketDataPoint, detector: &PairDetector, flash_loan_window: i64) -> Option<Anomaly> {
        // Flash loan signature: huge volume spike + quick price movement + reversal
        let window_start = Utc::now() - Duration::seconds(flash_loan_window);
        let recent: Vec<_> = detector.price_history.iter()
            .filter(|p| p.timestamp >= window_start)
            .collect();

        if recent.len() < 3 {
            return None;
        }

        let volumes: Vec<f64> = recent.iter()
            .map(|p| p.volume.to_string().parse::<f64>().unwrap_or(0.0))
            .collect();

        let prices: Vec<f64> = recent.iter()
            .map(|p| p.price.to_string().parse::<f64>().unwrap_or(0.0))
            .collect();

        let avg_volume = detector.volume_stats.mean;
        let max_volume = volumes.iter().copied().fold(f64::MIN, f64::max);

        // Volume spike > 20x average
        if max_volume > avg_volume * 20.0 && !prices.is_empty() {
            let first = prices.first()?;
            let last = prices.last()?;
            let max_price = prices.iter().copied().fold(f64::MIN, f64::max);

            let spike = (max_price - first) / first * 100.0;
            let reversal = (max_price - last) / max_price * 100.0;

            // Big spike and quick reversal
            if spike > 10.0 && reversal > 8.0 {
                let mut context = HashMap::new();
                context.insert("volume_spike".to_string(), format!("{:.0}x", max_volume / avg_volume));
                context.insert("price_spike".to_string(), format!("{:.1}%", spike));

                return Some(Anomaly {
                    anomaly_type: AnomalyType::FlashLoanAttack,
                    pair: pair.to_string(),
                    detected_at: Utc::now(),
                    severity: 0.9,
                    confidence: 0.65,
                    description: format!(
                        "Suspected flash loan attack: {}x volume, {:.1}% price spike",
                        max_volume / avg_volume, spike
                    ),
                    data: AnomalyData {
                        current_value: data.volume,
                        expected_value: Decimal::from_f64_retain(avg_volume)?,
                        z_score: (max_volume - avg_volume) / detector.volume_stats.std_dev(),
                        timestamps: recent.iter().map(|p| p.timestamp).collect(),
                        context,
                    },
                    recommended_action: RecommendedAction::TriggerCircuitBreaker,
                });
            }
        }

        None
    }

    fn detect_ml_anomaly_impl(pair: &str, data: &MarketDataPoint, detector: &PairDetector) -> Option<Anomaly> {
        let forest = detector.isolation_forest.as_ref()?;

        if detector.price_history.is_empty() {
            return None;
        }

        let prev = detector.price_history.back()?;
        let price = data.price.to_string().parse::<f64>().ok()?;
        let volume = data.volume.to_string().parse::<f64>().ok()?;
        let prev_price = prev.price.to_string().parse::<f64>().ok()?;

        let return_pct = if prev_price > 0.0 {
            (price - prev_price) / prev_price * 100.0
        } else {
            0.0
        };

        let bid_ask_ratio = match (data.bid_volume, data.ask_volume) {
            (Some(bid), Some(ask)) => {
                let bid_f = bid.to_string().parse::<f64>().ok()?;
                let ask_f = ask.to_string().parse::<f64>().ok()?;
                if ask_f > 0.0 { bid_f / ask_f } else { 1.0 }
            }
            _ => 1.0,
        };

        let point = vec![price, volume, return_pct, bid_ask_ratio];
        let score = forest.anomaly_score(&point);

        if score > 0.7 {
            let mut context = HashMap::new();
            context.insert("anomaly_score".to_string(), format!("{:.3}", score));

            Some(Anomaly {
                anomaly_type: AnomalyType::StatisticalOutlier,
                pair: pair.to_string(),
                detected_at: Utc::now(),
                severity: score,
                confidence: 0.6,
                description: format!(
                    "ML model detected anomaly with score {:.3}",
                    score
                ),
                data: AnomalyData {
                    current_value: data.price,
                    expected_value: Decimal::from_f64_retain(detector.price_stats.mean)?,
                    z_score: detector.price_stats.z_score(price),
                    timestamps: vec![data.timestamp],
                    context,
                },
                recommended_action: if score > 0.85 {
                    RecommendedAction::Alert
                } else {
                    RecommendedAction::Monitor
                },
            })
        } else {
            None
        }
    }

    /// Get recent anomalies for a pair
    pub fn get_anomalies(&self, pair: &str) -> Vec<Anomaly> {
        self.detectors.get(pair)
            .map(|d| d.anomalies.clone())
            .unwrap_or_default()
    }

    /// Get anomaly statistics
    pub fn get_stats(&self, pair: &str) -> Option<AnomalyStats> {
        let detector = self.detectors.get(pair)?;

        let by_type: HashMap<AnomalyType, usize> = detector.anomalies.iter()
            .fold(HashMap::new(), |mut acc, a| {
                *acc.entry(a.anomaly_type.clone()).or_insert(0) += 1;
                acc
            });

        Some(AnomalyStats {
            total_anomalies: detector.anomalies.len(),
            by_type,
            data_points: detector.price_history.len(),
            avg_severity: if detector.anomalies.is_empty() {
                0.0
            } else {
                detector.anomalies.iter().map(|a| a.severity).sum::<f64>()
                    / detector.anomalies.len() as f64
            },
        })
    }

    /// Clear old anomalies
    pub fn cleanup(&mut self, max_age: Duration) {
        let cutoff = Utc::now() - max_age;
        for detector in self.detectors.values_mut() {
            detector.anomalies.retain(|a| a.detected_at >= cutoff);
        }
    }
}

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

/// Anomaly detection statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyStats {
    pub total_anomalies: usize,
    pub by_type: HashMap<AnomalyType, usize>,
    pub data_points: usize,
    pub avg_severity: f64,
}

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

    #[test]
    fn test_running_stats() {
        let mut stats = RunningStats::new();
        for i in 1..=10 {
            stats.update(i as f64);
        }

        assert_eq!(stats.count, 10);
        assert!((stats.mean - 5.5).abs() < 0.001);
        assert!(stats.std_dev() > 0.0);
    }

    #[test]
    fn test_z_score() {
        let mut stats = RunningStats::new();
        // Add values with some natural variance (e.g., 98-102 range)
        for i in 0..100 {
            let value = 100.0 + ((i % 5) as f64) - 2.0; // Values: 98, 99, 100, 101, 102
            stats.update(value);
        }

        // Mean should be ~100, std dev ~1.4
        // z_score(150) should be (150 - 100) / ~1.4 = ~35
        let z = stats.z_score(150.0);
        assert!(z.abs() > 3.0); // Outlier
    }

    #[test]
    fn test_price_outlier_detection() {
        let mut detector = AnomalyDetector::new();

        // Add normal prices
        for i in 0..50 {
            let data = MarketDataPoint {
                price: dec!(100) + Decimal::from(i % 3),
                volume: dec!(1000),
                timestamp: Utc::now() - Duration::minutes(50 - i),
                bid_volume: None,
                ask_volume: None,
                trade_count: None,
                addresses: vec![],
            };
            detector.process("SYNOR/USD", data);
        }

        // Add outlier
        let outlier = MarketDataPoint {
            price: dec!(200), // Way above normal
            volume: dec!(1000),
            timestamp: Utc::now(),
            bid_volume: None,
            ask_volume: None,
            trade_count: None,
            addresses: vec![],
        };

        let anomalies = detector.process("SYNOR/USD", outlier);
        assert!(!anomalies.is_empty());
        assert!(anomalies.iter().any(|a| a.anomaly_type == AnomalyType::PriceOutlier));
    }

    #[test]
    fn test_volume_spike_detection() {
        let mut detector = AnomalyDetector::new();

        // Add normal volumes
        for i in 0..50 {
            let data = MarketDataPoint {
                price: dec!(100),
                volume: dec!(1000),
                timestamp: Utc::now() - Duration::minutes(50 - i),
                bid_volume: None,
                ask_volume: None,
                trade_count: None,
                addresses: vec![],
            };
            detector.process("SYNOR/USD", data);
        }

        // Add volume spike
        let spike = MarketDataPoint {
            price: dec!(100),
            volume: dec!(10000), // 10x normal
            timestamp: Utc::now(),
            bid_volume: None,
            ask_volume: None,
            trade_count: None,
            addresses: vec![],
        };

        let anomalies = detector.process("SYNOR/USD", spike);
        assert!(anomalies.iter().any(|a| a.anomaly_type == AnomalyType::VolumeSpike));
    }
}
