// rl_cmdp_constraints_check.cu — Layer 1 hard CMDP gates.
//
// Spec: docs/superpowers/specs/2026-05-30-adaptive-risk-management-design.md
//
// Maintains the session-level + cooldown state that downstream consumers
// (actions_to_market_targets) read as override flags. Three independent
// risk constraints are tracked here:
//
//   1. Session pnl accumulation + DD limit (sticky `triggered` flag)
//   2. Cooldown counter decrement (after consecutive losses limit fires)
//   3. Consecutive-loss tracking (sets cooldown when limit hit)
//
// Max-open-units + net-inventory checks are evaluated PER BATCH inside
// actions_to_market_targets where per-batch unit state is available.
// This kernel handles only session-wide state.
//
// Per `feedback_no_atomicadd`: single-thread single-block, sums sequentially.
// Per `feedback_cpu_is_read_only`: pure device kernel.
// Per `feedback_no_partial_refactor`: ISV slots must match isv_slots.rs.
//
// Launch config:
//   grid  = (1, 1, 1)
//   block = (1, 1, 1)
//   smem  = 0
//   stream = main RL stream (sequenced AFTER rl_fused_reward_pipeline
//            writes realized_pnl / outcomes, BEFORE actions_to_market_targets
//            reads override flags).

#include <stdint.h>

#define RL_SESSION_PNL_USD_INDEX            662
#define RL_SESSION_DD_LIMIT_USD_INDEX       663
#define RL_SESSION_DD_TRIGGERED_INDEX       664
#define RL_CONSEC_LOSS_LIMIT_INDEX          666
#define RL_CONSEC_LOSS_COUNT_INDEX          667
#define RL_COOLDOWN_REMAINING_STEPS_INDEX   668
#define RL_COOLDOWN_DURATION_INDEX          669
#define RL_SESSION_PNL_WORST_INDEX          684

// Fleet-fraction observability (2026-06-01). Existing aggregates above
// emit the SINGLE WORST batch state, which at b=1024 stays pegged at
// the limit even when most batches are healthy. These fractions emit
// what proportion of the fleet is currently constrained, distinguishing
// "normal per-batch risk-management" from "fleet-wide absorbing trap".
#define RL_CMDP_FRAC_IN_COOLDOWN_INDEX        743
#define RL_CMDP_FRAC_CONSEC_NEAR_LIMIT_INDEX  744
#define RL_CMDP_FRAC_DD_TRIGGERED_INDEX       745
#define RL_CMDP_FRAC_SESSION_NEG_INDEX        746

// Edge-decay detector Phase 1 (2026-06-01). Page-Hinkley change-point
// detector on σ_welford-normalized per-trade PnL stream. Diagnostic-only
// (no behavior change). See spec
// docs/superpowers/specs/2026-06-01-edge-decay-detector-phase1-diagnostic.md.
//
// Kernel ordering: rl_cmdp_constraints_check fires BEFORE apply_reward_scale
// and rl_popart_normalize in step_with_lobsim_gpu_body. So rewards[b] at
// kernel entry is RAW per-step PnL (USD), NOT popart-scaled. The σ slot
// read is the PRIOR step's σ_welford (one-step lag). Slot 725 is the
// pre-envelope-floor Welford σ — chosen over slot 555 (σ_effective)
// because slot 555 spikes 1000× at the eval shock window, blinding the
// detector.
#define RL_POPART_SIGMA_WELFORD_INDEX         725
#define RL_EDGE_PH_TOLERANCE_INDEX            747
#define RL_EDGE_PH_THRESHOLD_INDEX            748
#define RL_EDGE_PH_WARMUP_MIN_INDEX           749
#define RL_EDGE_PH_MEAN_INDEX                 750
#define RL_EDGE_PH_FRAC_ALERTED_INDEX         751
#define RL_EDGE_PH_FRAC_WARMUP_INDEX          752

