// crates/ml/src/cuda_pipeline/cooldown_kernel.cu
//
// SP15 Phase 3.5.3 — cooldown gate.
//
// After K consecutive losing trades, force Hold for M bars. K and M are
// ISV-tracked (slots 433/434); initial sentinels K=5, M=20 hardcoded in
// the trainer constructor. Counter (COOLDOWN_BARS_REMAINING slot 435)
// decremented per bar — part of state, so the model can reason about it.
//
// Per spec §9.2 (3.5.3) post-amendment-2 fix: `consec_losses` streak
// counter is updated only on `trade_close_event != 0`. Per-bar
// non-close calls just decrement the cooldown counter without touching
// the loss tracker. This separates the per-bar cadence (decrement) from
// the per-trade cadence (streak update) — only trade closes can extend
// or reset the streak.
//
// MEDIAN_STREAK_LENGTH (slot 442) producer + ISV-driven K formula
// reading the rolling median of consecutive-loss streak lengths via the
// two-heap algorithm are documented Phase 3.5.3 follow-up sub-tasks
// per `feedback_isv_for_adaptive_bounds.md`. The ISV-driven M from
// vol_normalizer time-to-mean-reversion is a parallel follow-up.
//
// `consecutive_losses` is a persistent host-allocated 1-element f32
// scratch buffer on the trainer struct (`sp15_cooldown_consecutive_losses`),
// initialised to 0.0 in the constructor and reset to 0.0 at fold
// boundary via `host_slice_mut().fill(0.0)` (mirrors the Phase 3.1
// `sp15_alpha_warm_count` non-ISV mapped-pinned reset pattern). f32
// representation is exact for any reachable streak length because the
// kernel only performs `+= 1.0f` increments and `= 0.0f` resets.
//
// Hard rules:
//
//   * `feedback_no_atomicadd` — single-thread, single-block, pure
//     scalar arithmetic; no reductions, no `atomicAdd`.
//   * `feedback_no_partial_refactor` — kernel + launcher land first;
//     consumer wiring (action selection forces Hold while
//     `cooldown_remaining > 0`) deferred to a follow-up atomic commit
//     per the established Phase 1.1-1.5 / 3.1-3.5.2 precedent.
//   * `feedback_no_stubs` — kernel returns the actual updated state;
//     the three oracle tests verify trip-after-K-losses,
//     streak-broken-by-win, and decrement-on-non-trade-bar behaviors.
//   * `feedback_isv_for_adaptive_bounds` — K and M are ISV-tracked
//     (slots 433/434); the producers driving them are deferred but the
//     kernel reads them rather than hardcoded literals so the follow-up
//     swap is a no-op at the consumer.
//
// Single-thread, single-block (mirrors `dd_penalty_kernel` /
// `regret_signal_kernel` / `hold_floor_kernel` /
// `dd_asymmetric_reward_kernel` — no reduction).

extern "C" __global__ void cooldown_kernel(
    int                 last_trade_outcome_loss,   /* 1 if last trade was a loss, 0 otherwise */
    int                 trade_close_event,         /* 1 on bars where a trade closed */
    float* __restrict__ isv,
    float* __restrict__ consecutive_losses         /* persistent state, f32 representing int */
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;

    // Track consecutive losses on trade-close events only. Per-bar
    // non-close calls leave the streak counter untouched per spec
    // post-amendment-2 fix (decrement-only path below).
    if (trade_close_event != 0) {
        if (last_trade_outcome_loss != 0) {
            *consecutive_losses += 1.0f;
        } else {
            *consecutive_losses = 0.0f;   // streak broken by a win
        }
    }

    // Read K and M (ISV-driven; producer kernels deferred to follow-up
    // sub-tasks per `feedback_isv_for_adaptive_bounds.md`). Floor K at 2
    // and M at 1 to defang accidental ISV under-write — at K<2 the gate
    // would fire on every loss, at M<1 the cooldown would be a no-op.
    const float K_f = isv[/*COOLDOWN_K_THRESHOLD_INDEX*/ 433];
    const float M_f = isv[/*COOLDOWN_M_BARS_INDEX*/     434];
    const float K = fmaxf(2.0f, K_f);
    const float M = fmaxf(1.0f, M_f);

    float cooldown_remaining = isv[/*COOLDOWN_BARS_REMAINING_INDEX*/ 435];

    // Trigger: K consecutive losses → start cooldown (only on
    // trade-close events when consec_losses crosses K AND cooldown is
    // not already active — re-arming mid-cooldown would extend the gate
    // every bar a closed trade landed during the freeze, which is not
    // the spec). Reset the streak counter after firing so the next
    // streak starts from 0.
    if (trade_close_event != 0 &&
        *consecutive_losses >= K &&
        cooldown_remaining <= 0.0f) {
        cooldown_remaining = M;
        *consecutive_losses = 0.0f;
    }

    // Decrement (every bar, regardless of trade-close). The trigger
    // above runs first so the same call that fires the gate also
    // immediately consumes one bar of cooldown — bias downward by 1 is
    // acceptable per the test's [18, 20] tolerance and avoids the
    // bookkeeping branch needed to skip the decrement on the trip step.
    if (cooldown_remaining > 0.0f) {
        cooldown_remaining -= 1.0f;
    }
    isv[/*COOLDOWN_BARS_REMAINING_INDEX*/ 435] = cooldown_remaining;
    __threadfence_system();
}
