// crates/ml/src/cuda_pipeline/alpha_regime_vol_update.cu
//
// Phase E.4.A.T16 (2026-05-16): vol-EMA regime detection.
//
// Per inference step, reads B mid-prices and the previous step's mids,
// computes the cross-env mean squared log-return, and updates two ISV
// slots:
//
//   slot 549 (REGIME_VOL_EMA): Wiener-α EMA of mean-squared log-return.
//                              Pearl A bootstrap (sentinel 0 → first
//                              observation replaces directly). Pearl D
//                              Wiener-α with floor 0.4 per
//                              pearl_wiener_alpha_floor_for_nonstationary
//                              (the controller co-adapts with policy
//                              and the target itself drifts — Wiener
//                              would collapse to 0 without the floor).
//
//   slot 550 (REGIME_VOL_REF): slow-tracking reference vol. β=0.005
//                              per-step (about 200-step horizon). On
//                              first observation, bootstraps directly
//                              from slot 549 so the policy's "expected
//                              vol regime" matches the early-training
//                              observation. Drifts slowly enough that
//                              acute spikes do NOT pollute the
//                              reference, preserving the spike-detector
//                              property of `vol_ema / vol_ref`.
//
// Downstream: `stacker_threshold_controller.cu` reads slots 549/550 in
// the Kelly-attenuation branch — when vol_ema >> vol_ref the controller
// shrinks Kelly *before* losses materialise (pre-emptive defense), while
// the existing Sharpe-error controller continues to react after losses
// (reactive defense). The two compose multiplicatively.
//
// Why mid-price log-return and not spread/imbalance? Vol is the single
// strongest regime indicator empirically (cf. walk-forward Fold B failure
// in Q1 2024 mid-quarter). Multi-feature regime classification is a
// follow-up; this kernel is the MVP minimal-borrow regime signal.
//
// Mapped-pinned discipline: caller passes device pointers for the mids,
// and ISV is the standard device-side bus. No CPU branches inside the
// kernel — single thread does the EMA arithmetic after a CUB-free
// block reduction over the B mids.

#include <cuda_runtime.h>

// Wiener-α floor — keeps the EMA responsive when the target drifts.
// Mirrors the 0.4 floor used by stacker_threshold_controller and
// elsewhere per pearl_wiener_alpha_floor_for_nonstationary.
__device__ __forceinline__ float wiener_alpha_floored(
    float diff_var, float sample_var, float floor
) {
    const float eps = 1e-12f;
    float alpha = diff_var / (diff_var + sample_var + eps);
    return fmaxf(floor, fminf(alpha, 1.0f));
}

