//! Bridge between `ml::ensemble::ModelInferenceAdapter` (per-model GPU inference)
//! and `common::ml_strategy::MLModelAdapter` (trait consumed by `SharedMLStrategy`).
//!
//! `EnsembleModelAdapter` is a thin adapter: it holds a polymorphic
//! `Arc<dyn ModelInferenceAdapter>` (the real, GPU-resident per-model adapter —
//! `DqnInferenceAdapter`, `PpoInferenceAdapter`, `TftInferenceAdapter`, ...) and
//! forwards `predict` to it.  The inner adapter already runs a full forward pass
//! on its wrapped model and returns a normalized `(direction, confidence)` pair;
//! this bridge remaps `direction ∈ [-1, 1]` to `prediction_value ∈ [0, 1]` so the
//! result fits `MLPrediction`'s bullish-probability contract (values > 0.5 =
//! bullish, < 0.5 = bearish; consumed by `SharedMLStrategy::validate_predictions`).

use std::sync::Arc;

use anyhow::{anyhow, Result};
use chrono::Utc;
use common::ml_strategy::{MLModelAdapter, MLPrediction, SharedMLStrategy};

use crate::ensemble::inference_adapter::{FeatureVector, ModelInferenceAdapter};
use crate::features::production_adapter::ProductionFeatureExtractorAdapter;

/// Adapter that forwards `MLModelAdapter::predict` to a wrapped
/// `ModelInferenceAdapter` running real GPU inference on a specific model
/// (DQN, PPO, TFT, Mamba2, Liquid, KAN, xLSTM, TGGN, TLOB, Diffusion, ...).
///
/// The wrapped adapter is held behind `Arc<dyn ModelInferenceAdapter>`, giving
/// `EnsembleModelAdapter` polymorphic dispatch across every model type that
/// implements the inference trait — the bridge itself knows nothing about
/// individual model architectures.
pub struct EnsembleModelAdapter {
    model_id: String,
    inner: Arc<dyn ModelInferenceAdapter>,
}

impl std::fmt::Debug for EnsembleModelAdapter {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("EnsembleModelAdapter")
            .field("model_id", &self.model_id)
            .field("inner_model_name", &self.inner.model_name())
            .field("inner_ready", &self.inner.is_ready())
            .finish()
    }
}

impl EnsembleModelAdapter {
    /// Create a bridge wrapping a real per-model inference adapter.
    ///
    /// `model_id` is the public ensemble identifier exposed via
    /// `MLModelAdapter::model_id` (e.g. "dqn", "ppo", "tft"); `inner` is the
    /// actual GPU-resident adapter that will produce predictions.
    pub fn new<S: Into<String>>(model_id: S, inner: Arc<dyn ModelInferenceAdapter>) -> Self {
        Self {
            model_id: model_id.into(),
            inner,
        }
    }
}

impl MLModelAdapter for EnsembleModelAdapter {
    fn predict(&self, features: &[f64]) -> Result<MLPrediction> {
        if !self.inner.is_ready() {
            return Err(anyhow!(
                "EnsembleModelAdapter: inner model '{}' (id={}) is not ready for inference",
                self.inner.model_name(),
                self.model_id
            ));
        }

        if features.is_empty() {
            return Err(anyhow!(
                "EnsembleModelAdapter: empty feature vector for model '{}'",
                self.model_id
            ));
        }

        let fv = FeatureVector {
            values: features.to_vec(),
            timestamp: Utc::now().timestamp_micros(),
        };

        let raw = self.inner.predict(&fv).map_err(|e| {
            anyhow!(
                "EnsembleModelAdapter: inference failed for model '{}': {e}",
                self.model_id
            )
        })?;

        if !raw.direction.is_finite() || !raw.confidence.is_finite() {
            return Err(anyhow!(
                "EnsembleModelAdapter: non-finite prediction from model '{}' (direction={}, confidence={})",
                self.model_id,
                raw.direction,
                raw.confidence,
            ));
        }

        // Map direction ∈ [-1, 1] -> prediction_value ∈ [0, 1]:
        //   direction = -1  -> 0.0  (strong bearish)
        //   direction =  0  -> 0.5  (neutral)
        //   direction = +1  -> 1.0  (strong bullish)
        // This matches SharedMLStrategy's bullish-probability contract
        // (predicted_positive = prediction_value > 0.5).
        let prediction_value = ((raw.direction + 1.0) * 0.5).clamp(0.0, 1.0);
        let confidence = raw.confidence.clamp(0.0, 1.0);

        Ok(MLPrediction {
            model_id: self.model_id.clone(),
            prediction_value,
            confidence,
            features: features.to_vec(),
            timestamp: Utc::now(),
            inference_latency_us: raw.metadata.latency_us,
        })
    }

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

