//! CPU-side observer for the GPU-driven C51 adaptive atom grid.
//!
//! The `atoms_update` CUDA kernel (Plan 1 Task 9) reads ISV v-range slots
//! [23..31) (per-branch centre + half-width) and spacing_raw parameters to
//! compute adaptive atom positions for all 4 branches in a single GPU launch.
//! This monitor reads the representative `V_CENTER_DIR_INDEX=23` scalar for
//! fire-rate tracking, and exposes all 8 v-range ISV slots in `diagnose()`.
//!
//! The monitor's `read()` returns `isv[V_CENTER_DIR_INDEX]`. `diagnose()`
//! exposes the direction branch v-range (centre + half), all branch v-ranges,
//! and fire-rate.

use crate::cuda_pipeline::gpu_dqn_trainer::{
    V_CENTER_DIR_INDEX, V_HALF_DIR_INDEX,
    V_CENTER_MAG_INDEX, V_HALF_MAG_INDEX,
    V_CENTER_ORD_INDEX, V_HALF_ORD_INDEX,
    V_CENTER_URG_INDEX, V_HALF_URG_INDEX,
};
use crate::trainers::dqn::adaptive_monitor::{
    AdaptiveMonitor, DiagSnapshot, FireRateStats, IsvBus,
};

pub(crate) struct AtomsMonitor {
    last_center: f32,
    fire_rate: FireRateStats,
}

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

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

    fn diagnose(&self, isv: &IsvBus<'_>) -> DiagSnapshot {
        DiagSnapshot::new()
            .with("v_center_dir", isv.read(V_CENTER_DIR_INDEX))
            .with("v_half_dir",   isv.read(V_HALF_DIR_INDEX))
            .with("v_center_mag", isv.read(V_CENTER_MAG_INDEX))
            .with("v_half_mag",   isv.read(V_HALF_MAG_INDEX))
            .with("v_center_ord", isv.read(V_CENTER_ORD_INDEX))
            .with("v_half_ord",   isv.read(V_HALF_ORD_INDEX))
            .with("v_center_urg", isv.read(V_CENTER_URG_INDEX))
            .with("v_half_urg",   isv.read(V_HALF_URG_INDEX))
            .with("fire_rate",    self.fire_rate.fire_rate())
    }

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

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

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

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

    fn make_isv(center_dir: f32, half_dir: f32) -> Vec<f32> {
        let mut v = vec![0.0f32; 49];
        v[V_CENTER_DIR_INDEX] = center_dir;
        v[V_HALF_DIR_INDEX]   = half_dir;
        v[V_CENTER_MAG_INDEX] = 0.1;
        v[V_HALF_MAG_INDEX]   = 0.5;
        v
    }

    #[test]
    fn read_returns_v_center_dir_slot() {
        let mut buf = make_isv(0.05, 2.5);
        let bus = IsvBus::new(&mut buf);
        let mon = AtomsMonitor::new();
        let v = mon.read(&bus);
        assert!((v - 0.05).abs() < 1e-7, "expected v_center_dir=0.05, got {v}");
    }

    #[test]
    fn diagnose_exposes_all_fields() {
        let mut buf = make_isv(0.10, 3.0);
        let bus = IsvBus::new(&mut buf);
        let mon = AtomsMonitor::new();
        let snap = mon.diagnose(&bus);
        assert!(snap.fields.contains_key("v_center_dir"));
        assert!(snap.fields.contains_key("v_half_dir"));
        assert!(snap.fields.contains_key("v_center_mag"));
        assert!(snap.fields.contains_key("v_half_mag"));
        assert!(snap.fields.contains_key("v_center_ord"));
        assert!(snap.fields.contains_key("v_half_ord"));
        assert!(snap.fields.contains_key("v_center_urg"));
        assert!(snap.fields.contains_key("v_half_urg"));
        assert!(snap.fields.contains_key("fire_rate"));
        let center = snap.fields["v_center_dir"];
        assert!((center - 0.10).abs() < 1e-6, "expected v_center_dir=0.10, got {center}");
    }

    #[test]
    fn observe_tracks_fire_rate_on_change() {
        let mut mon = AtomsMonitor::new();
        mon.observe(1.0);    // differs from initial last_value=0.0 → fires
        mon.observe(1.0);    // unchanged → no fire
        mon.observe(1.5);    // differs → fires
        let rate = mon.fire_rate().fire_rate();
        assert!((rate - 2.0 / 3.0).abs() < 1e-6,
                "expected 2/3 fire rate, got {rate}");
    }
}
