/* scripted_policy_select — GPU-driven seed-phase action selection.
 *
 * Plan 3 Task 8 (B.3 GPU-only seeded warm-start).
 *
 * Replaces `experience_action_select`'s network-Q argmax during the seed
 * phase. Per-sample: deterministically assign one of 4 policies (40/20/20/20
 * mix) based on `i % 5`, compute the scripted action, write to actions_out
 * and conviction_out. Driven from the launcher in `gpu_experience_collector.rs`
 * which reads ISV[SEED_STEPS_DONE_INDEX] vs ISV[SEED_STEPS_TARGET_INDEX] at
 * the per-epoch boundary (cold path) to decide whether to dispatch this
 * kernel or `experience_action_select`. The action computation itself is
 * 100% GPU; CPU only orchestrates the dispatch.
 *
 * Policy types (factored action layout: dir(4) × mag(3) × ord(3) × urg(3)):
 *   0 (UNIFORM, 40%):    LCG random direction + half magnitude + market +
 *                        normal urgency. Conviction floored at 0.5
 *                        (uniform doesn't express confidence).
 *   1 (MOMENTUM, 20%):   sign(recent_return) → direction; conviction =
 *                        |return| × 10 capped at 1.
 *   2 (MEAN_REV, 20%):   −sign(recent_return) → direction; same conviction.
 *   3 (VWAP_DEV, 20%):   Approximate VWAP as one-bar lag (PS_PREV_CLOSE);
 *                        deviation outside ±5bp band → mean-revert direction;
 *                        conviction by relative band breach magnitude.
 *
 * "Recent return" comes from the current bar's raw_close (read from
 * `targets[bar*6+TARGET_RAW_CLOSE=2]` per the fxcache contract) and
 * `portfolio[PS_PREV_CLOSE]` (last bar's raw_close, written by env_step at
 * the previous step). We do NOT pass historical bars — one-bar return is
 * sufficient for a seed-phase exploration policy.
 *
 * Fix 30 Stale-C (Bug-1 contract drift, Fix 29 row #12): pre-fix the kernel
 * read `state[MARKET_START]` as `close_now`. Post Bug 1 (commit `5a5dd0fed`)
 * `state[MARKET_START]` is z-normed log-return at the WRITER, not raw_close,
 * so `(close_now − prev_close) / prev_close` mixed two unrelated signals
 * (z-normed return vs dollar price) and `recent_ret` was meaningless. We
 * now route raw_close from the same fxcache target buffer the env_step
 * kernel reads from — `targets[bar*6+TARGET_RAW_CLOSE]` — using the
 * `episode_starts` + per-step `t` indexing pattern env_step already uses
 * (`experience_kernels.cu:1769`). This restores the recent_return contract
 * to a real one-bar % return and matches `feedback_no_functionality_removal`
 * (the recent_ret signal stays — only its source is fixed; we do NOT swap
 * to CUSUM as the audit's alternative-path bullet considered).
 *
 * Seed-policy direction-flip thresholds (`±0.0001f`) and VWAP band width
 * (`±0.0005f × prev_close`) are noise-floor cutoffs below which we hold —
 * NOT scaling coefficients (per feedback_adaptive_not_tuned.md carve-out).
 *
 * GPU-only: no CPU physics mirror. Existing `experience_env_step` runs
 * unchanged; we only swap the action source kernel.
 */

#include <cuda_runtime.h>
#include "state_layout.cuh"

/* fxcache target stride must match crates/ml/src/fxcache.rs::TARGET_DIM (6)
 * and the column for raw_close (TARGET_RAW_CLOSE = 2). Kernels can't import
 * Rust constants; mirroring the literal value here keeps the host-side
 * writer + this kernel consumer at the same call site for any future
 * stride / column rename (Fix 27 prevention pattern). */
#define FXCACHE_TARGET_STRIDE       6
#define FXCACHE_TARGET_RAW_CLOSE    2

