/* ══════════════════════════════════════════════════════════════════════════
 * SP14 Layer C Phase C.4b — adaptive aux prediction horizon producer
 * (2026-05-08).
 *
 * Drives `ISV[AUX_PRED_HORIZON_BARS_INDEX=450]` (the H consumed by the
 * aux next-bar sign label kernels) from observed avg winning hold time
 * `ISV[AVG_WIN_HOLD_TIME_BARS_INDEX=451]` (produced upstream by
 * `avg_win_hold_time_update_kernel`).
 *
 * Algorithm (per `pearl_first_observation_bootstrap.md` +
 * `pearl_wiener_optimal_adaptive_alpha.md`):
 *
 *   1. If `h_target <= 0`  → no winning trade observed yet. Keep
 *      sentinel H=60.0 (≈1hr @ 1-min bars). This is the cold-start guard
 *      per `pearl_cold_start_exit_signal_or.md` — without it, a sentinel
 *      `h_target == 0.0` would clamp to h_min=1 on the bootstrap branch.
 *
 *   2. If `|h_current - sentinel_h| < 1e-6f` → first valid observation.
 *      REPLACE H with `clamp(h_target, h_min, h_max)` directly (Pearl-A
 *      first-observation bootstrap). NO blend — the sentinel must not
 *      contaminate the EMA.
 *
 *   3. Otherwise → steady-state Wiener-α EMA blend. Since no variance
 *      EMA exists for the target signal, falls back to fixed slow blend
 *      α=0.01 (per `pearl_wiener_optimal_adaptive_alpha.md` fallback).
 *      H is slow-moving (avg winning hold time changes on epoch scale,
 *      not step scale), so a slow EMA is appropriate — would otherwise
 *      track noise.
 *
 * Pearls + invariants:
 *
 *   - `pearl_no_host_branches_in_captured_graph.md` — single-thread
 *     kernel (one block, one thread). The launcher in `gpu_aux_trunk.rs`
 *     pre-loads the `CudaFunction` once at construction; no runtime
 *     branch on host. Per-epoch boundary launch (horizon is slow-moving;
 *     per-step would be wasteful).
 *
 *   - `feedback_no_atomicadd.md` — single-thread compute, no atomics.
 *
 *   - `feedback_isv_for_adaptive_bounds.md` — `h_min=1, h_max=240` are
 *     fundamental floors/ceilings (cannot predict the past; rollout
 *     buffer guard), NOT tuning parameters. Acceptable as constants.
 *
 *   - `feedback_no_stubs.md` — full body, no placeholder return.
 *
 * Args:
 *   isv                  — `[ISV_TOTAL_DIM]` mapped-pinned f32, modified
 *                          in place at index `isv_h_idx`.
 *   isv_h_idx            — AUX_PRED_HORIZON_BARS_INDEX (450).
 *   isv_h_target_idx     — AVG_WIN_HOLD_TIME_BARS_INDEX (451) — read-only
 *                          source slot.
 *   isv_h_target_var_idx — variance EMA slot for the target signal, or
 *                          `-1` if no variance EMA exists (uses fixed
 *                          α=0.01). Currently `-1` — Phase C.4b does
 *                          not introduce a target-variance EMA.
 *   h_min, h_max         — fundamental bounds (1.0, 240.0).
 *   sentinel_h           — sentinel value (60.0) used to detect first
 *                          observation. Must match
 *                          `SENTINEL_AUX_PRED_HORIZON_BARS` in
 *                          `sp14_isv_slots.rs`.
 * ══════════════════════════════════════════════════════════════════════════ */
extern "C" __global__
void aux_horizon_update(
    float* __restrict__ isv,
    int   isv_h_idx,
    int   isv_h_target_idx,
    int   isv_h_target_var_idx,  /* -1 if not available */
    float h_min,
    float h_max,
    float sentinel_h)
{
    /* Single-thread kernel: only thread (0, 0) does the compute. The
     * extra threads early-return so the launch dim can stay (1, 1, 1)
     * without branching on the host side. */
    if (blockIdx.x != 0 || threadIdx.x != 0) return;

    float h_target  = isv[isv_h_target_idx];
    float h_current = isv[isv_h_idx];

    /* Guard 1: no winning trade observed yet. Keep sentinel. */
    if (h_target <= 0.0f) {
        isv[isv_h_idx] = sentinel_h;
        return;
    }

    /* Guard 2: Pearl-A first-observation bootstrap. The current value is
     * exactly the sentinel ⇒ this is the first valid h_target observation;
     * REPLACE directly (no blend) per
     * `pearl_first_observation_bootstrap.md`. */
    if (fabsf(h_current - sentinel_h) < 1e-6f) {
        float h_clamped = fmaxf(h_min, fminf(h_max, h_target));
        isv[isv_h_idx] = h_clamped;
        return;
    }

    /* Steady-state EMA blend. */
    float alpha;
    if (isv_h_target_var_idx >= 0) {
        /* Wiener-α: α = sample_var / (sample_var + diff_var + ε).
         * Bounded into [0.001, 0.1] so a single noisy sample can never
         * fully overwrite the EMA, and a perfectly steady target can
         * still adapt. Currently unreachable — Phase C.4b passes -1. */
        float sample_var = isv[isv_h_target_var_idx];
        float diff       = h_target - h_current;
        float diff_var   = diff * diff;  /* single-sample approximation */
        alpha = sample_var / (sample_var + diff_var + 1e-6f);
        alpha = fmaxf(0.001f, fminf(0.1f, alpha));
    } else {
        /* No variance EMA available — fixed slow blend. The horizon
         * tracks avg winning hold time which itself is an EMA, so a
         * second-order slow blend (α=0.01) suffices to filter
         * remaining sample-to-sample noise. */
        alpha = 0.01f;
    }

    float h_blended = (1.0f - alpha) * h_current + alpha * h_target;
    isv[isv_h_idx] = fmaxf(h_min, fminf(h_max, h_blended));
}
