/* ══════════════════════════════════════════════════════════════════════════
 * SP20 Phase 3 Task 3.2 (2026-05-10) — Hold opportunity-cost dual emission
 * helpers (Component 2).
 *
 * Per spec `docs/superpowers/specs/2026-05-09-sp19-20-wr-first-design.md` §4.2.
 *
 * Two helpers shared between `experience_env_step` (production caller) and
 * the SP20 hold-baseline GPU oracle test wrapper:
 *
 *   1. `sp20_compute_per_bar_aux_conf_k2(logits)`:
 *        Per-row K=2 peak-confidence above the K=2 uniform mean.
 *        `aux_conf = max(softmax(logits)) - 0.5` ∈ [0, 0.5].
 *        Numerically-stable: subtract row-max before exp.
 *        Bit-identical to the formula in `sp20_stats_compute_kernel.cu`
 *        Pass A (lines 152-167) — the per-row aux_conf computation that
 *        feeds the batch p50/std reduction.
 *
 *   2. `sp20_sum_hold_baseline_over_trade(buf, env, hold_buffer_size,
 *                                          current_t, segment_hold_time)`:
 *        Sum the per-env circular `hold_baseline_buffer` over the most
 *        recent `min(hold_buffer_size, segment_hold_time)` bars BEFORE
 *        `current_t`. The trade-close bar's reward path doesn't write
 *        to the buffer (segment_complete bypasses the per-bar branches),
 *        so the most recent write was at `(current_t - 1) %
 *        hold_buffer_size`, walking backwards.
 *
 *        Layout: `buf[env * hold_buffer_size + (current_t - k) %
 *        hold_buffer_size]` for k = 1..=n_bars where
 *        `n_bars = min(hold_buffer_size, segment_hold_time)`.
 *
 *        `segment_hold_time` is captured BEFORE the trade-physics helper
 *        resets `PS_HOLD_TIME` to 0 (saved_hold_time at
 *        experience_kernels.cu:2618), so it correctly reflects the trade's
 *        duration.
 *
 *        Trades older than `hold_buffer_size` get UNDER-counted (only the
 *        most recent 30 bars are summed). Per spec §4.2 buffer-sizing
 *        note this is acceptable: "trades > 30 bars don't sample beyond
 *        their last 30 bars."
 *
 * ── Pearls + invariants ────────────────────────────────────────────────
 *  - `pearl_event_driven_reward_density_alignment` — Component 2's
 *    per-bar density alignment is intentional: every bar contributes a
 *    counterfactual cost ("what would Hold have paid"), and the trade
 *    consumer sums the trade's bars to subtract from the trade-close
 *    R_event. Density-aligned by construction.
 *  - `pearl_one_unbounded_signal_per_reward` — `aux_conf ∈ [0, 0.5]`
 *    (softmax-bounded), `cost_scale ∈ [0.01, 0.5]` (controller-bounded),
 *    so `per_bar_opp_cost = -aux_conf × cost_scale ∈ [-0.25, 0]`. Sum of
 *    30 such terms is in `[-7.5, 0]`. Bounded by construction.
 *  - `feedback_no_atomicadd` — both helpers are pure read/compute (no
 *    writes to shared memory or device memory).
 *  - `feedback_no_cpu_test_fallbacks` — both helpers are __device__ +
 *    __forceinline__ so the test wrapper can call them bit-for-bit.
 *  - `feedback_isv_for_adaptive_bounds` — `cost_scale` is read from
 *    `ISV[HOLD_COST_SCALE_INDEX]` by the caller; this header takes it
 *    as a scalar arg, decoupled from slot indices.
 *
 * ── Wiring contract ────────────────────────────────────────────────────
 *
 *   Per-bar emission (positioned-non-event AND flat-non-event branches):
 *     - Read `aux_conf_i = sp20_compute_per_bar_aux_conf_k2(...)` once.
 *     - Read `cost_scale = ISV[HOLD_COST_SCALE_INDEX]`.
 *     - Compute `per_bar_opp_cost = -aux_conf × cost_scale`.
 *     - Write Path 2 (counterfactual): `hold_baseline_buffer[env *
 *       hold_buffer_size + current_t % hold_buffer_size] = per_bar_opp_cost`
 *       (always, regardless of action).
 *     - Write Path 1 (real reward): if `dir_idx == DIR_HOLD`,
 *       `r_micro_or_opp_cost += per_bar_opp_cost - ISV[HOLD_REWARD_EMA_INDEX]`
 *       (centered for Q-target stability).
 *
 *   Trade-close consumer (segment_complete branch):
 *     - `hold_baseline = sp20_sum_hold_baseline_over_trade(...)` —
 *       replaces the Phase 2 Task 2.2 placeholder `hold_baseline = 0.0f`.
 *     - `alpha = R_event - hold_baseline` continues to feed
 *       `alpha_per_env[env]` for the SP20 aggregation kernel.
 *
 * ══════════════════════════════════════════════════════════════════════════ */

