//! CPU-side observer for the GPU-driven effective Kelly position cap.
//!
//! The `kelly_cap_update` CUDA kernel aggregates Kelly win/loss statistics
//! across all environments from `portfolio_states[n_envs, PS_STRIDE]`, computes
//! the mean half-Kelly fraction, multiplies by a health-coupled safety factor
//! (0.5 + 0.5×health), and writes the result to ISV[KELLY_CAP_EFF_INDEX=47].
//! This monitor reads that slot for HEALTH_DIAG emission and fire-rate tracking.
//!
//! The monitor's `read()` returns `isv[KELLY_CAP_EFF_INDEX]`. `diagnose()`
//! exposes the effective cap, health, and fire-rate.

use crate::cuda_pipeline::gpu_dqn_trainer::{
    LEARNING_HEALTH_INDEX, KELLY_CAP_EFF_INDEX,
};
use crate::trainers::dqn::adaptive_monitor::{
    AdaptiveMonitor, DiagSnapshot, FireRateStats, IsvBus,
};

pub(crate) struct KellyCapMonitor {
    last_cap: f32,
    fire_rate: FireRateStats,
}

impl KellyCapMonitor {
    pub(crate) fn new() -> Self {
        Self { last_cap: 0.0, fire_rate: FireRateStats::default() }
    }
}

impl AdaptiveMonitor for KellyCapMonitor {
    fn read(&self, isv: &IsvBus<'_>) -> f32 {
        isv.read(KELLY_CAP_EFF_INDEX)
    }

    fn diagnose(&self, isv: &IsvBus<'_>) -> DiagSnapshot {
        let kelly_cap = isv.read(KELLY_CAP_EFF_INDEX);
        let health    = isv.read(LEARNING_HEALTH_INDEX);
        DiagSnapshot::new()
            .with("kelly_cap_eff", kelly_cap)
            .with("health",        health)
            .with("fire_rate",     self.fire_rate.fire_rate())
    }

    fn observe(&mut self, current: f32) {
        let fired = (current - self.last_cap).abs() > 1e-6;
        self.fire_rate.record_fire(fired);
        self.last_cap = current;
    }

    fn fire_rate(&self) -> &FireRateStats {
        &self.fire_rate
    }

    fn name(&self) -> &'static str {
        "kelly_cap"
    }
}

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

    fn make_isv(health: f32, kelly_cap: f32) -> Vec<f32> {
        let mut v = vec![0.0f32; 49];
        v[LEARNING_HEALTH_INDEX] = health;
        v[KELLY_CAP_EFF_INDEX]   = kelly_cap;
        v
    }

    #[test]
    fn read_returns_kelly_cap_eff_slot() {
        let mut buf = make_isv(0.8, 0.35);
        let bus = IsvBus::new(&mut buf);
        let mon = KellyCapMonitor::new();
        let v = mon.read(&bus);
        assert!((v - 0.35).abs() < 1e-7, "expected kelly_cap_eff=0.35, got {v}");
    }

    #[test]
    fn diagnose_exposes_all_fields() {
        let mut buf = make_isv(0.6, 0.25);
        let bus = IsvBus::new(&mut buf);
        let mon = KellyCapMonitor::new();
        let snap = mon.diagnose(&bus);
        assert!(snap.fields.contains_key("kelly_cap_eff"));
        assert!(snap.fields.contains_key("health"));
        assert!(snap.fields.contains_key("fire_rate"));
        let cap = snap.fields["kelly_cap_eff"];
        assert!((cap - 0.25).abs() < 1e-6, "expected kelly_cap_eff=0.25, got {cap}");
    }

    #[test]
    fn observe_tracks_fire_rate_on_change() {
        let mut mon = KellyCapMonitor::new();
        mon.observe(0.30);
        mon.observe(0.30);
        mon.observe(0.35);
        let rate = mon.fire_rate().fire_rate();
        assert!((rate - 2.0 / 3.0).abs() < 1e-6,
                "expected 2/3 fire rate, got {rate}");
    }
}
