/* ══════════════════════════════════════════════════════════════════════════
 * SP14 β-migration step 3 — per-rollout-step variant of aux_sign_label_kernel
 * (2026-05-07). SP14 Layer C Phase C.4b (2026-05-08): migrated to
 * ISV-driven multi-bar horizon `(p_{t+H} > p_t)` where H is read from
 * `ISV[isv_h_idx = AUX_PRED_HORIZON_BARS_INDEX]` (slot 450). Migration
 * MUST happen atomically with the trajectory kernel
 * (`aux_sign_label_kernel.cu`) per `feedback_no_partial_refactor.md` —
 * if only one is migrated, training-time and eval-time labels diverge.
 *
 * The full collector-end `aux_sign_label_kernel` (this directory) writes
 * the entire trajectory's labels at the end of `collect_experiences_gpu`
 * (after the timestep loop). The β migration's collector-native EGF
 * producer chain needs the labels per rollout step — only the current
 * step's slice of `n_episodes` envs, derived from
 * `bar = episode_starts[ep] + t` per env.
 *
 * Same per-thread O(1) map as the trajectory kernel: read `targets[bar*6+2]`
 * (raw_close column — see `dt_kernels.cu:803` for the canonical column
 * layout: 0=preproc_close, 1=preproc_next, 2=raw_close, 3=raw_next,
 * 4=raw_open, 5=mid_open) at `bar` and `bar+H`, classify by
 * strict greater-than. Skip sentinel -1 fires when the lookahead window
 * runs past `total_bars`.
 *
 * H clamping: `H = clamp(round(isv[isv_h_idx]), 1, 240)` — same as
 * trajectory variant per `feedback_isv_for_adaptive_bounds.md`.
 *
 * Pure map; no atomicAdd, no reduction, no shared memory — block
 * tree-reduce N/A here per `feedback_no_atomicadd.md`. GPU-only data
 * read/write per `feedback_cpu_is_read_only.md`. Inputs are mapped-pinned
 * (targets, isv) or device-allocated (episode_starts, out_labels) per
 * `feedback_no_htod_htoh_only_mapped_pinned.md`.
 *
 * Launch config: grid=(ceil(n_episodes / 256)), block=(256). Mirrors the
 * trajectory variant.
 *
 * Args:
 *   targets        — `[total_bars, 6]` mapped-pinned f32.
 *   episode_starts — `[n_episodes]` device i32, base bar index per env.
 *   t              — current rollout step (0..timesteps).
 *   isv            — `[ISV_TOTAL_DIM]` mapped-pinned f32.
 *   isv_h_idx      — AUX_PRED_HORIZON_BARS_INDEX (450).
 *   out_labels     — `[n_episodes]` device i32; receives -1/0/1 per env.
 *   n_episodes     — number of envs in this rollout slice.
 *   total_bars     — total bars in the data series.
 * ══════════════════════════════════════════════════════════════════════════ */
extern "C" __global__
void aux_sign_label_per_step_kernel(
    const float* __restrict__ targets,
    const int*   __restrict__ episode_starts,
    int          t,
    const float* __restrict__ isv,
    int          isv_h_idx,
    int*         __restrict__ out_labels,
    int n_episodes,
    int total_bars)
{
    int ep = blockIdx.x * blockDim.x + threadIdx.x;
    if (ep >= n_episodes) return;

    /* Read H from ISV (broadcast value); clamp [1, 240]. */
    int H = (int)__float2int_rn(isv[isv_h_idx]);
    if (H < 1)   H = 1;
    if (H > 240) H = 240;

    int bar = episode_starts[ep] + t;
    if (bar < 0 || bar + H >= total_bars) {
        out_labels[ep] = -1;  // skip sentinel — no lookahead window available
        return;
    }

    float p_now = targets[bar * 6 + 2];
    float p_fut = targets[(bar + H) * 6 + 2];

    // Strict greater-than tie-break: matches trajectory kernel — flat/down
    // both map to "down"=0 because the K=2 head can only distinguish
    // "up" from "not-up".
    out_labels[ep] = (p_fut > p_now) ? 1 : 0;
}
