#![allow(unsafe_code)] // CUDA kernel launch via cudarc requires unsafe
//! SP4 Pearl A (first-observation bootstrap) + Pearl D (Wiener-optimal adaptive α)
//! constants, Wiener state, GPU launcher, and host-side reference implementation.
//!
//! Production code path is the GPU `apply_pearls_ad_kernel` (file
//! `apply_pearls_kernel.cu`); the host-side `pearls_ad_update` reference
//! implementation is retained as a test oracle and for the post-Adam
//! Pearl C engagement-rate-deficit diagnostic (which reduces a host-
//! readable mapped-pinned i32 buffer outside any captured graph and is
//! out of scope for the GPU-Pearls graph-capture fix).
//!
//! See `docs/superpowers/specs/2026-04-30-sp4-signal-driven-magnitude-control-design.md`
//! sections "Pearl A: First-observation bootstrap" and "Pearl D: Wiener-optimal
//! adaptive α" for the design rationale, and `feedback_no_cpu_compute_strict.md` /
//! `pearl_cold_path_no_exception_to_gpu_drives.md` for why the host-side
//! `apply_pearls_to_slot` helper was eliminated 2026-05-01.

/// Pearl D meta-α: the rate at which the Wiener state's variance estimates
/// (`sample_var`, `diff_var`) themselves update. Structurally derived from
/// typical per-fold step count (~1000 steps in smoke). Single uniform value
/// across all producers — no per-signal tuning. Same theoretical-constant
/// category as Adam β values (a published-algorithm constant, not a tuning
/// knob).
pub const ALPHA_META: f32 = 1.0e-3;

/// Pearl D division-safety ε. Prevents 0/0 in the optimal-α formula
/// `α* = diff_var / (diff_var + sample_var + ε_div)`. Same Adam-ε numerical
/// category as `ε = 1e-8` in the AdamW denominator — protects arithmetic,
/// not a magnitude estimate.
pub const EPS_DIV: f32 = 1.0e-8;

/// Consumer-side cold-start floor. Used by clamps as
/// `bound = isv[X].max(EPS_CLAMP_FLOOR)` to prevent `bound = 0` from
/// collapsing the clamp at step 0 before any producer runs. Adam-ε category.
pub const EPS_CLAMP_FLOOR: f32 = 1.0;

/// Per-slot Pearl D Wiener state. Stored in `wiener_state_buf` indexed by
/// the ISV slot's offset from `SP4_SLOT_BASE` (× 3 floats per slot).
///
/// Layout matches the mapped-pinned buffer's flat-float layout:
/// `[slot_0_sample_var, slot_0_diff_var, slot_0_x_lag,
///   slot_1_sample_var, slot_1_diff_var, slot_1_x_lag, ...]`.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct WienerState {
    pub sample_var: f32,
    pub diff_var:   f32,
    pub x_lag:      f32,
}

impl WienerState {
    /// Zero state — Pearl A sentinel (untouched / reset to fold-boundary).
    pub const ZERO: WienerState = WienerState { sample_var: 0.0, diff_var: 0.0, x_lag: 0.0 };
}

/// **Test-only reference implementation** of Pearl A + Pearl D for one
/// step observation. Production code MUST use the GPU
/// `apply_pearls_ad_kernel` (launched via [`launch_apply_pearls`]).
///
/// Retained for two reasons:
///   1. The new GPU oracle test in `tests/sp4_producer_unit_tests.rs`
///      asserts kernel output matches this reference within fp32 ULP
///      across 5 representative trajectories.
///   2. The post-Adam `pearl_c_post_adam_engagement_check` host-side
///      diagnostic reduces a mapped-pinned i32 counter array on the
///      host (outside any captured graph) and applies Pearls A+D to
///      the resulting rate-deficit time series. That path is out of
///      scope for the 2026-05-01 GPU-Pearls graph-capture fix because
///      it is post-Adam (not inside the per-step captured graph) and
///      its input lives only in a host-readable counter buffer.
///
/// - `prev_x_mean`: current ISV slot value. Zero on the first call after a
///   fold-boundary reset (Pearl A sentinel).
/// - `state`: this slot's Wiener state. Mutated in place.
/// - `step_observation`: the producer's per-step output (e.g., step_p99).
///
/// Returns: new x_mean to write back to the ISV slot.
///
/// **Pearl A** (sentinel-detect): if `prev_x_mean == 0.0` AND `state.x_lag == 0.0`,
/// this is step 0 after a fold reset. Replace x_mean directly with
/// step_observation, initialize `state.x_lag = step_observation`,
/// keep `sample_var = diff_var = 0`. Bypass Pearl D's Wiener formula.
///
/// **Pearl D** (steps 1+): compute
/// `α* = diff_var / (diff_var + sample_var + EPS_DIV)`, then
/// `x_mean[t] = (1 - α*) · x_mean[t-1] + α* · x[t]`.
/// Variances themselves use uniform meta-α `ALPHA_META`.
///
/// **NOTE:** Pearl D does NOT subsume Pearl A. At t=0 with both variances
/// zero, the formula yields `x_mean[0] = 0` — not `x[0]`. The explicit
/// sentinel branch is REQUIRED. (See test
/// `pearl_d_does_not_subsume_pearl_a_at_t0` below for the math.)
pub fn pearls_ad_update(
    prev_x_mean: f32,
    state: &mut WienerState,
    step_observation: f32,
) -> f32 {
    // Pearl A: first-observation replacement at sentinel.
    if prev_x_mean == 0.0 && state.x_lag == 0.0 {
        state.x_lag = step_observation;
        // sample_var and diff_var stay at 0 — they will accumulate from step 1.
        return step_observation;
    }

    // Pearl D: Wiener-optimal blend.
    let dx_mean = step_observation - prev_x_mean; // residual from prior estimate
    let dx_step = step_observation - state.x_lag; // step-to-step delta

    state.sample_var =
        (1.0 - ALPHA_META) * state.sample_var + ALPHA_META * (dx_mean * dx_mean);
    state.diff_var =
        (1.0 - ALPHA_META) * state.diff_var + ALPHA_META * (dx_step * dx_step);

    let alpha_star = state.diff_var / (state.diff_var + state.sample_var + EPS_DIV);
    let new_x_mean = (1.0 - alpha_star) * prev_x_mean + alpha_star * step_observation;

    state.x_lag = step_observation;
    new_x_mean
}

