/* ══════════════════════════════════════════════════════════════════════════
 * Class A P0-A — adaptive REWARD_POS_CAP / REWARD_NEG_CAP producer (2026-05-08).
 *
 * Replaces the hardcoded `REWARD_POS_CAP=+5.0f` / `REWARD_NEG_CAP=-10.0f`
 * (state_layout.cuh:266-267) with ISV-driven adaptive bounds derived from
 * the realized winning-trade return distribution.
 *
 * Why this matters (per Class A audit, the highest-suspected-impact fix
 * for the months-long WR-stuck-at-46-48% plateau across 11 superprojects):
 *
 *     EV(trade) = WR × min(realized_win, +5)
 *               + (1−WR) × max(realized_loss, −10)
 *
 * With WR ≈ 0.46, the cap asymmetry is fully active. Wins clip to +5
 * regardless of magnitude. Controller signals (sharpe EMA, var_q,
 * q_gap) all derive from this CAPPED buffer, so the controller cannot
 * select for trades it cannot see. Selectivity gradient evaporates —
 * small wins clip to +5 alongside large wins also clipping to +5.
 *
 * The state_layout.cuh comment block (lines 261-264) explicitly
 * deferred Phase 2 (ISV-driven) "IF Phase 1 validation reveals
 * adaptive need". Phase 1 has been running across 11 SPs without
 * budging WR — adaptive need is revealed.
 *
 * Algorithm:
 *
 *   Phase 1 (block-tree-reduce): each block of `BLK_DIM` threads sweeps
 *   its tile of N=B*T samples, masking by `trade_close_per_sample[i]`
 *   and partitioning by `step_ret_per_sample[i] > 0` (winning) vs
 *   `< 0` (losing). Local block reduce in shared memory accumulates a
 *   per-block sum-of-squares over winning returns AND a count, plus a
 *   max-of-winning-returns. Single-block kernel — the per-epoch sample
 *   buffer is bounded (≤ ~64k slots).
 *
 *   Phase 2 (Pearl-A + EMA, single thread): if no winning trades
 *   observed, keep ISV slots unchanged (zero contributors → no signal).
 *   The "p99 estimator" without an actual histogram is the
 *   one-step-of-Welford rule of thumb:
 *
 *       p99(winning_returns) ≈ mean + Z_99 × sigma   (Z_99 ≈ 2.326)
 *
 *   for a roughly-normal positive tail, OR `max(winning_returns)` for
 *   small-N regimes, whichever is larger. We do NOT histogram (no
 *   atomicAdd; would also need full sample-buffer materialisation in
 *   shmem). The mean+sigma estimator with the conservative max
 *   takeover handles both heavy-tail outliers (max wins) and the
 *   well-behaved bulk (Gaussian-ish wins). Per
 *   `feedback_no_atomicadd.md`.
 *
 *   POS_target = max(p99_estimate, max_win) × safety_factor (1.5×)
 *
 *   NEG_target = − REWARD_NEG_TO_POS_RATIO × POS_target  (= −2 × POS,
 *   preserving Kahneman/Tversky loss-aversion asymmetry per
 *   `pearl_audit_unboundedness_for_implicit_asymmetry`). Asymmetry
 *   stays — moved from hardcoded scalar to producer-time multiplier.
 *   SINGLE SOURCE OF TRUTH: every consumer reads its corresponding
 *   ISV slot, no consumer applies the 2× ratio itself.
 *
 *   Pearl-A first-observation bootstrap: if `current_pos == sentinel`
 *   (5.0 within 1e-6f), REPLACE with target directly. Otherwise EMA
 *   blend with α=REWARD_CAP_EMA_ALPHA (0.01, slow — reward
 *   distribution is the foundation of training and shouldn't move
 *   fast). Per `pearl_first_observation_bootstrap.md`.
 *
 *   Bounds: POS clamped to [REWARD_POS_CAP_MIN=1.0, REWARD_POS_CAP_MAX=50.0]
 *   (Category-1 dimensional safety per `feedback_isv_for_adaptive_bounds`).
 *   NEG = −2 × POS by construction; resulting NEG ∈ [−100, −2].
 *
 * Pearls + invariants:
 *
 *   - `feedback_no_atomicadd.md` — single-block kernel; block-tree-reduce
 *     in shmem; no atomics across blocks.
 *
 *   - `pearl_no_host_branches_in_captured_graph.md` — `CudaFunction`
 *     pre-loaded at construction; on-device guards for "no winning
 *     trades" and Pearl-A sentinel detection.
 *
 *   - `pearl_first_observation_bootstrap.md` — first valid sample
 *     replaces sentinel directly; no blend.
 *
 *   - `feedback_no_stubs.md` — full body, no return-zero placeholders.
 *
 *   - `feedback_isv_for_adaptive_bounds.md` — POS/NEG ranges
 *     [1, 50] / [−100, −2] are dimensional safety bounds, NOT tuning.
 *
 *   - `pearl_symmetric_clamp_audit.md` — bilateral `fmaxf(lo, fminf(x, hi))`
 *     clamp on POS_target before writing the ISV slot. NEG ratio
 *     applied AFTER clamp on POS, then NEG bilaterally clamped to its
 *     own [−100, −2] range as a defensive layer.
 *
 * Args:
 *   step_ret_per_sample          — `[total_samples]` device f32; raw
 *                                  signed per-step return populated by
 *                                  `unified_env_step_core` in
 *                                  `experience_kernels.cu` (line ~2779).
 *   trade_close_per_sample       — `[total_samples]` device i32; 1 iff
 *                                  exiting_trade || reversing_trade
 *                                  (`experience_kernels.cu` line ~2780).
 *   total_samples                — N*L (B*T total samples in the epoch).
 *   isv                          — `[ISV_TOTAL_DIM]` device f32, modified
 *                                  in place at indices `pos_idx` and
 *                                  `neg_idx`.
 *   pos_idx                      — REWARD_POS_CAP_ADAPTIVE_INDEX (452).
 *   neg_idx                      — REWARD_NEG_CAP_ADAPTIVE_INDEX (453).
 *   sentinel_pos                 — SENTINEL_REWARD_POS_CAP (5.0).
 *   sentinel_neg                 — SENTINEL_REWARD_NEG_CAP (-10.0).
 *   safety_factor                — REWARD_CAP_SAFETY_FACTOR (1.5).
 *   neg_to_pos_ratio             — REWARD_NEG_TO_POS_RATIO (2.0).
 *   pos_min                      — REWARD_POS_CAP_MIN (1.0).
 *   pos_max                      — REWARD_POS_CAP_MAX (50.0).
 *   alpha                        — REWARD_CAP_EMA_ALPHA (0.01, slow).
 *
 * Launch: grid=(1, 1, 1), block=(BLK_DIM=256, 1, 1).
 * Shared memory: 4 * BLK_DIM floats:
 *   sh_sum_pos    — sum of positive (winning) returns
 *   sh_sumsq_pos  — sum-of-squares of positive returns
 *   sh_count_pos  — count of positive returns
 *   sh_max_pos    — max of positive returns
 * The count is summed as float for unified shmem layout — values are
 * bounded by total_samples ≤ 2^16 so f32 representation is exact for
 * counts up to 2^24.
 * ══════════════════════════════════════════════════════════════════════════ */

