//! Prometheus Metrics for Ensemble ML Production Monitoring
//!
//! This module defines 10 ensemble-specific Prometheus metrics for production
//! observability as specified in ENSEMBLE_PRODUCTION_DEPLOYMENT_STRATEGY.md
//!
//! ## Metrics Overview
//!
//! 1. **ensemble_aggregation_latency_microseconds**: Histogram tracking time to aggregate signals
//! 2. **ensemble_confidence_score**: Gauge for ensemble prediction confidence (0-1)
//! 3. **ensemble_disagreement_rate**: Gauge for model disagreement percentage (0-1)
//! 4. **ensemble_predictions_total**: Counter for predictions by action type
//! 5. **ensemble_model_weight**: Gauge for per-model contribution weights
//! 6. **ensemble_high_disagreement_total**: Counter for high disagreement events
//! 7. **ensemble_model_pnl_contribution_dollars**: Histogram for P&L attribution per model
//! 8. **checkpoint_swaps_total**: Counter for checkpoint hot-swaps
//! 9. **ab_test_assignments_total**: Counter for A/B test group assignments
//! 10. **ab_test_metric_difference**: Gauge for treatment-control metric differences

#![allow(clippy::expect_used)] // Metric registration failures are fatal

use once_cell::sync::Lazy;
use prometheus::{
    register_counter_vec, register_gauge_vec, register_histogram_vec, CounterVec, GaugeVec,
    HistogramVec,
};

/// Histogram for ensemble aggregation latency (microseconds)
///
/// Labels:
/// - aggregation_method: weighted_average, majority_vote, confidence_weighted
///
/// Buckets: 1, 5, 10, 25, 50, 100μs (target: P99 < 25μs)
pub static ENSEMBLE_AGGREGATION_LATENCY_US: Lazy<HistogramVec> = Lazy::new(|| {
    register_histogram_vec!(
        "ensemble_aggregation_latency_microseconds",
        "Time to aggregate signals from all models (microseconds)",
        &["aggregation_method"],
        vec![1.0, 5.0, 10.0, 25.0, 50.0, 100.0]
    )
    .expect("Failed to register ensemble_aggregation_latency_microseconds")
});

/// Gauge for ensemble confidence score (0.0 to 1.0)
///
/// Labels:
/// - symbol: Trading symbol (ES.FUT, NQ.FUT, ZN.FUT, 6E.FUT)
///
/// Values: 0.0 (no confidence) to 1.0 (maximum confidence)
pub static ENSEMBLE_CONFIDENCE: Lazy<GaugeVec> = Lazy::new(|| {
    register_gauge_vec!(
        "ensemble_confidence_score",
        "Ensemble prediction confidence (0.0-1.0)",
        &["symbol"]
    )
    .expect("Failed to register ensemble_confidence_score")
});

/// Gauge for model disagreement rate (0.0 to 1.0)
///
/// Labels:
/// - symbol: Trading symbol
///
/// Values: 0.0 (all models agree) to 1.0 (all models disagree)
/// Alert threshold: > 0.5 indicates high uncertainty or regime shift
pub static ENSEMBLE_DISAGREEMENT_RATE: Lazy<GaugeVec> = Lazy::new(|| {
    register_gauge_vec!(
        "ensemble_disagreement_rate",
        "Percentage of models disagreeing with ensemble decision (0.0-1.0)",
        &["symbol"]
    )
    .expect("Failed to register ensemble_disagreement_rate")
});

/// Counter for ensemble predictions by action type
///
/// Labels:
/// - action: buy, sell, hold
/// - symbol: Trading symbol
///
/// Tracks total predictions over time by action type
pub static ENSEMBLE_PREDICTIONS_TOTAL: Lazy<CounterVec> = Lazy::new(|| {
    register_counter_vec!(
        "ensemble_predictions_total",
        "Total ensemble predictions by action type",
        &["action", "symbol"]
    )
    .expect("Failed to register ensemble_predictions_total")
});

/// Gauge for per-model contribution weight (0.0 to 1.0)
///
/// Labels:
/// - model_id: DQN, PPO, MAMBA-2, TFT, TLOB
/// - symbol: Trading symbol
///
/// Values: 0.0 (no contribution) to 1.0 (dominant model)
/// Weights should sum to 1.0 across all models for a given symbol
pub static ENSEMBLE_MODEL_WEIGHT: Lazy<GaugeVec> = Lazy::new(|| {
    register_gauge_vec!(
        "ensemble_model_weight",
        "Dynamic weight assigned to each model in ensemble (0.0-1.0)",
        &["model_id", "symbol"]
    )
    .expect("Failed to register ensemble_model_weight")
});