/// SP4 GPU-only Pearls A+D applicator launcher (2026-05-01).
///
/// Single launcher consumed by all 11 SP4 producer launchers (5 SP4
/// producers + 6 A13 retrofits) plus the inline `aux_heads_forward` Step 2b
/// call site. Replaces the pre-refactor host-side `apply_pearls_to_slot`
/// helper per `feedback_no_cpu_compute_strict.md`.
///
/// Launches the single-thread `apply_pearls_ad_kernel` device-side loop on
/// the producer's stream. The kernel iterates `n_slots` consecutive
/// `(scratch[scratch_idx_base + i], isv[isv_idx_base + i],
/// wiener[wiener_offset_base + 3*i ..])` triples and applies Pearl A
/// (sentinel-bootstrap) or Pearl D (Wiener-optimal blend) per slot. Stream
/// ordering with the producer kernel that wrote `scratch_buf` provides the
/// host-visibility / device-visibility fence — the kernel itself issues
/// `__threadfence_system()` on completion. No host sync is needed; the
/// launcher is graph-capture-compatible by construction (which is the
/// whole point of the refactor — the host-sync `apply_pearls_to_slot`
/// pattern broke graph capture at `aux_heads_forward` Step 2b's
/// `aux_label_scale_ema`, smoke `smoke-test-v9kjv`).
///
/// Buffers MUST be device pointers (the `dev_ptr` field on a
/// `MappedF32Buffer`, or `isv_signals_dev_ptr`). Passing host (mapped-
/// pinned) pointers will fault the kernel — the kernel deferences directly
/// in device address space.
///
/// `n_slots` is at most 33 (the Pearl B param-group oracle's per-group
/// 4-or-5 outputs aggregated across the 8 groups + L1 trunk lambda) and
/// typically 1..9. Single-thread device-side loop is correct because each
/// slot has its own state — no cross-slot parallelism is meaningful.
///
/// `alpha_meta`: meta-EMA rate for Wiener variance tracking. SP4 default
/// and all SP5 producers except Pearl 4 use [`ALPHA_META`] (1e-3). Pearl 4
/// uses 5e-4 (half the default per spec line 89 to limit β change rate).
///
/// # Safety
///
/// All three buffer pointers MUST be valid device pointers. Index ranges
/// MUST stay within their respective buffer bounds:
///   - `scratch_idx_base + n_slots ≤ SP4_PRODUCER_COUNT`
///   - `isv_idx_base + n_slots ≤ ISV_TOTAL_DIM`
///   - `wiener_offset_base + 3 * n_slots ≤ SP4_PRODUCER_COUNT * 3`
///
/// The caller MUST issue this launch on the SAME stream as the producer
/// kernel that wrote `scratch_buf[scratch_idx_base..]`; same-stream
/// operations are sequentially consistent so no explicit barrier is
/// required.
pub unsafe fn launch_apply_pearls(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    scratch_dev: u64,
    scratch_idx_base: i32,
    isv_dev: u64,
    isv_idx_base: i32,
    wiener_state_dev: u64,
    wiener_offset_base: i32,
    n_slots: i32,
    alpha_meta: f32,
) -> Result<(), crate::MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(n_slots >= 0,
        "launch_apply_pearls: n_slots must be non-negative, got {n_slots}");
    if n_slots == 0 {
        return Ok(());
    }
    debug_assert!(scratch_dev != 0,
        "launch_apply_pearls: scratch_dev must be a valid device pointer");
    debug_assert!(isv_dev != 0,
        "launch_apply_pearls: isv_dev must be a valid device pointer");
    debug_assert!(wiener_state_dev != 0,
        "launch_apply_pearls: wiener_state_dev must be a valid device pointer");

    let cfg = LaunchConfig {
        grid_dim:         (1, 1, 1),
        block_dim:        (1, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&scratch_dev)
        .arg(&scratch_idx_base)
        .arg(&isv_dev)
        .arg(&isv_idx_base)
        .arg(&wiener_state_dev)
        .arg(&wiener_offset_base)
        .arg(&n_slots)
        .arg(&alpha_meta)
        .launch(cfg)
        .map_err(|e| {
            crate::MLError::ModelError(format!("apply_pearls_ad_kernel launch: {e}"))
        })?;
    Ok(())
}

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

    #[test]
    fn pearl_a_first_observation_replaces_sentinel() {
        let mut state = WienerState::ZERO;
        let new_mean = pearls_ad_update(0.0, &mut state, 42.0);
        assert_eq!(new_mean, 42.0,
            "Pearl A: first observation replaces sentinel directly");
        assert_eq!(state.x_lag, 42.0,
            "Pearl A: x_lag initialized to first observation");
        assert_eq!(state.sample_var, 0.0);
        assert_eq!(state.diff_var, 0.0);
    }

    #[test]
    fn pearl_d_stationary_signal_alpha_approaches_zero() {
        // After many observations of a constant signal, α* → 0
        // (sample_var > 0, diff_var → 0).
        let mut state = WienerState::ZERO;
        let mut x_mean = pearls_ad_update(0.0, &mut state, 5.0); // step 0 (Pearl A)
        for _ in 0..10_000 {
            x_mean = pearls_ad_update(x_mean, &mut state, 5.0);
        }
        // Constant signal: diff_var ≈ 0, x_mean stays at 5.0.
        assert!(state.diff_var < 1e-6,
            "diff_var should be near 0 for constant signal, got {}", state.diff_var);
        assert!((x_mean - 5.0).abs() < 1e-3,
            "x_mean should anchor at constant signal, got {}", x_mean);
    }

    #[test]
    fn pearl_d_step_change_tracks_within_meta_window() {
        // Signal jumps from 1.0 → 100.0; α* should rise; x_mean should track.
        let mut state = WienerState::ZERO;
        let mut x_mean = pearls_ad_update(0.0, &mut state, 1.0);
        for _ in 0..1000 {
            x_mean = pearls_ad_update(x_mean, &mut state, 1.0);
        }

        // Now step-change.
        for _ in 0..2000 {
            x_mean = pearls_ad_update(x_mean, &mut state, 100.0);
        }

        // After ~2000 steps post-change, x_mean should be much closer to 100 than 1.
        assert!(x_mean > 50.0,
            "x_mean should track step-change toward 100, got {}", x_mean);
    }

    #[test]
    fn pearl_d_does_not_subsume_pearl_a_at_t0() {
        // Mathematical correctness check: at t=0 with all variances at 0,
        // Pearl D's formula `x_mean = (1 - α*) · prev + α* · obs` would yield
        // x_mean = (1 - 0) · 0 + 0 · obs = 0, NOT obs. Pearl A's explicit
        // sentinel-detect branch is REQUIRED.
        let mut state = WienerState::ZERO;
        let result = pearls_ad_update(0.0, &mut state, 7.5);
        assert_eq!(result, 7.5,
            "Pearl A sentinel branch must override Pearl D at t=0");
    }

    #[test]
    fn meta_constants_are_structural() {
        // Document-as-code: these are the single set of structural constants.
        // ALPHA_META is the meta-EMA rate for variance tracking; EPS_DIV is
        // numerical division-safety; EPS_CLAMP_FLOOR is consumer cold-start.
        assert_eq!(ALPHA_META, 1.0e-3);
        assert_eq!(EPS_DIV, 1.0e-8);
        assert_eq!(EPS_CLAMP_FLOOR, 1.0);
    }

    #[test]
    fn pearl_a_only_fires_when_both_x_mean_and_x_lag_are_zero() {
        // If prev_x_mean is 0 but x_lag is non-zero (e.g., a previous Pearl A
        // fired and was followed by Pearl D), Pearl A must NOT re-fire.
        let mut state = WienerState { sample_var: 0.5, diff_var: 0.3, x_lag: 10.0 };
        let result = pearls_ad_update(0.0, &mut state, 20.0);
        // Result is Pearl D's output, not 20.0 directly.
        assert_ne!(result, 20.0,
            "Pearl A must not re-fire when x_lag is already populated");
        // x_lag advanced to step_observation.
        assert_eq!(state.x_lag, 20.0);
    }
}
