// crates/ml/src/cuda_pipeline/regret_signal_kernel.cu
//
// SP15 Phase 3.4 — regret/opportunity-cost signal = r_discipline content.
//
// When policy holds AND a trade WOULD have been profitable past
// `cost_t + trail_dist`:
//     regret_bar = ideal_pnl_missed - cost_t
// Otherwise:
//     regret_bar = 0  (no regret — either policy was active or the
//                     ideal trade did not clear the cost+trail floor).
//
// REGRET_EMA (slot 423) is updated via first-observation bootstrap (per
// pearl_first_observation_bootstrap — sentinel 0 replaces directly on the
// first non-zero observation) + simple exponential decay with α=0.05.
// Wiener-α adaptive smoothing per pearl_wiener_optimal_adaptive_alpha is
// a documented follow-up.
//
// The reward composer subtracts `LAMBDA_REGRET (slot 424) × REGRET_EMA`
// from r_discipline at the reward-split site in the deferred follow-up
// commit per feedback_no_partial_refactor.md (Phase 1.1-1.3 + 3.1-3.3
// precedent — kernel + launcher land first; consumer wiring follows).
//
// Single-thread, single-block (mirrors dd_penalty_kernel — no reduction).

extern "C" __global__ void regret_signal_kernel(
    int                 action,             /* 0 = Hold (regret-eligible) */
    float               ideal_pnl_missed,
    float               cost_t,
    float               trail_dist,
    float* __restrict__ isv,
    float* __restrict__ regret_bar_out
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;

    // Asymmetric gate: regret only fires when policy was Hold AND the
    // ideal trade cleared the cost+trail floor (so the lost opportunity
    // is real after slippage/trail-stop overhead).
    float regret_bar = 0.0f;
    if (action == 0 /* Hold */ && ideal_pnl_missed > (cost_t + trail_dist)) {
        regret_bar = ideal_pnl_missed - cost_t;
    }
    *regret_bar_out = regret_bar;

    // EMA update with first-observation bootstrap (sentinel 0 → first
    // non-zero observation replaces directly, per
    // pearl_first_observation_bootstrap). Wiener-α adaptive swap is the
    // documented follow-up.
    //
    // ISV slot indices (mirror dd_penalty_kernel's literal-index pattern;
    // matched against sp15_isv_slots.rs by sp15_slot_layout_locked).
    const float prev_ema = isv[/*REGRET_EMA_INDEX*/ 423];
    const float ALPHA = 0.05f;
    float new_ema;
    if (prev_ema == 0.0f && regret_bar > 0.0f) {
        // Bootstrap: sentinel zero → first non-zero observation replaces
        // directly so the EMA is not anchored to its boot-time zero.
        new_ema = regret_bar;
    } else {
        new_ema = prev_ema * (1.0f - ALPHA) + regret_bar * ALPHA;
    }
    isv[/*REGRET_EMA_INDEX*/ 423] = new_ema;

    __threadfence_system();
}