#include <cuda_runtime.h>

#define BLK_DIM 256
#define EPS_F   1e-6f
/* Z-score for ~p99 of a normal distribution (one-sided). Used as a
 * lightweight estimator: p99 ≈ mean + Z_99 × sigma. */
#define Z_99    2.326f

extern "C" __global__
void reward_cap_update(
    const float* __restrict__ step_ret_per_sample,
    const int*   __restrict__ trade_close_per_sample,
    int          total_samples,
    float* __restrict__ isv,
    int          pos_idx,
    int          neg_idx,
    float        sentinel_pos,
    float        sentinel_neg,
    float        safety_factor,
    float        neg_to_pos_ratio,
    float        pos_min,
    float        pos_max,
    float        alpha)
{
    /* Single-block kernel — only block 0 does anything. */
    if (blockIdx.x != 0) return;

    extern __shared__ float shmem[];
    float* sh_sum_pos    = shmem;                       /* [BLK_DIM] */
    float* sh_sumsq_pos  = shmem + BLK_DIM;             /* [BLK_DIM] */
    float* sh_count_pos  = shmem + 2 * BLK_DIM;         /* [BLK_DIM] */
    float* sh_max_pos    = shmem + 3 * BLK_DIM;         /* [BLK_DIM] */

    int tid = threadIdx.x;

    /* Phase 1a: stride-based sweep — accumulate stats on closed-trade
     * winning returns only. Losing closes are NOT stored (we don't
     * adapt the negative cap from realized losses; the 2:1 ratio is
     * spec'd-asymmetric, not data-driven). */
    float local_sum    = 0.0f;
    float local_sumsq  = 0.0f;
    float local_count  = 0.0f;
    float local_max    = 0.0f;
    for (int i = tid; i < total_samples; i += BLK_DIM) {
        int   is_close = trade_close_per_sample[i];     /* 0 or 1 */
        float ret      = step_ret_per_sample[i];        /* signed */
        if (is_close != 0 && ret > 0.0f) {
            local_sum    += ret;
            local_sumsq  += ret * ret;
            local_count  += 1.0f;
            if (ret > local_max) local_max = ret;
        }
    }
    sh_sum_pos[tid]    = local_sum;
    sh_sumsq_pos[tid]  = local_sumsq;
    sh_count_pos[tid]  = local_count;
    sh_max_pos[tid]    = local_max;
    __syncthreads();

    /* Phase 1b: block-tree-reduce (no atomicAdd) — halve every iter.
     * Sum/sumsq/count fold via addition; max folds via fmaxf. */
    for (int s = BLK_DIM >> 1; s > 0; s >>= 1) {
        if (tid < s) {
            sh_sum_pos[tid]    += sh_sum_pos[tid + s];
            sh_sumsq_pos[tid]  += sh_sumsq_pos[tid + s];
            sh_count_pos[tid]  += sh_count_pos[tid + s];
            sh_max_pos[tid]     = fmaxf(sh_max_pos[tid], sh_max_pos[tid + s]);
        }
        __syncthreads();
    }

    /* Phase 2: thread 0 finalises EMA + Pearl-A + clamp + writes BOTH slots. */
    if (tid != 0) return;

    float total_sum    = sh_sum_pos[0];
    float total_sumsq  = sh_sumsq_pos[0];
    float total_count  = sh_count_pos[0];
    float total_max    = sh_max_pos[0];

    /* Guard: no winning trades observed this epoch — keep ISV slots
     * unchanged (cold-start sentinel persists; consumers fall back to
     * `state_layout.cuh` constants per the cold-start behavior). */
    if (total_count <= 0.0f) {
        return;
    }

    /* Welford-rule p99 estimator: mean + Z_99 × sigma. Sigma is
     * computed as `sqrt(max(0, sumsq/n − mean²))` — the inner max
     * absorbs floating-point under-flow when all observations are
     * (near-)identical. */
    float mean         = total_sum / total_count;
    float variance     = fmaxf(0.0f, total_sumsq / total_count - mean * mean);
    float sigma        = sqrtf(variance);
    float p99_estimate = mean + Z_99 * sigma;

    /* Conservative takeover by realized max: in small-N or heavy-tail
     * regimes the parametric estimate underestimates the true tail.
     * Using `max(p99_estimate, total_max)` ensures the cap does NOT
     * actively clip any observed winning return. */
    float pos_target = fmaxf(p99_estimate, total_max) * safety_factor;

    /* Clamp POS to dimensional-safety bounds [pos_min, pos_max].
     * Bilateral per `pearl_symmetric_clamp_audit.md`. */
    pos_target = fmaxf(pos_min, fminf(pos_target, pos_max));

    /* Pearl-A first-observation bootstrap on POS slot. The sentinel is
     * exactly SENTINEL_REWARD_POS_CAP=5.0; any value within EPS_F of
     * the sentinel is treated as sentinel. (Direct equality on f32
     * literals would also work, but `|x − sentinel| < eps` is the
     * established pattern for ISV slot bootstrap detection.) */
    float current_pos  = isv[pos_idx];
    float blended_pos;
    if (fabsf(current_pos - sentinel_pos) < EPS_F) {
        blended_pos = pos_target;
    } else {
        blended_pos = (1.0f - alpha) * current_pos + alpha * pos_target;
    }
    /* Re-clamp post-blend (defensive — handles malformed prior state
     * outside [pos_min, pos_max]). */
    blended_pos = fmaxf(pos_min, fminf(blended_pos, pos_max));

    /* NEG cap = −neg_to_pos_ratio × POS cap. Asymmetry preserved at
     * producer-time, single source of truth. NEG bounds derived from
     * POS bounds × ratio: NEG ∈ [−ratio × pos_max, −ratio × pos_min] =
     * [−100, −2] for the production constants. The bilateral clamp
     * here is a defensive layer (POS clamp above already guarantees
     * NEG sits within its derived range). */
    float blended_neg  = -neg_to_pos_ratio * blended_pos;
    float neg_lo       = -neg_to_pos_ratio * pos_max;   /* −100 */
    float neg_hi       = -neg_to_pos_ratio * pos_min;   /* −2 */
    blended_neg        = fmaxf(neg_lo, fminf(blended_neg, neg_hi));

    /* Suppress unused-parameter warning for sentinel_neg — kept in the
     * signature for symmetry with the POS sentinel and to allow a
     * future Pearl-A variant on the NEG slot if the 2:1 ratio
     * spec ever evolves. */
    (void)sentinel_neg;

    isv[pos_idx] = blended_pos;
    isv[neg_idx] = blended_neg;
}