/// Counter for high-disagreement events
///
/// Labels:
/// - symbol: Trading symbol
/// - threshold: 0.5, 0.7, 0.9 (disagreement rate thresholds)
///
/// Tracks predictions where model disagreement exceeds threshold
/// High disagreement may indicate:
/// - Market regime shift
/// - Model drift
/// - Data quality issues
pub static ENSEMBLE_HIGH_DISAGREEMENT_TOTAL: Lazy<CounterVec> = Lazy::new(|| {
    register_counter_vec!(
        "ensemble_high_disagreement_total",
        "Count of predictions with high model disagreement",
        &["symbol", "threshold"]
    )
    .expect("Failed to register ensemble_high_disagreement_total")
});

/// Histogram for P&L attribution per model (dollars)
///
/// Labels:
/// - model_id: DQN, PPO, MAMBA-2, TFT, TLOB
/// - symbol: Trading symbol
///
/// Buckets: -1000, -500, -100, 0, 100, 500, 1000, 5000 dollars
/// Tracks profit/loss contribution from each model's signals
pub static ENSEMBLE_MODEL_PNL_CONTRIBUTION: Lazy<HistogramVec> = Lazy::new(|| {
    register_histogram_vec!(
        "ensemble_model_pnl_contribution_dollars",
        "P&L contribution from each model in ensemble (dollars)",
        &["model_id", "symbol"],
        vec![-1000.0, -500.0, -100.0, 0.0, 100.0, 500.0, 1000.0, 5000.0]
    )
    .expect("Failed to register ensemble_model_pnl_contribution_dollars")
});

/// Counter for checkpoint swaps (hot-swapping)
///
/// Labels:
/// - model_id: DQN, PPO, MAMBA-2, TFT, TLOB
/// - status: success, failed, rollback
///
/// Tracks checkpoint update operations:
/// - success: Checkpoint loaded and validated successfully
/// - failed: Checkpoint load failed (kept active buffer)
/// - rollback: Canary validation failed (reverted to previous)
pub static CHECKPOINT_SWAPS_TOTAL: Lazy<CounterVec> = Lazy::new(|| {
    register_counter_vec!(
        "checkpoint_swaps_total",
        "Total checkpoint hot-swaps by status",
        &["model_id", "status"]
    )
    .expect("Failed to register checkpoint_swaps_total")
});

/// Counter for A/B test group assignments
///
/// Labels:
/// - test_id: UUID of A/B test
/// - group: control, treatment
///
/// Tracks user assignments to A/B test groups
/// Should be balanced (50/50 split) for valid experiments
pub static AB_TEST_ASSIGNMENTS_TOTAL: Lazy<CounterVec> = Lazy::new(|| {
    register_counter_vec!(
        "ab_test_assignments_total",
        "Total A/B test group assignments",
        &["test_id", "group"]
    )
    .expect("Failed to register ab_test_assignments_total")
});

/// Gauge for A/B test metric difference (treatment - control)
///
/// Labels:
/// - test_id: UUID of A/B test
/// - metric: sharpe_ratio, win_rate, pnl, latency_us
///
/// Values: Positive = treatment better, Negative = control better
/// Used to visualize lift in real-time during A/B test
pub static AB_TEST_METRIC_DIFF: Lazy<GaugeVec> = Lazy::new(|| {
    register_gauge_vec!(
        "ab_test_metric_difference",
        "Difference in metrics between treatment and control groups",
        &["test_id", "metric"]
    )
    .expect("Failed to register ab_test_metric_difference")
});

/// Helper struct for tracking ensemble prediction metrics
#[derive(Debug, Clone)]
pub struct EnsemblePredictionMetrics {
    pub symbol: String,
    pub action: String,
    pub confidence: f64,
    pub disagreement_rate: f64,
    pub aggregation_latency_us: f64,
    pub aggregation_method: String,
}

