//! Read-only observer for GPU-computed adaptive values in the DQN trainer.
//! Spec §4.C.6 (2026-04-24 revision: GPU computes, CPU reads).
//!
//! All adaptive values in the DQN are computed by GPU kernels from ISV-visible
//! inputs and written back to ISV slots. CPU-side code never computes an
//! adaptive value — it only reads and reports. Implementors of this trait are
//! pure observers.
//!
//! The `observe()` method drives fire-rate tracking across epoch boundaries:
//! monitors compare the current GPU-written value to the previous observation
//! and count a "fire" when the value materially changed. This supports the
//! `controller_activity` smoke test which asserts load-bearing mechanisms do
//! not fire in > 50% of epochs (indicating thrash).

use std::collections::HashMap;

/// Thin read/write view over the ISV pinned-memory bus.
///
/// Monitors use `read()` exclusively. `write()` is reserved for CPU-born
/// inputs — epoch_idx, total_epochs, static config constants written once
/// at the appropriate cadence. GPU kernels write via their device pointer,
/// not via `IsvBus`.
pub(crate) struct IsvBus<'a> {
    signals: &'a mut [f32],
}

impl<'a> IsvBus<'a> {
    pub(crate) fn new(signals: &'a mut [f32]) -> Self {
        Self { signals }
    }
    pub(crate) fn read(&self, slot: usize) -> f32 { self.signals[slot] }
    pub(crate) fn write(&mut self, slot: usize, value: f32) { self.signals[slot] = value; }
    pub(crate) fn len(&self) -> usize { self.signals.len() }
}

/// Fire-rate statistics tracked by a monitor across observations.
#[derive(Debug, Default, Clone)]
pub(crate) struct FireRateStats {
    fires: u32,
    epochs: u32,
}

impl FireRateStats {
    pub(crate) fn record_fire(&mut self, fired: bool) {
        self.epochs += 1;
        if fired { self.fires += 1; }
    }
    pub(crate) fn fire_rate(&self) -> f32 {
        if self.epochs == 0 { 0.0 } else { self.fires as f32 / self.epochs as f32 }
    }
    pub(crate) fn reset(&mut self) {
        self.fires = 0;
        self.epochs = 0;
    }
}

/// Diagnostic snapshot emitted by a monitor each epoch.
///
/// Uses `&'static str` keys (no allocation per key). Built via `.with()`.
#[derive(Debug, Default, Clone)]
pub(crate) struct DiagSnapshot {
    pub fields: HashMap<&'static str, f32>,
}

impl DiagSnapshot {
    pub(crate) fn new() -> Self { Self { fields: HashMap::new() } }
    pub(crate) fn with(mut self, key: &'static str, value: f32) -> Self {
        self.fields.insert(key, value);
        self
    }
}

/// Read-only observer for a GPU-computed adaptive value.
///
/// Implementations are pure observers: they read from ISV, compare against
/// their previous observation to drive fire-rate tracking, and emit
/// diagnostic snapshots. They never compute adaptive values — that is the
/// exclusive responsibility of the GPU kernels.
pub(crate) trait AdaptiveMonitor {
    /// Read the GPU-computed current value from ISV.
    fn read(&self, isv: &IsvBus<'_>) -> f32;

    /// Emit diagnostic snapshot for HEALTH_DIAG.
    fn diagnose(&self, isv: &IsvBus<'_>) -> DiagSnapshot;

    /// Record one observation; drives fire-rate tracking at epoch boundary.
    fn observe(&mut self, current: f32);

    /// Fire-rate statistics across prior observations.
    fn fire_rate(&self) -> &FireRateStats;

    /// Short name used in diagnostic output (e.g. "gamma", "kelly_cap").
    fn name(&self) -> &'static str;
}

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

    #[test]
    fn fire_rate_stats_compute() {
        let mut stats = FireRateStats::default();
        stats.record_fire(true);
        stats.record_fire(false);
        stats.record_fire(true);
        assert!((stats.fire_rate() - 2.0f32 / 3.0f32).abs() < 1e-6);
    }

    #[test]
    fn diag_snapshot_builder() {
        let snap = DiagSnapshot::new().with("v", 0.5).with("t", 1.0);
        assert_eq!(snap.fields.len(), 2);
        assert_eq!(snap.fields.get("v"), Some(&0.5f32));
    }

    #[test]
    fn isv_bus_read_write() {
        let mut slots = vec![0.0f32; 4];
        let mut bus = IsvBus::new(&mut slots);
        bus.write(2, 1.5);
        assert_eq!(bus.read(2), 1.5);
        assert_eq!(bus.len(), 4);
    }
}