extern "C" __global__ void scripted_policy_select(
    const float* __restrict__ states,           /* [N, state_dim] assembled state vector */
    const float* __restrict__ portfolio_states, /* [N, PS_STRIDE] for prev_close access */
    const float* __restrict__ targets,          /* [total_bars, 6] fxcache targets — read at offset bar*6+2 (raw_close). Fix 30 Stale-C. */
    const int*   __restrict__ episode_starts,   /* [N] global bar index of episode start (matches env_step convention) */
    int          n_samples,
    int          state_dim,
    int          t,                             /* per-episode timestep ∈ [0, L); mixes into LCG seed */
    int          total_bars,                    /* fxcache target row count, used to clamp the bar lookup */
    int*         __restrict__ actions_out,      /* [N] factored action index ∈ [0, 108) */
    float*       __restrict__ conviction_out,   /* [N] direction conviction ∈ [0, 1] */
    float*       __restrict__ magnitude_conviction_out /* [N] magnitude conviction ∈ [0, 1] */
) {
    const int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i >= n_samples) return;

    /* Policy assignment: deterministic mix.
     * i % 5 ∈ {0,1,2,3,4}; map {0,1} → UNIFORM (2/5 = 40%), 2 → MOMENTUM,
     * 3 → MEAN_REV, 4 → VWAP_DEV. Each of the latter three gets 20%. */
    const int mod5 = i % 5;
    const int policy_id = (mod5 < 2) ? 0 : (mod5 - 1);  /* 0=UNIFORM, 1=MOM, 2=REV, 3=VWAP */

    /* Read raw_close (current bar's `targets[bar*6+2]`) and prev_close
     * (`PS_PREV_CLOSE` = last bar's raw_close, written by env_step at the
     * previous step). Indexing matches `experience_env_step` exactly:
     * `bar_idx = episode_starts[i] + t`. PS_PREV_CLOSE = 6 in PS_STRIDE
     * (43) layout. Bounds: clamp to total_bars-1 to mirror env_step's
     * out-of-bounds early-return — the seed-phase scripted policy still
     * fires at the last bar, just on the last available raw_close. */
    int bar_idx = episode_starts[i] + t;
    if (bar_idx < 0) bar_idx = 0;
    if (bar_idx >= total_bars) bar_idx = total_bars - 1;
    const float close_now  = targets[(long long)bar_idx * FXCACHE_TARGET_STRIDE
                                     + FXCACHE_TARGET_RAW_CLOSE];
    const float prev_close = portfolio_states[(long long)i * PS_STRIDE + PS_PREV_CLOSE];
    const float recent_ret = (prev_close > 1e-3f)
        ? (close_now - prev_close) / prev_close
        : 0.0f;

    int   dir        = DIR_HOLD;   /* default Hold */
    float conviction = 0.5f;

    if (policy_id == 0) {
        /* UNIFORM: LCG random direction across all 4 values */
        unsigned int seed = ((unsigned int)i * 2654435761u)
                          ^ ((unsigned int)bar_idx * 16777619u);
        seed = seed * 1103515245u + 12345u;
        dir = (int)((seed >> 16) & 0x3);  /* 0..3 */
        conviction = 0.5f;
    } else if (policy_id == 1) {
        /* MOMENTUM: trade with the move */
        if      (recent_ret >  0.0001f) { dir = DIR_LONG;  }
        else if (recent_ret < -0.0001f) { dir = DIR_SHORT; }
        else                            { dir = DIR_HOLD;  }
        conviction = fminf(1.0f, fabsf(recent_ret) * 10.0f);
    } else if (policy_id == 2) {
        /* MEAN_REV: trade against the move */
        if      (recent_ret >  0.0001f) { dir = DIR_SHORT; }
        else if (recent_ret < -0.0001f) { dir = DIR_LONG;  }
        else                            { dir = DIR_HOLD;  }
        conviction = fminf(1.0f, fabsf(recent_ret) * 10.0f);
    } else {
        /* VWAP_DEV: approximate VWAP as one-bar lag (PS_PREV_CLOSE).
         * The actual VWAP isn't stored in PS; the one-bar-lag proxy is
         * acceptable for a seed-phase exploration policy. */
        const float dev        = close_now - prev_close;
        const float band_width = fmaxf(prev_close * 0.0005f, 1e-3f);
        if      (dev >  prev_close * 0.0005f) { dir = DIR_SHORT; }
        else if (dev < -prev_close * 0.0005f) { dir = DIR_LONG;  }
        else                                  { dir = DIR_HOLD;  }
        conviction = fminf(1.0f, fabsf(dev) / band_width);
    }

    /* Magnitude = Half (idx 1), order = Market (idx 0), urgency = Normal (idx 1).
     * Factored layout: action = dir*27 + mag*9 + ord*3 + urg.
     * For DIR_HOLD/DIR_FLAT, mag is forced to MAG_QUARTER (0) elsewhere by
     * the env-step kernel's deterministic encoding (see CLAUDE.md project
     * memory note on direction encoding); we still emit MAG_HALF here
     * because the env-step kernel ignores magnitude for Hold/Flat. */
    const int mag = MAG_HALF;     /* 1 */
    const int ord = 0;            /* Market */
    const int urg = 1;            /* Normal */
    actions_out[i]    = dir * 27 + mag * 9 + ord * 3 + urg;
    conviction_out[i] = conviction;
    /* The scripted policy hard-codes magnitude=Half and does not express
     * a per-sample magnitude preference. Emit 0.0 for magnitude conviction
     * so the env-step's max(dir_conv, mag_conv) Kelly composer falls back
     * to the direction signal (the only signal this policy actually
     * represents). The network's `experience_action_select` overwrites
     * both buffers with real Q-spread normalised conviction once seeded. */
    magnitude_conviction_out[i] = 0.0f;
}