impl EnsemblePredictionMetrics {
    /// Record all metrics for a single ensemble prediction
    pub fn record(&self) {
        // Record aggregation latency
        ENSEMBLE_AGGREGATION_LATENCY_US
            .with_label_values(&[self.aggregation_method.as_str()])
            .observe(self.aggregation_latency_us);

        // Record confidence
        ENSEMBLE_CONFIDENCE
            .with_label_values(&[self.symbol.as_str()])
            .set(self.confidence);

        // Record disagreement rate
        ENSEMBLE_DISAGREEMENT_RATE
            .with_label_values(&[self.symbol.as_str()])
            .set(self.disagreement_rate);

        // Record prediction count
        ENSEMBLE_PREDICTIONS_TOTAL
            .with_label_values(&[self.action.as_str(), self.symbol.as_str()])
            .inc();

        // Record high disagreement events
        if self.disagreement_rate >= 0.5 {
            ENSEMBLE_HIGH_DISAGREEMENT_TOTAL
                .with_label_values(&[self.symbol.as_str(), "0.5"])
                .inc();
        }
        if self.disagreement_rate >= 0.7 {
            ENSEMBLE_HIGH_DISAGREEMENT_TOTAL
                .with_label_values(&[self.symbol.as_str(), "0.7"])
                .inc();
        }
        if self.disagreement_rate >= 0.9 {
            ENSEMBLE_HIGH_DISAGREEMENT_TOTAL
                .with_label_values(&[self.symbol.as_str(), "0.9"])
                .inc();
        }
    }
}

/// Helper struct for tracking model weight updates
#[derive(Debug, Clone)]
pub struct ModelWeightUpdate {
    pub model_id: String,
    pub symbol: String,
    pub weight: f64,
}

impl ModelWeightUpdate {
    /// Record model weight metric
    pub fn record(&self) {
        ENSEMBLE_MODEL_WEIGHT
            .with_label_values(&[self.model_id.as_str(), self.symbol.as_str()])
            .set(self.weight);
    }
}

/// Helper struct for tracking P&L attribution
#[derive(Debug, Clone)]
pub struct ModelPnLAttribution {
    pub model_id: String,
    pub symbol: String,
    pub pnl_dollars: f64,
}

impl ModelPnLAttribution {
    /// Record P&L contribution metric
    pub fn record(&self) {
        ENSEMBLE_MODEL_PNL_CONTRIBUTION
            .with_label_values(&[self.model_id.as_str(), self.symbol.as_str()])
            .observe(self.pnl_dollars);
    }
}

/// Helper struct for tracking checkpoint swaps
#[derive(Debug, Clone)]
pub struct CheckpointSwapEvent {
    pub model_id: String,
    pub status: CheckpointSwapStatus,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckpointSwapStatus {
    Success,
    Failed,
    Rollback,
}

impl CheckpointSwapStatus {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Success => "success",
            Self::Failed => "failed",
            Self::Rollback => "rollback",
        }
    }
}

impl CheckpointSwapEvent {
    /// Record checkpoint swap metric
    pub fn record(&self) {
        CHECKPOINT_SWAPS_TOTAL
            .with_label_values(&[self.model_id.as_str(), self.status.as_str()])
            .inc();
    }
}

/// Helper struct for tracking A/B test assignments
#[derive(Debug, Clone)]
pub struct ABTestAssignment {
    pub test_id: String,
    pub group: ABTestGroup,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ABTestGroup {
    Control,
    Treatment,
}

impl ABTestGroup {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Control => "control",
            Self::Treatment => "treatment",
        }
    }
}

impl ABTestAssignment {
    /// Record A/B test assignment metric
    pub fn record(&self) {
        AB_TEST_ASSIGNMENTS_TOTAL
            .with_label_values(&[self.test_id.as_str(), self.group.as_str()])
            .inc();
    }
}

/// Helper struct for tracking A/B test metric differences
#[derive(Debug, Clone)]
pub struct ABTestMetricDiff {
    pub test_id: String,
    pub metric: ABTestMetric,
    pub difference: f64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ABTestMetric {
    SharpeRatio,
    WinRate,
    PnL,
    LatencyUs,
}

impl ABTestMetric {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::SharpeRatio => "sharpe_ratio",
            Self::WinRate => "win_rate",
            Self::PnL => "pnl",
            Self::LatencyUs => "latency_us",
        }
    }
}