    fn validate_prediction(&mut self, _prediction: &MLPrediction, _actual_outcome: bool) {
        // Performance tracking is handled by SharedMLStrategy.
        // The wrapped ModelInferenceAdapter trait does not expose a mutable
        // validation hook; model-side performance feedback, when required,
        // is driven by the training pipeline, not the ensemble read path.
    }
}

/// Build a production `SharedMLStrategy` using the 225-feature extractor and
/// one `EnsembleModelAdapter` per injected inference adapter.
///
/// The caller owns model construction (checkpoint loading, device selection,
/// config) and passes the concrete `ModelInferenceAdapter` implementations —
/// `DqnInferenceAdapter::from_checkpoint(...)`, etc. — paired with the public
/// ensemble id each model should advertise.
///
/// Passing an empty `adapters` vec produces a strategy with zero models; that
/// is an honest reflection of "no models are loaded", not a stub —
/// `SharedMLStrategy::get_ensemble_prediction` will return an empty prediction
/// list rather than fabricate neutral outputs.
pub fn build_production_strategy(
    min_confidence_threshold: f64,
    adapters: Vec<(String, Arc<dyn ModelInferenceAdapter>)>,
) -> SharedMLStrategy {
    let extractor = Box::new(ProductionFeatureExtractorAdapter::new());

    let models: Vec<Box<dyn MLModelAdapter>> = adapters
        .into_iter()
        .map(|(id, inner)| {
            Box::new(EnsembleModelAdapter::new(id, inner)) as Box<dyn MLModelAdapter>
        })
        .collect();

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ensemble::inference_adapter::{EnsemblePrediction, PredictionMeta};
    use crate::MLResult;

    /// Minimal non-GPU `ModelInferenceAdapter` used to exercise the bridge
    /// semantics deterministically without spinning up CUDA.
    struct StubInferenceAdapter {
        name: String,
        direction: f64,
        confidence: f64,
        ready: bool,
        should_fail: bool,
    }

    impl StubInferenceAdapter {
        fn bullish(name: &str) -> Self {
            Self {
                name: name.to_string(),
                direction: 0.6,
                confidence: 0.8,
                ready: true,
                should_fail: false,
            }
        }

        fn bearish(name: &str) -> Self {
            Self {
                name: name.to_string(),
                direction: -0.4,
                confidence: 0.7,
                ready: true,
                should_fail: false,
            }
        }

        fn not_ready(name: &str) -> Self {
            Self {
                name: name.to_string(),
                direction: 0.0,
                confidence: 0.0,
                ready: false,
                should_fail: false,
            }
        }

        fn failing(name: &str) -> Self {
            Self {
                name: name.to_string(),
                direction: 0.0,
                confidence: 0.0,
                ready: true,
                should_fail: true,
            }
        }

        fn non_finite(name: &str) -> Self {
            Self {
                name: name.to_string(),
                direction: f64::NAN,
                confidence: 0.5,
                ready: true,
                should_fail: false,
            }
        }
    }

    impl ModelInferenceAdapter for StubInferenceAdapter {
        fn model_name(&self) -> &str {
            &self.name
        }

        fn predict(&self, _features: &FeatureVector) -> MLResult<EnsemblePrediction> {
            if self.should_fail {
                return Err(crate::MLError::InferenceError(
                    "stub forced failure".to_owned(),
                ));
            }
            Ok(EnsemblePrediction {
                model_name: self.name.clone(),
                direction: self.direction,
                confidence: self.confidence,
                metadata: PredictionMeta {
                    latency_us: 42,
                    quantiles: None,
                    attention_weights: None,
                    q_values: None,
                },
            })
        }

        fn is_ready(&self) -> bool {
            self.ready
        }
    }

    #[test]
    fn predict_forwards_bullish_inner_prediction() {
        let inner: Arc<dyn ModelInferenceAdapter> =
            Arc::new(StubInferenceAdapter::bullish("DQN"));
        let adapter = EnsembleModelAdapter::new("dqn", inner);
        let features = vec![0.1_f64; 42];

        let pred = adapter.predict(&features).expect("predict should succeed");

        assert_eq!(pred.model_id, "dqn");
        // direction=0.6 -> prediction_value=(0.6+1)/2 = 0.8
        assert!((pred.prediction_value - 0.8).abs() < 1e-9);
        assert!((pred.confidence - 0.8).abs() < 1e-9);
        assert!(pred.prediction_value > 0.5, "bullish inner -> >0.5 bridge output");
        assert_eq!(pred.inference_latency_us, 42);
    }

    #[test]
    fn predict_forwards_bearish_inner_prediction() {
        let inner: Arc<dyn ModelInferenceAdapter> =
            Arc::new(StubInferenceAdapter::bearish("PPO"));
        let adapter = EnsembleModelAdapter::new("ppo", inner);
        let features = vec![0.1_f64; 42];

        let pred = adapter.predict(&features).expect("predict should succeed");

        // direction=-0.4 -> prediction_value=(-0.4+1)/2 = 0.3
        assert!((pred.prediction_value - 0.3).abs() < 1e-9);
        assert!(pred.prediction_value < 0.5, "bearish inner -> <0.5 bridge output");
    }

    #[test]
    fn predict_returns_err_when_inner_not_ready() {
        let inner: Arc<dyn ModelInferenceAdapter> =
            Arc::new(StubInferenceAdapter::not_ready("TFT"));
        let adapter = EnsembleModelAdapter::new("tft", inner);
        let features = vec![0.1_f64; 42];

        let err = adapter
            .predict(&features)
            .expect_err("not-ready model must return Err, not confidence=0 success");
        let msg = err.to_string();
        assert!(msg.contains("not ready"), "err message should mention readiness: {msg}");
    }

    #[test]
    fn predict_returns_err_when_inner_fails() {
        let inner: Arc<dyn ModelInferenceAdapter> =
            Arc::new(StubInferenceAdapter::failing("Mamba2"));
        let adapter = EnsembleModelAdapter::new("mamba2", inner);
        let features = vec![0.1_f64; 42];

        let err = adapter
            .predict(&features)
            .expect_err("inner predict failure must propagate");
        assert!(err.to_string().contains("inference failed"));
    }

    #[test]
    fn predict_returns_err_on_non_finite_output() {
        let inner: Arc<dyn ModelInferenceAdapter> =
            Arc::new(StubInferenceAdapter::non_finite("Liquid"));
        let adapter = EnsembleModelAdapter::new("liquid", inner);
        let features = vec![0.1_f64; 42];

        let err = adapter
            .predict(&features)
            .expect_err("NaN direction must return Err, not fake confidence=0");
        assert!(err.to_string().contains("non-finite"));
    }

    #[test]
    fn predict_returns_err_on_empty_features() {
        let inner: Arc<dyn ModelInferenceAdapter> =
            Arc::new(StubInferenceAdapter::bullish("DQN"));
        let adapter = EnsembleModelAdapter::new("dqn", inner);
        let features: Vec<f64> = Vec::new();

        let err = adapter
            .predict(&features)
            .expect_err("empty features must be rejected");
        assert!(err.to_string().contains("empty feature vector"));
    }

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

    #[tokio::test]
    async fn build_production_strategy_forwards_real_predictions() {
        let dqn: Arc<dyn ModelInferenceAdapter> =
            Arc::new(StubInferenceAdapter::bullish("DQN"));
        let ppo: Arc<dyn ModelInferenceAdapter> =
            Arc::new(StubInferenceAdapter::bearish("PPO"));

        let strategy = build_production_strategy(
            0.5, // threshold below stub confidences (0.7, 0.8)
            vec![
                ("dqn".to_string(), dqn),
                ("ppo".to_string(), ppo),
            ],
        );

        let predictions = strategy
            .get_ensemble_prediction(100.0, 1000.0, Utc::now())
            .await
            .expect("ensemble prediction should succeed");

        // With real (non-zero) confidences, both adapters should clear the
        // threshold -- the previous stub behaviour dropped them all.
        assert_eq!(
            predictions.len(),
            2,
            "expected 2 real predictions (DQN + PPO); got {} — the stub \
             ghost would have produced 0",
            predictions.len(),
        );
        for p in &predictions {
            assert!(p.confidence > 0.0, "real inner confidence must be > 0");
            assert!(p.prediction_value >= 0.0 && p.prediction_value <= 1.0);
        }
    }

    #[tokio::test]
    async fn build_production_strategy_filters_low_confidence_honestly() {
        let low_conf: Arc<dyn ModelInferenceAdapter> = Arc::new(StubInferenceAdapter {
            name: "KAN".to_string(),
            direction: 0.1,
            confidence: 0.2, // below threshold
            ready: true,
            should_fail: false,
        });

        let strategy = build_production_strategy(
            0.5,
            vec![("kan".to_string(), low_conf)],
        );

        let predictions = strategy
            .get_ensemble_prediction(100.0, 1000.0, Utc::now())
            .await
            .expect("ensemble prediction should succeed");

        // Filtered by genuine threshold comparison (0.2 < 0.5), not by a
        // fake 0.0 baked in by the adapter.
        assert!(predictions.is_empty());
    }
}