#ifndef SP20_HOLD_BASELINE_CUH
#define SP20_HOLD_BASELINE_CUH

#include <cuda_runtime.h>

/* K=2 uniform softmax mean. Subtracted from peak softmax to produce
 * `aux_conf ∈ [0, 0.5]`. Exactly representable in f32. */
#define SP20_HOLD_BASELINE_UNIFORM_K2 0.5f

/* Per-row K=2 peak-confidence: `max(softmax(logits)) - 0.5`. Numerically
 * stable via row-max subtraction. Returns f32 in [0, 0.5]. */
__device__ __forceinline__ float sp20_compute_per_bar_aux_conf_k2(
    const float* __restrict__ logits_row     /* [K=2] f32 */
) {
    const float l0 = logits_row[0];
    const float l1 = logits_row[1];
    const float m  = fmaxf(l0, l1);
    const float e0 = __expf(l0 - m);
    const float e1 = __expf(l1 - m);
    const float denom = e0 + e1;
    /* denom >= 1.0f always (the larger of e0/e1 is exp(0) = 1.0
     * after row-max subtract), so no division-by-zero guard needed. */
    const float sm0 = e0 / denom;
    const float sm1 = e1 / denom;
    const float max_sm = fmaxf(sm0, sm1);
    return max_sm - SP20_HOLD_BASELINE_UNIFORM_K2;
}

/* Sum over a per-env trade range of the circular `hold_baseline_buffer`.
 *
 * Args:
 *   buf                — [N_envs × hold_buffer_size] f32 row-major.
 *   env                — env index (row).
 *   hold_buffer_size   — circular ring size (spec §4.2: 30).
 *   current_t          — current bar (the trade-close bar — its OWN slot
 *                        was NOT written by any per-bar branch because
 *                        segment_complete bypasses them).
 *   segment_hold_time  — float (saved_hold_time captured before
 *                        PS_HOLD_TIME reset). The trade's bar count.
 *
 * Returns:
 *   Sum of `buf[env, (current_t - k) % hold_buffer_size]` for
 *   k = 1..=min(hold_buffer_size, floor(segment_hold_time)).
 *   Walks backwards from `current_t - 1` (the most recent per-bar write).
 *
 * Pearl note: kernel is per-env-parallel. Each thread owns exactly one
 * env's row; no atomic / cross-thread reads. The modulo arithmetic is
 * scalar so the loop is sequential per thread (cheap — at most 30
 * iterations). */
__device__ __forceinline__ float sp20_sum_hold_baseline_over_trade(
    const float* __restrict__ buf,
    int    env,
    int    hold_buffer_size,
    int    current_t,
    float  segment_hold_time
) {
    /* Defensive guard: empty trade or zero buffer ⇒ no contribution. */
    if (hold_buffer_size <= 0) return 0.0f;
    if (segment_hold_time <= 0.0f) return 0.0f;

    /* Bound the back-walk. Cast through int for the loop counter; the
     * kernel-side caller passes `(int)segment_hold_time` semantics
     * already (saved_hold_time is `(float)hold_time` at the trade-close
     * branch — non-negative integer values stored as f32). */
    int n_bars = (int)segment_hold_time;
    if (n_bars > hold_buffer_size) n_bars = hold_buffer_size;

    /* Modulo arithmetic for negative numerators is implementation-
     * defined in C / undefined for very negative values; bring the
     * starting index into the positive range first. The expression
     *   ((current_t - k) % hold_buffer_size + hold_buffer_size)
     *       % hold_buffer_size
     * computes the canonical non-negative modulo result. We pre-add
     * hold_buffer_size once before the loop so each iteration just
     * decrements. */
    const long long row_off = (long long)env * (long long)hold_buffer_size;
    int idx = (current_t - 1) % hold_buffer_size;
    if (idx < 0) idx += hold_buffer_size;

    float sum = 0.0f;
    for (int k = 0; k < n_bars; ++k) {
        sum += buf[row_off + idx];
        --idx;
        if (idx < 0) idx += hold_buffer_size;
    }
    return sum;
}

#endif /* SP20_HOLD_BASELINE_CUH */
