//! Bridge between ml model registry and common::ml_strategy::MLModelAdapter trait.
//!
//! EnsembleModelAdapter wraps a model ID and implements MLModelAdapter so it can
//! be injected into SharedMLStrategy. When real checkpoint loading is wired, this
//! adapter will delegate to the loaded model for inference.

use anyhow::Result;
use chrono::Utc;
use common::ml_strategy::{MLModelAdapter, MLPrediction, SharedMLStrategy};
use crate::features::production_adapter::ProductionFeatureExtractorAdapter;

/// Adapter that will delegate predict() to a loaded model checkpoint.
///
/// Currently returns neutral predictions with zero confidence (filtered out
/// by the confidence threshold) until checkpoint loading is production-ready.
#[derive(Debug)]
pub struct EnsembleModelAdapter {
    model_id: String,
}

impl EnsembleModelAdapter {
    pub fn new<S: Into<String>>(model_id: S) -> Self {
        Self {
            model_id: model_id.into(),
        }
    }
}

impl MLModelAdapter for EnsembleModelAdapter {
    fn predict(&self, features: &[f64]) -> Result<MLPrediction> {
        // TODO: Wire to real model inference via checkpoint loading when
        // the model registry is production-ready. For now, return neutral
        // prediction so the ensemble pipeline is fully wired end-to-end.
        let _ = features;
        Ok(MLPrediction {
            model_id: self.model_id.clone(),
            prediction_value: 0.5,
            confidence: 0.0, // Zero confidence = filtered out by threshold
            features: vec![],
            timestamp: Utc::now(),
            inference_latency_us: 0,
        })
    }

    fn model_id(&self) -> &str {
        &self.model_id
    }

    fn validate_prediction(&mut self, _prediction: &MLPrediction, _actual_outcome: bool) {
        // Performance tracking handled by SharedMLStrategy
    }
}

/// Build a production-ready SharedMLStrategy with model adapters for each
/// model in the 10-model ensemble.
///
/// Returns a strategy with:
/// - ProductionFeatureExtractorAdapter (42 market features currently)
/// - One EnsembleModelAdapter per known model type
///
/// When no checkpoints are loaded, models return neutral predictions with
/// zero confidence, which get filtered out by the confidence threshold.
pub fn build_production_strategy(min_confidence_threshold: f64) -> SharedMLStrategy {
    let extractor = Box::new(ProductionFeatureExtractorAdapter::new());

    let model_ids = [
        "dqn", "ppo", "tft", "mamba2", "tggn", "tlob", "liquid", "kan", "xlstm", "diffusion",
    ];
    let models: Vec<Box<dyn MLModelAdapter>> = model_ids
        .iter()
        .map(|&id| Box::new(EnsembleModelAdapter::new(id)) as Box<dyn MLModelAdapter>)
        .collect();

    SharedMLStrategy::new(extractor, models, min_confidence_threshold)
}

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

    #[test]
    fn test_ensemble_model_adapter_neutral_prediction() {
        let adapter = EnsembleModelAdapter::new("dqn");
        let features = vec![0.1; 51];
        let prediction = adapter.predict(&features).unwrap();

        assert_eq!(prediction.model_id, "dqn");
        assert_eq!(prediction.prediction_value, 0.5);
        assert_eq!(prediction.confidence, 0.0);
    }

    #[test]
    fn test_build_production_strategy() {
        let strategy = build_production_strategy(0.6);
        assert_eq!(strategy.min_confidence_threshold(), 0.6);
    }

    #[tokio::test]
    async fn test_production_strategy_filters_neutral_predictions() {
        let strategy = build_production_strategy(0.5);
        let predictions = strategy
            .get_ensemble_prediction(100.0, 1000.0, Utc::now())
            .await
            .unwrap();

        // All 10 adapters return confidence=0.0, so threshold=0.5 filters them all
        assert!(
            predictions.is_empty(),
            "Neutral predictions (confidence=0.0) should be filtered by threshold=0.5"
        );
    }

    #[tokio::test]
    async fn test_production_strategy_zero_threshold_keeps_all() {
        let strategy = build_production_strategy(0.0);
        let predictions = strategy
            .get_ensemble_prediction(100.0, 1000.0, Utc::now())
            .await
            .unwrap();

        // With threshold=0.0, all 10 neutral predictions pass through
        assert_eq!(
            predictions.len(),
            10,
            "Zero threshold should keep all 10 model predictions"
        );
    }
}