// Per-batch session-pnl + consec-loss tracking. Each batch element is an
// independent backtest "session" with its own $35k starting capital;
// `session_pnl_per_batch[b]` is the running PnL of that session and
// `consec_loss_per_batch[b]` is its losing-trade streak. The kernel
// writes per-batch state out for `actions_to_market_targets` to read
// (per-batch DD-triggered / cooldown gating) and also writes the
// canonical summary slots so diag + IQN-τ keep a single-account view.
extern "C" __global__ void rl_cmdp_constraints_check(
    float*       __restrict__ isv,
    const float* __restrict__ rewards,                       // [b_size] RAW per-step pnl (USD)
    const float* __restrict__ dones,                         // [b_size] 1.0 = close
    float*       __restrict__ session_pnl_per_batch,         // [b_size] IN/OUT
    float*       __restrict__ consec_loss_per_batch,         // [b_size] IN/OUT
    float*       __restrict__ session_dd_triggered_per_batch,// [b_size] IN/OUT (0/1)
    float*       __restrict__ cooldown_remaining_per_batch,  // [b_size] IN/OUT
    // Edge-decay Phase 1: Page-Hinkley per-batch state (5 buffers).
    // Zero-init at construction AND on every fold/eval boundary via
    // reset_session_state (predictive-reset discipline).
    float*       __restrict__ ph_mu_per_batch,               // [b_size] IN/OUT — pre-update Welford mean (σ-normalized y)
    float*       __restrict__ ph_count_per_batch,            // [b_size] IN/OUT — trades observed (f32)
    float*       __restrict__ ph_m_per_batch,                // [b_size] IN/OUT — Σ(y_i − μ̄_pre − δ) post-warmup
    float*       __restrict__ ph_mmin_per_batch,                // [b_size] IN/OUT — min(ph_m_b) so far post-warmup
    float*       __restrict__ ph_stat_per_batch,             // [b_size] IN/OUT — current ph_m − ph_M (the PH statistic)
    int                       b_size
) {
    if (threadIdx.x != 0 || threadIdx.y != 0 || threadIdx.z != 0) return;
    if (blockIdx.x  != 0 || blockIdx.y  != 0 || blockIdx.z  != 0) return;

    const float dd_limit    = isv[RL_SESSION_DD_LIMIT_USD_INDEX];
    const float consec_lim  = isv[RL_CONSEC_LOSS_LIMIT_INDEX];
    const float cool_dur    = isv[RL_COOLDOWN_DURATION_INDEX];

    // Edge-decay Phase 1: read PH config + σ_welford for normalization.
    // pop_sigma is clamped to 1e-6 to avoid div-by-zero in early bootstrap
    // (before any reward has been observed, σ_welford may still be 0).
    const float ph_delta      = isv[RL_EDGE_PH_TOLERANCE_INDEX];
    const float ph_lambda     = isv[RL_EDGE_PH_THRESHOLD_INDEX];
    const float ph_warmup_min = isv[RL_EDGE_PH_WARMUP_MIN_INDEX];
    const float pop_sigma     = fmaxf(isv[RL_POPART_SIGMA_WELFORD_INDEX], 1e-6f);

    // Summary aggregates (single-account view, written at end of loop).
    //
    // Fix B: IQN-τ reads `RL_SESSION_PNL_USD_INDEX`, so we expose the
    // MEAN-of-active-accounts there instead of the worst — one bad
    // account no longer drags the fleet's risk aversion to its floor.
    // The worst per-batch pnl mirrors to `RL_SESSION_PNL_WORST_INDEX`
    // for diag (helps spot fleet skew).
    float worst_pnl        = 0.0f;
    float worst_consec     = 0.0f;
    float any_dd_trig      = 0.0f;
    float max_cool_remain  = 0.0f;
    float active_pnl_sum   = 0.0f;
    int   n_active         = 0;

    // Fleet-fraction observability counters (2026-06-01). See header
    // block above for the falsification criteria these support.
    int   n_in_cooldown     = 0;  // batches with cooldown_remaining > 0
    int   n_consec_near_lim = 0;  // batches with consec >= limit - 1
    int   n_dd_triggered    = 0;  // batches with dd_triggered flag set
    int   n_session_neg     = 0;  // batches with session_pnl < 0

    // Edge-decay Phase 1 fleet-fraction counters.
    float ph_active_sum     = 0.0f;
    int   n_ph_active       = 0;
    int   n_ph_alerted      = 0;
    int   n_ph_warmup       = 0;

    for (int b = 0; b < b_size; ++b) {
        // Snapshot cooldown state BEFORE Section 1, so Section 2's
        // decrement only consumes what was already in flight from a
        // prior step. Without this, a fresh cooldown set in Section 1
        // would be immediately decremented in Section 2 same-step
        // (off-by-one — G1 caught this with cool=499 vs expected 500).
        const float cool_prev = cooldown_remaining_per_batch[b];

        // ── 1. Per-batch session pnl + DD check ─────────────────
        // Fix A: when DD trips, also START a recovery cooldown — sticky
        // dd_triggered without a recovery path leaves accounts dead for
        // the rest of the fold, leaking variance from popart σ and
        // pinning IQN-τ at its floor.
        const float pnl_new = session_pnl_per_batch[b] + rewards[b];
        session_pnl_per_batch[b] = pnl_new;
        const bool was_triggered = session_dd_triggered_per_batch[b] >= 0.5f;
        if (pnl_new < dd_limit && !was_triggered) {
            session_dd_triggered_per_batch[b] = 1.0f;
            cooldown_remaining_per_batch[b]   = cool_dur;  // recovery clock
        }

        // ── 2. Per-batch cooldown decrement + recovery reset ────
        // When the cooldown clock expires, give the account a fresh
        // start — matches [[feedback_surfer_philosophy_trading]]:
        // accept the wipeout, take the forced break, then get back on
        // the board. Otherwise the account contributes V_target ≈
        // γ·V(s'_unchanged) for the rest of the fold (zero-variance
        // dead-weight that compounds in popart σ).
        if (cool_prev > 0.0f) {
            const float cool_new = cool_prev - 1.0f;
            cooldown_remaining_per_batch[b] = cool_new;
            if (cool_new == 0.0f) {
                session_pnl_per_batch[b]          = 0.0f;
                session_dd_triggered_per_batch[b] = 0.0f;
                // consec already cleared at its own limit-trip path
            }
        }

        // ── 3. Per-batch consec-loss tracking on closes ─────────
        // r == 0 with done: ambiguous break-even, leave streak as-is.
        float consec = consec_loss_per_batch[b];
        if (dones[b] >= 0.5f) {
            const float r = rewards[b];
            if      (r < 0.0f) consec += 1.0f;
            else if (r > 0.0f) consec  = 0.0f;
        }
        if (consec >= consec_lim) {
            // Streak limit on THIS account — open its cooldown, reset
            // streak. Other accounts unaffected. Recovery reset above
            // will also clear it when the cooldown clock expires.
            cooldown_remaining_per_batch[b] = cool_dur;
            consec = 0.0f;
        }
        consec_loss_per_batch[b] = consec;

        // ── Aggregates ──────────────────────────────────────────
        // Active = post-recovery state (after the reset path above), so
        // accounts that just recovered this step count as active again.
        const float final_pnl       = session_pnl_per_batch[b];
        const float final_consec    = consec_loss_per_batch[b];
        const float final_cool      = cooldown_remaining_per_batch[b];
        const float final_triggered = session_dd_triggered_per_batch[b];
        if (final_pnl   < worst_pnl)    worst_pnl       = final_pnl;
        if (final_consec > worst_consec) worst_consec    = final_consec;
        if (final_triggered >= 0.5f)    any_dd_trig     = 1.0f;
        if (final_cool > max_cool_remain) max_cool_remain = final_cool;
        if (final_triggered < 0.5f) {
            active_pnl_sum += final_pnl;
            n_active       += 1;
        }

        // Fleet-fraction counters. consec_near_lim uses (limit - 1) as
        // the threshold since the kernel resets consec to 0 the moment
        // it hits the limit (line above), so we'd never observe consec
        // AT limit at the aggregation point. (limit - 1) catches "about
        // to trip" which is the diagnostic-relevant state.
        if (final_cool > 0.0f)                       n_in_cooldown += 1;
        if (final_consec >= consec_lim - 1.0f)       n_consec_near_lim += 1;
        if (final_triggered >= 0.5f)                 n_dd_triggered += 1;
        if (final_pnl < 0.0f)                        n_session_neg += 1;

        // ── Edge-decay Phase 1: Page-Hinkley state update ─────────
        // Updates ONLY on done events (close-trade gate, same as consec).
        // σ-normalized via σ_welford (slot 725); δ, λ are unit-less.
        // m, M only accumulate AFTER warmup completes (cnt_prev >= warmup_min)
        // to avoid cold-start Welford bias being baked into permanent offset.
        if (dones[b] >= 0.5f) {
            const float y        = rewards[b] / pop_sigma;
            const float mu_prev  = ph_mu_per_batch[b];
            const float cnt_prev = ph_count_per_batch[b];
            const float cnt      = cnt_prev + 1.0f;
            // Welford mean update always runs (post-update value stored).
            ph_mu_per_batch[b]    = mu_prev + (y - mu_prev) / cnt;
            ph_count_per_batch[b] = cnt;
            // PH cumulative + M gated behind warmup completion.
            // SIGN: detect mean DECREASE (edge decay). The cumulative
            // m = Σ(μ̄_pre − x_t − δ) grows when x_t < μ̄ − δ (signal is
            // below recent mean by more than tolerance → degradation).
            // The earlier formulation (x − μ̄ − δ) detects INCREASE which
            // is the opposite of what we want; first cluster smoke
            // (alpha-rl-n5x87 train step 1277) showed 77% of fleet
            // alerted during normal training improvement, confirming
            // the wrong-direction sign.
            if (cnt_prev >= ph_warmup_min) {
                const float m = ph_m_per_batch[b] + (mu_prev - y - ph_delta);
                ph_m_per_batch[b] = m;
                float M = ph_mmin_per_batch[b];
                if (m < M) {
                    ph_mmin_per_batch[b] = m;
                    M = m;
                }
                ph_stat_per_batch[b] = m - M;
            }
        }

        // Edge-decay fleet aggregates (every batch, every step — not gated
        // on done). Active = past warmup; alerted = active AND stat > λ.
        const float ph_stat_b = ph_stat_per_batch[b];
        const float ph_cnt_b  = ph_count_per_batch[b];
        if (ph_cnt_b < ph_warmup_min) {
            n_ph_warmup += 1;
        } else {
            n_ph_active   += 1;
            ph_active_sum += ph_stat_b;
            if (ph_stat_b > ph_lambda) n_ph_alerted += 1;
        }
    }

    // ── Canonical summary slots ────────────────────────────────────
    // Fix B: IQN-τ reads `RL_SESSION_PNL_USD_INDEX` — feed it the
    // mean-of-active-accounts so τ reflects the fleet's typical DD
    // rather than one outlier's catastrophic loss. Worst stays
    // visible for diag.
    const float mean_active = (n_active > 0)
        ? (active_pnl_sum / (float)n_active)
        : 0.0f;
    isv[RL_SESSION_PNL_USD_INDEX]          = mean_active;
    isv[RL_SESSION_PNL_WORST_INDEX]        = worst_pnl;
    isv[RL_SESSION_DD_TRIGGERED_INDEX]     = any_dd_trig;
    isv[RL_CONSEC_LOSS_COUNT_INDEX]        = worst_consec;
    isv[RL_COOLDOWN_REMAINING_STEPS_INDEX] = max_cool_remain;

    // Fleet-fraction emits. b_size guaranteed > 0 by the launch-config
    // assertion in the trainer (kernel returns early at threadIdx check
    // above if b_size == 0 was somehow let through).
    const float inv_b = 1.0f / (float)b_size;
    isv[RL_CMDP_FRAC_IN_COOLDOWN_INDEX]       = (float)n_in_cooldown * inv_b;
    isv[RL_CMDP_FRAC_CONSEC_NEAR_LIMIT_INDEX] = (float)n_consec_near_lim * inv_b;
    isv[RL_CMDP_FRAC_DD_TRIGGERED_INDEX]      = (float)n_dd_triggered * inv_b;
    isv[RL_CMDP_FRAC_SESSION_NEG_INDEX]       = (float)n_session_neg * inv_b;

    // Edge-decay Phase 1 fleet emits. ph_mean and frac_ph_alerted use
    // n_active denominator (NOT b_size) — only batches past warmup can
    // be alerted; before warmup completes the denominator would structurally
    // cap the alert rate. frac_ph_warmup uses b_size (proper fleet fraction).
    isv[RL_EDGE_PH_MEAN_INDEX]         = (n_ph_active > 0) ? (ph_active_sum / (float)n_ph_active) : 0.0f;
    isv[RL_EDGE_PH_FRAC_ALERTED_INDEX] = (n_ph_active > 0) ? ((float)n_ph_alerted / (float)n_ph_active) : 0.0f;
    isv[RL_EDGE_PH_FRAC_WARMUP_INDEX]  = (float)n_ph_warmup * inv_b;
}
