// rl_surfer_scaffold_controller.cu — adaptive Phase 5 shaping weight.
//
// Spec: docs/superpowers/specs/2026-06-01-reward-policy-alignment-investigation.md §6
//
// Reads (from ISV):
//   RL_WIN_RATE_EMA_INDEX (677)            — agent's running win rate
//   RL_CUMULATIVE_DONES_INDEX (660)        — total closed-trade count (resets at
//                                            fold/eval boundary; controller bootstraps
//                                            scaffold back to 1.0 from there)
//   RL_EDGE_PH_FRAC_ALERTED_INDEX (751)    — fraction of batches whose Page-Hinkley
//                                            edge-decay detector has alerted; used as
//                                            re-engagement trigger when policy edge
//                                            degrades
//   RL_SURFER_BREAK_EVEN_WR_INDEX (754)    — config, default 0.30
//   RL_SURFER_K_SHARPNESS_INDEX (755)      — config, default 30.0
//   RL_SURFER_WARMUP_TRADES_INDEX (756)    — config, default 200.0
//
// Writes (to ISV):
//   RL_SURFER_SCAFFOLD_WEIGHT_INDEX (753)  — w ∈ [0,1] consumed by
//                                            rl_fused_reward_pipeline.cu Phase 5
//
// Math (v5.2):
//   competence   = confidence(n_trades) × sigmoid(k_sharp × (wr_ema − break_even))
//   w_competence = 1 − competence
//   w_decay      = max(0, 2 × frac_alerted − 1)   // v5.1: fires above 50% baseline
//   w            = clamp(max(w_competence, w_decay), 0, 1)
//
// v5.2 fix (alpha-rl-mjsmr step 99-249 verdict 2026-06-02): dropped the
// `max(0, …)` clamp on `wr_excess`. With the clamp, wr<break_even mapped to
// sigmoid(0)=0.5 (half-scaffold at novice agent — wrong). Now wr=0.20 maps
// to sigmoid(−3)≈0.05 ⇒ competence≈0.05 ⇒ w_competence≈0.95 (full scaffold,
// correct novice-bias). Symmetric around break_even — competence saturates
// fast both directions for k_sharp=30.
//
// Behavioral table:
//   wr_ema     wr-bk    sigmoid    competence (n_trades≫warmup)    w_competence
//   0.20      -0.10    0.047       0.05                            0.95   (full scaffold)
//   0.27      -0.03    0.29        0.29                            0.71
//   0.30       0.00    0.50        0.50                            0.50   (half at break-even)
//   0.33      +0.03    0.71        0.71                            0.29
//   0.40      +0.10    0.953       0.95                            0.05   (pure pnl)
//
// Per `feedback_no_atomicadd`: single-thread launch (1×1×1), no atomic ops.
// Per `feedback_cpu_is_read_only`: pure device-side.
// Per `feedback_no_nvrtc`: pre-compiled cubin.

#include <stdint.h>
#include <math_constants.h>

// ISV slot indices (must match crates/ml-alpha/src/rl/isv_slots.rs)
#define RL_WIN_RATE_EMA_INDEX               677
#define RL_CUMULATIVE_DONES_INDEX           660
#define RL_EDGE_PH_FRAC_ALERTED_INDEX       751
#define RL_SURFER_SCAFFOLD_WEIGHT_INDEX     753
#define RL_SURFER_BREAK_EVEN_WR_INDEX       754
#define RL_SURFER_K_SHARPNESS_INDEX         755
#define RL_SURFER_WARMUP_TRADES_INDEX       756
#define RL_SURFER_SCAFFOLD_FORCE_PIN_INDEX  824

__device__ static inline float sigmoidf(float x) {
    return 1.0f / (1.0f + expf(-x));
}

extern "C" __global__ void rl_surfer_scaffold_controller(float* isv) {
    // Single-thread kernel — launched (1,1,1)/(1,1,1).
    if (threadIdx.x != 0 || blockIdx.x != 0) return;

    // Phase A force-pin (2026-06-05): when set, leave slot 753 at its
    // bootstrap value (pure-pnl) instead of overwriting it. Fixes the
    // cosmetic-bootstrap bug (pearl_reward_misalign_blocks_f4_slot753_override).
    if (isv[RL_SURFER_SCAFFOLD_FORCE_PIN_INDEX] > 0.5f) {
        return;  // do NOT write isv[RL_SURFER_SCAFFOLD_WEIGHT_INDEX]
    }

    const float wr_ema       = isv[RL_WIN_RATE_EMA_INDEX];
    const float n_trades     = isv[RL_CUMULATIVE_DONES_INDEX];
    const float frac_decay   = isv[RL_EDGE_PH_FRAC_ALERTED_INDEX];
    const float break_even   = isv[RL_SURFER_BREAK_EVEN_WR_INDEX];
    const float k_sharp      = isv[RL_SURFER_K_SHARPNESS_INDEX];
    const float warmup_n     = isv[RL_SURFER_WARMUP_TRADES_INDEX];

    // Confidence: sigmoid centered on warmup_n, scale = 30% of warmup.
    // n_trades=0 → confidence ≈ sigmoid(-warmup/(0.3*warmup)) = sigmoid(-3.33) ≈ 0.034
    // n_trades=warmup → confidence = sigmoid(0) = 0.5
    // n_trades=2*warmup → confidence ≈ sigmoid(3.33) ≈ 0.966
    const float warmup_scale = fmaxf(warmup_n * 0.3f, 1.0f);  // floor to avoid div-by-zero
    const float confidence   = sigmoidf((n_trades - warmup_n) / warmup_scale);

    // Win-rate distance from break-even (signed — v5.2 drops the max(0,...) clamp
    // so wr<break_even properly maps to low competence ⇒ full scaffold).
    const float wr_distance = wr_ema - break_even;

    // Competence: agent must have BOTH trade-count confidence AND wr above break-even.
    const float competence = confidence * sigmoidf(k_sharp * wr_distance);

    // Inverse: scaffold weight from competence.
    const float w_competence = 1.0f - competence;

    // Edge-decay re-engagement: only fires ABOVE 50% baseline. Train-time PH
    // naturally alerts on a steady fraction of batches even during healthy
    // learning (pearl_edge_decay_detector_phase1_validated_train_also_decays);
    // re-engagement must measure EXCESS alertedness, not absolute level, or it
    // pins the scaffold at w=1 forever (alpha-rl-zf6s5 step 5719 failure mode).
    const float w_decay = fmaxf(0.0f, 2.0f * frac_decay - 1.0f);

    const float w_raw = fmaxf(w_competence, w_decay);
    const float w     = fmaxf(0.0f, fminf(1.0f, w_raw));

    isv[RL_SURFER_SCAFFOLD_WEIGHT_INDEX] = w;
}