// Cooperative block reduction over B values stored at mids[b]. Returns
// the mean SQUARED log-return for thread 0; other threads return 0.
extern "C" __global__ void alpha_regime_vol_update_kernel(
    const float* __restrict__ mids_curr,   // [B] current mids (device)
    const float* __restrict__ mids_prev,   // [B] previous step mids
    float*       __restrict__ isv,         // ISV bus
    int          B,
    int          vol_ema_index,            // 549
    int          vol_ref_index,            // 550
    int          vol_ref_samples_index,    // 551 (bootstrap counter)
    int          vol_ref_floor_index,      // 552 (TrainingPersist floor anchor)
    int          vol_obs_min_index,        // 553 (running min — input to floor controller)
    int          bootstrap_samples,        // N — typ. 100
    float        ref_track_rate            // β (per-step), e.g. 0.005
) {
    // Single-block design: each thread handles one b. Final reduction by
    // thread 0. B is typically 50 (n_eval_episodes) — fits easily in 256
    // threads. If B > blockDim.x, each thread handles a strided slice.
    __shared__ float partial_sum[256];
    __shared__ int   partial_cnt[256];

    const int tid = threadIdx.x;
    float my_sum = 0.0f;
    int   my_cnt = 0;
    for (int b = tid; b < B; b += blockDim.x) {
        const float curr = mids_curr[b];
        const float prev = mids_prev[b];
        // Guard against zero / non-finite mids (zero-init buffer at
        // episode 0 will have curr == prev == 0 for some envs).
        if (curr > 0.0f && prev > 0.0f && isfinite(curr) && isfinite(prev)) {
            const float r = logf(curr / prev);
            my_sum += r * r;
            my_cnt += 1;
        }
    }
    partial_sum[tid] = my_sum;
    partial_cnt[tid] = my_cnt;
    __syncthreads();

    // Tree-reduce (block-internal; no atomicAdd per feedback_no_atomicadd).
    for (int stride = blockDim.x >> 1; stride > 0; stride >>= 1) {
        if (tid < stride) {
            partial_sum[tid] += partial_sum[tid + stride];
            partial_cnt[tid] += partial_cnt[tid + stride];
        }
        __syncthreads();
    }

    if (tid != 0) return;

    const int total = partial_cnt[0];
    if (total == 0) return; // no valid envs this step — leave ISV unchanged

    const float vol_obs = partial_sum[0] / (float)total;

    // Pearl A first-observation bootstrap on slot 549.
    const float vol_ema_prev = isv[vol_ema_index];
    float vol_ema_new;
    if (vol_ema_prev <= 0.0f) {
        vol_ema_new = vol_obs;
    } else {
        // Wiener-optimal α with 0.4 floor. diff_var proxy: (vol_obs −
        // vol_ema_prev)²; sample_var proxy: vol_ema_prev² (rough running
        // variance — exact Welford machinery is heavier than the spike-
        // detector application needs at this point).
        const float diff = vol_obs - vol_ema_prev;
        const float diff_var = diff * diff;
        const float sample_var = vol_ema_prev * vol_ema_prev;
        const float alpha = wiener_alpha_floored(diff_var, sample_var, 0.4f);
        vol_ema_new = vol_ema_prev + alpha * diff;
    }
    isv[vol_ema_index] = vol_ema_new;

    // Slow reference vol tracking on slot 550 with a bootstrap window.
    //
    // Old behaviour: vol_ref ← vol_ema on the very first observation.
    // Problem: vol_obs is itself high-variance, so vol_ref started off
    // noisy and short-training-window walk-forward folds saw vol_ema
    // diverge from a noisy vol_ref → spurious early Kelly attenuation.
    //
    // New behaviour: while sample count < bootstrap_samples, vol_ref
    // is the running mean over the observed vol_ema values
    //   vol_ref ← vol_ref + (vol_ema − vol_ref) / (count + 1)
    // After bootstrap, switch to slow β-tracking
    //   vol_ref ← vol_ref + β · (vol_ema − vol_ref)
    // Running mean shrinks the bootstrap variance by √N relative to
    // single-shot replace; median would shrink it further but requires
    // either O(N) buffered samples or a quantile sketch — running mean
    // is single-pass with one f32 of extra state, which fits naturally
    // in an ISV slot.
    const float vol_ref_prev = isv[vol_ref_index];
    const float sample_count_prev = isv[vol_ref_samples_index];
    float vol_ref_new;
    if (sample_count_prev < (float)bootstrap_samples) {
        // Bootstrap phase: running mean of observed vol_ema values.
        const float n = sample_count_prev + 1.0f;
        vol_ref_new = vol_ref_prev + (vol_ema_new - vol_ref_prev) / n;
    } else {
        // Steady state: slow β-tracking.
        vol_ref_new = vol_ref_prev + ref_track_rate * (vol_ema_new - vol_ref_prev);
    }
    // Permanent floor (`pearl_blend_formulas_must_have_permanent_floor`).
    // Two layers per `pearl_kelly_cap_signal_driven_floors`:
    //   * ISV-driven floor (slot 552, TrainingPersist) — the learned
    //     anchor. Sentinel 0 → fall back to the hardcoded sub-floor.
    //   * Hardcoded sub-floor 1e-12 — purely a float-underflow guard
    //     against the ISV slot itself collapsing to zero.
    // Prevents the asymmetric deadlock: if vol_ref decays to ≈0 during
    // a deadband stretch (calm market, near-zero squared log-returns),
    // the spike-detector ratio vol_ref / vol_ema fires the moment any
    // realistic vol resumes — Kelly stays trapped at regime_scale_floor
    // until a regime change pushes vol_ref back up. Floor caps how small
    // "trained-regime baseline" can claim to be.
    const float floor_anchor = isv[vol_ref_floor_index];
    const float effective_floor = fmaxf(floor_anchor, 1.0e-12f);
    vol_ref_new = fmaxf(vol_ref_new, effective_floor);
    isv[vol_ref_index] = vol_ref_new;
    isv[vol_ref_samples_index] = sample_count_prev + 1.0f;

    // Per-cell running min of vol_obs, FILTERED for vol_obs > 1e-12. At
    // MBP-10 cadence many snapshots see no mid-price change (curr ==
    // prev → vol_obs = 0), so an unfiltered min would pin at 0 on the
    // first such bar and the floor controller would converge to 0. The
    // filter skips those artifact-zero observations and tracks the min
    // over "actually moved" steps — that's the signal the floor
    // controller needs.
    if (vol_obs > 1.0e-12f) {
        const float min_prev = isv[vol_obs_min_index];
        isv[vol_obs_min_index] = fminf(min_prev, vol_obs);
    }
}
