//! CPU-side observer for the GPU-driven seed-phase progress.
//!
//! Plan 3 Task 8 B.3. The `seed_step_counter_update` GPU kernel reads
//! ISV[SEED_STEPS_TARGET_INDEX=82] (constructor-written from
//! `config.replay_seed_steps`) and increments ISV[SEED_STEPS_DONE_INDEX=83]
//! by the number of (i,t) samples in each `collect_experiences_gpu` call.
//! It then EMAs the derived `max(0, 1 - DONE/TARGET)` into
//! ISV[SEED_FRAC_EMA_INDEX=84]. The seed-fraction signal is consumed by
//! the Task 9 `cql_alpha_seed_update` kernel to ramp CQL α from 0
//! (during the seed phase) toward `config.cql_alpha` (post-seed).
//!
//! This monitor is a pure read-only observer: it surfaces both the
//! cumulative steps and the seed-fraction EMA in HEALTH_DIAG and tracks
//! per-epoch fire-rate against the seed-fraction so the
//! `controller_activity` smoke test can flag thrash.

use crate::cuda_pipeline::gpu_dqn_trainer::{
    SEED_FRAC_EMA_INDEX, SEED_STEPS_DONE_INDEX, SEED_STEPS_TARGET_INDEX,
};
use crate::trainers::dqn::adaptive_monitor::{
    AdaptiveMonitor, DiagSnapshot, FireRateStats, IsvBus,
};

pub(crate) struct SeedMonitor {
    last_frac: f32,
    fire_rate: FireRateStats,
}

impl SeedMonitor {
    pub(crate) fn new() -> Self {
        // Sentinel `0.0` matches the convention used by `PlanThresholdMonitor`
        // and `StateKlMonitor`: the first `observe` call always fires (the
        // GPU-driven seed-fraction starts at cold-start 1.0, so the
        // 0.0 → 1.0 first transition is a real "first observation" event).
        Self { last_frac: 0.0, fire_rate: FireRateStats::default() }
    }
}

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

    fn diagnose(&self, isv: &IsvBus<'_>) -> DiagSnapshot {
        let target = isv.read(SEED_STEPS_TARGET_INDEX);
        let done   = isv.read(SEED_STEPS_DONE_INDEX);
        let frac   = isv.read(SEED_FRAC_EMA_INDEX);
        DiagSnapshot::new()
            .with("seed.steps_target",   target)
            .with("seed.steps_done",     done)
            .with("seed.frac_ema",       frac)
            .with("fire_rate",           self.fire_rate.fire_rate())
    }

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

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

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

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

    fn make_isv(target: f32, done: f32, frac: f32) -> Vec<f32> {
        let mut v = vec![0.0f32; SEED_FRAC_EMA_INDEX + 1];
        v[SEED_STEPS_TARGET_INDEX] = target;
        v[SEED_STEPS_DONE_INDEX]   = done;
        v[SEED_FRAC_EMA_INDEX]     = frac;
        v
    }

    #[test]
    fn read_returns_frac_slot() {
        let mut buf = make_isv(100_000.0, 5_000.0, 0.95);
        let bus = IsvBus::new(&mut buf);
        let mon = SeedMonitor::new();
        let v = mon.read(&bus);
        assert!((v - 0.95).abs() < 1e-7, "expected frac=0.95, got {v}");
    }

    #[test]
    fn diagnose_exposes_target_done_frac() {
        let mut buf = make_isv(100_000.0, 25_000.0, 0.75);
        let bus = IsvBus::new(&mut buf);
        let mon = SeedMonitor::new();
        let snap = mon.diagnose(&bus);
        assert!(snap.fields.contains_key("seed.steps_target"));
        assert!(snap.fields.contains_key("seed.steps_done"));
        assert!(snap.fields.contains_key("seed.frac_ema"));
        assert!(snap.fields.contains_key("fire_rate"));
        let got_target = snap.fields["seed.steps_target"];
        let got_done   = snap.fields["seed.steps_done"];
        let got_frac   = snap.fields["seed.frac_ema"];
        assert!((got_target - 100_000.0).abs() < 1e-3);
        assert!((got_done   -  25_000.0).abs() < 1e-3);
        assert!((got_frac   -       0.75).abs() < 1e-6);
    }

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