impl ABTestMetricDiff {
    /// Record A/B test metric difference
    pub fn record(&self) {
        AB_TEST_METRIC_DIFF
            .with_label_values(&[self.test_id.as_str(), self.metric.as_str()])
            .set(self.difference);
    }
}

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

    #[test]
    fn test_ensemble_prediction_metrics_recording() {
        let metrics = EnsemblePredictionMetrics {
            symbol: "ES.FUT".to_string(),
            action: "buy".to_string(),
            confidence: 0.85,
            disagreement_rate: 0.25,
            aggregation_latency_us: 12.5,
            aggregation_method: "weighted_average".to_string(),
        };

        metrics.record();

        // Verify metrics are recorded (values should be accessible)
        let confidence = ENSEMBLE_CONFIDENCE.with_label_values(&["ES.FUT"]).get();
        assert!((confidence - 0.85).abs() < 1e-6);
    }

    #[test]
    fn test_model_weight_update() {
        let weight_update = ModelWeightUpdate {
            model_id: "DQN".to_string(),
            symbol: "ZN.FUT".to_string(),
            weight: 0.40,
        };

        weight_update.record();

        let weight = ENSEMBLE_MODEL_WEIGHT
            .with_label_values(&["DQN", "ZN.FUT"])
            .get();
        assert!((weight - 0.40).abs() < 1e-6);
    }

    #[test]
    fn test_pnl_attribution() {
        let pnl = ModelPnLAttribution {
            model_id: "MAMBA-2".to_string(),
            symbol: "6E.FUT".to_string(),
            pnl_dollars: 1234.56,
        };

        pnl.record();

        // Histogram metrics don't have direct get() - just verify it doesn't panic
        // In production, query via Prometheus API
    }

    #[test]
    fn test_high_disagreement_thresholds() {
        // High disagreement (0.75 > 0.5, 0.7)
        let metrics = EnsemblePredictionMetrics {
            symbol: "NQ.FUT".to_string(),
            action: "sell".to_string(),
            confidence: 0.60,
            disagreement_rate: 0.75,
            aggregation_latency_us: 8.0,
            aggregation_method: "confidence_weighted".to_string(),
        };

        let before_50 = ENSEMBLE_HIGH_DISAGREEMENT_TOTAL
            .with_label_values(&["NQ.FUT", "0.5"])
            .get() as i64;
        let before_70 = ENSEMBLE_HIGH_DISAGREEMENT_TOTAL
            .with_label_values(&["NQ.FUT", "0.7"])
            .get() as i64;
        let before_90 = ENSEMBLE_HIGH_DISAGREEMENT_TOTAL
            .with_label_values(&["NQ.FUT", "0.9"])
            .get() as i64;

        metrics.record();

        // Should increment 0.5 and 0.7 thresholds, but not 0.9
        assert_eq!(
            ENSEMBLE_HIGH_DISAGREEMENT_TOTAL
                .with_label_values(&["NQ.FUT", "0.5"])
                .get() as i64,
            before_50 + 1
        );
        assert_eq!(
            ENSEMBLE_HIGH_DISAGREEMENT_TOTAL
                .with_label_values(&["NQ.FUT", "0.7"])
                .get() as i64,
            before_70 + 1
        );
        assert_eq!(
            ENSEMBLE_HIGH_DISAGREEMENT_TOTAL
                .with_label_values(&["NQ.FUT", "0.9"])
                .get() as i64,
            before_90
        );
    }

    #[test]
    fn test_checkpoint_swap_events() {
        let swap = CheckpointSwapEvent {
            model_id: "PPO".to_string(),
            status: CheckpointSwapStatus::Success,
        };

        let before = CHECKPOINT_SWAPS_TOTAL
            .with_label_values(&["PPO", "success"])
            .get() as i64;

        swap.record();

        assert_eq!(
            CHECKPOINT_SWAPS_TOTAL
                .with_label_values(&["PPO", "success"])
                .get() as i64,
            before + 1
        );
    }

    #[test]
    fn test_ab_test_assignment() {
        let assignment = ABTestAssignment {
            test_id: "test-123".to_string(),
            group: ABTestGroup::Treatment,
        };

        let before = AB_TEST_ASSIGNMENTS_TOTAL
            .with_label_values(&["test-123", "treatment"])
            .get() as i64;

        assignment.record();

        assert_eq!(
            AB_TEST_ASSIGNMENTS_TOTAL
                .with_label_values(&["test-123", "treatment"])
                .get() as i64,
            before + 1
        );
    }

    #[test]
    fn test_ab_test_metric_diff() {
        let diff = ABTestMetricDiff {
            test_id: "test-456".to_string(),
            metric: ABTestMetric::SharpeRatio,
            difference: 0.32, // Treatment 32% better
        };

        diff.record();

        let recorded_diff = AB_TEST_METRIC_DIFF
            .with_label_values(&["test-456", "sharpe_ratio"])
            .get();
        assert!((recorded_diff - 0.32).abs() < 1e-6);
    }
}
