//! SP21 ISV slot reservations.
//!
//! SP21 introduces enrichment-driven controllers that read per-trade
//! signals from `GpuBacktestEvaluator::read_per_trade_tape()` (Phase
//! 1.5+2) and feed them back into the next epoch's training step.
//! Each slot here is a single producer / single consumer pair within
//! the SP21 wireup chain.
//!
//! ## Slot allocation
//!
//! | Index | Name | Producer | Consumer |
//! |-------|------|----------|----------|
//! | 520 | `Q_CORRECTION_INDEX` | `enrichment::compute_q_correction` (host, post-val) | `mse_loss_kernel` + `c51_loss_kernel::block_bellman_project_f` (Bellman target offset) |
//! | 521 | `BRANCH_LR_SCALE_DIR_INDEX` | `enrichment::compute_branch_lr_scale[0]` | `dqn_adam_update_kernel` per-branch sub-launch (dir tensors 17-20) |
//! | 522 | `BRANCH_LR_SCALE_MAG_INDEX` | `enrichment::compute_branch_lr_scale[1]` | `dqn_adam_update_kernel` per-branch sub-launch (mag tensors 21-24) |
//! | 523 | `BRANCH_LR_SCALE_ORDER_INDEX` | `enrichment::compute_branch_lr_scale[2]` | `dqn_adam_update_kernel` per-branch sub-launch (order tensors 25-28) |
//! | 524 | `BRANCH_LR_SCALE_URGENCY_INDEX` | `enrichment::compute_branch_lr_scale[3]` | `dqn_adam_update_kernel` per-branch sub-launch (urgency tensors 29-32) |
//! | 525 | `WINNER_CONCENTRATION_INDEX` | host-side aggregation of E6 winner P&L + total P&L | `per_update_pa` kernel (PER alpha scaling) |
//! | 526 | `HINDSIGHT_MAGNITUDE_INDEX` | host-side aggregation of E7 counterfactual P&L magnitudes | `per_update_pa` kernel (PER alpha scaling) |
//! | 527 | `CURRICULUM_CONCENTRATION_INDEX` | host-side entropy of E8 curriculum-weights distribution | `per_update_pa` kernel (PER alpha scaling) |
//! | 528..536 | `CURRICULUM_WEIGHT_{0..8}_INDEX` | E8 per-segment weights (full Vec<f32>) | `per_insert_pa` kernel (per-segment insert priority boost) |
//!
//! ## Bus extension
//!
//! Each new slot bumps `ISV_TOTAL_DIM` (declared in `gpu_dqn_trainer.rs`)
//! and the layout fingerprint (used by `verify_isv_layout_fingerprint`).
//! Both must move atomically with the slot definition here per
//! `feedback_no_partial_refactor`. The `verify_sp21_slot_end_within_total_dim`
//! test below enforces the bus is large enough at compile time.

/// SP21 T2.2 Phase 3 (2026-05-11) — additive Q-target offset from
/// E1 (`enrichment::compute_q_correction`).
///
/// Producer: host-side after each validation backtest pass.
/// `result.q_correction = clamp(-mean(predicted_q − pnl), -10.0, 10.0)`
/// — predicted_q comes from the per-trade tape's Phase 1.5
/// `predicted_q` field (entry_q at trade open). When the policy
/// systematically OVERESTIMATES Q at trade open relative to realized
/// pnl, `q_correction` is negative; underestimation produces a
/// positive correction. Cold start (no validation data yet) writes
/// 0.0 — sentinel matches `pearl_first_observation_bootstrap`.
///
/// Consumer:
///   - `mse_loss_kernel::mse_loss_batched`: scalar kernel arg added
///     to `target_q = (reward + q_correction) + γ × E[Q'] × (1 − done)`
///     before the v_min/v_max clamp.
///   - `c51_loss_kernel::block_bellman_project_f`: reads the slot
///     from the existing `isv_signals` arg. Adds to `t_z = (reward
///     + reward_bias + q_correction) + γ × z_j × (1 − done)`.
///
/// Sign convention: `q_correction` is the ADDITIVE correction to
/// the Bellman target, NOT the bias itself. E1 already negates the
/// bias inside `compute_q_correction`, so a positive value here
/// means "Q-targets should be raised". Cold-start bootstrap = 0.0
/// → no-op until the first val pass writes a real correction.
pub const Q_CORRECTION_INDEX: usize = 520;

/// SP21 T2.2 Phase 4 (2026-05-11) — per-branch Adam LR multiplicative
/// scale from E4 (`enrichment::compute_branch_lr_scale`).
///
/// Producer: host-side after each validation backtest pass. E4 emits
/// `[f32; 4]` indexed by branch (0=Dir, 1=Mag, 2=Order, 3=Urgency)
/// with each element clamped to `[0.5, 2.0]` based on the per-branch
/// loss / mismatch rate inferred from the per-trade tape's
/// (direction, magnitude, holding_bars, pnl) tuples. Branches with
/// higher loss rates get larger LR scales (faster correction);
/// branches with lower rates get smaller scales (less perturbation
/// of already-good behaviour).
///
/// Consumer: `dqn_adam_update_kernel` via `launch_adam_update`'s
/// per-branch sub-launches (Phase 4 split DqnBranches' single
/// sub-launch into 4 per-branch sub-launches, each reading its own
/// `BRANCH_LR_SCALE_*_INDEX` slot and passing it as the new
/// `float lr_scale` kernel arg). The kernel multiplies its
/// effective LR by `lr_scale` before the Adam update step.
///
/// Cold-start: E4 returns `[1.0; 4]` when no losers are observed
/// yet (empty `Vec<EvalTrade>` or all-winning trades), so the
/// initial bus values stay at sentinel 0.0 → kernel sees lr_scale
/// 0.0 which would zero out the update; we therefore floor the
/// kernel-side scale at 1.0 when the slot is at sentinel
/// (`if lr_scale_isv <= 1e-6 then 1.0 else lr_scale_isv`).
/// Matches `pearl_first_observation_bootstrap` discipline.
pub const BRANCH_LR_SCALE_DIR_INDEX: usize     = 521;
pub const BRANCH_LR_SCALE_MAG_INDEX: usize     = 522;
pub const BRANCH_LR_SCALE_ORDER_INDEX: usize   = 523;
pub const BRANCH_LR_SCALE_URGENCY_INDEX: usize = 524;

/// SP21 T2.2 Phase 5+6 combined (2026-05-11) — winner P&L
/// concentration from E6 (`enrichment::compute_winner_indices`).
///
/// Producer: host-side after each validation backtest pass. The raw
/// `result.winner_indices: Vec<usize>` returns top-decile bar
/// indices, but the literal indices are val-space (no mapping to
/// the training replay buffer). The MEANINGFUL signal in E6 is the
/// CONCENTRATION of profitability — top-decile mean P&L divided by
/// all-trade mean P&L. Concentration > 1.0 means winners
/// disproportionately drive total profit (a few big trades carry
/// the strategy); concentration ≈ 1.0 means profit is evenly
/// distributed (more uniform policy quality).
///
/// Consumer: `per_update_pa` kernel (PER priority alpha scaling).
/// Higher concentration → higher alpha multiplier → sharper PER
/// sampling on high-TD-error transitions (the "hard cases" that
/// the policy is missing). Lower concentration → uniform sampling.
///
/// Cold-start: 0.0 sentinel before first val pass. Kernel checks
/// `(winner_conc < 1e-6 && hindsight_mag < 1e-6)` and short-circuits
/// to alpha unchanged (no-op). Per `pearl_first_observation_bootstrap`.
pub const WINNER_CONCENTRATION_INDEX: usize = 525;

/// SP21 T2.2 Phase 5+6 combined (2026-05-11) — hindsight
/// counterfactual-P&L magnitude from E7
/// (`enrichment::compute_hindsight_labels`).
///
/// Producer: host-side after each validation backtest pass. Mean
/// of `|counterfactual_pnl|` across the `result.hindsight: Vec<
/// HindsightExperience>` items. Higher magnitude means the policy
/// missed larger opportunities relative to optimal counterfactuals
/// — more reason to focus PER on under-trained samples.
///
/// Consumer: `per_update_pa` kernel (PER priority alpha scaling),
/// composed multiplicatively with `WINNER_CONCENTRATION_INDEX` to
/// form `alpha_boost`. The combined boost lifts alpha when EITHER
/// E6 (winner concentration) OR E7 (missed opportunities) signal
/// that the policy needs more focus on hard cases.
///
/// Cold-start: 0.0 sentinel; kernel short-circuits to alpha
/// unchanged when both signals are at sentinel.
pub const HINDSIGHT_MAGNITUDE_INDEX: usize = 526;

/// SP21 T2.2 Phase 7 (2026-05-11) — curriculum-weight distribution
/// concentration from E8 (`enrichment::compute_curriculum_weights`).
///
/// Producer: host-side after each validation backtest pass. E8
/// emits per-segment sampling weights (typically `n_segments=8`,
/// normalised to sum=1, where each weight is the inverse-Sharpe
/// of that segment's trades — harder segments get larger weights).
/// `compute_curriculum_concentration` reduces that vector to a
/// scalar `1 - entropy/log(n_segments)` ∈ `[0, 1]`:
///
///   - 0.0 = perfectly uniform weights (all segments equally hard)
///   - 1.0 = one segment dominates (all difficulty concentrated)
///
/// Consumer: `per_update_pa` kernel (PER priority alpha scaling)
/// reads slot 527 device-side and composes it into the existing
/// `boost_delta` alongside `WINNER_CONCENTRATION_INDEX` (E6) and
/// `HINDSIGHT_MAGNITUDE_INDEX` (E7). High curriculum concentration
/// signals the policy has uneven mastery across the data distribution
/// → lift PER alpha to focus more on hard transitions.
///
/// Cold-start: 0.0 sentinel before first val pass — kernel
/// short-circuits to no-op. Per `pearl_first_observation_bootstrap`.
pub const CURRICULUM_CONCENTRATION_INDEX: usize = 527;

/// SP21 T2.2 Phase 7.5 (2026-05-11) — per-segment curriculum
/// weights from E8 (`enrichment::compute_curriculum_weights`).
///
/// Phase 7 wired the SCALAR concentration of these weights to
/// `per_update_pa`'s alpha boost; Phase 7.5 wires the FULL Vec
/// of per-segment weights to `per_insert_pa`'s priority boost,
/// closing the "true E8 consumer" loop that was deferred in 7's
/// audit entry.
///
/// Producer: host-side after each validation backtest pass.
/// `compute_curriculum_weights` returns `Vec<f32>` of length
/// `n_segments = 8` (default), normalised so sum = 1. Each weight
/// is `(1/sharpe).clamp(0.1, 10.0)` / total — segments with low
/// validation Sharpe (= "hard segments") get larger weights.
///
/// Consumer: `per_insert_pa` kernel. Each insert's effective
/// priority gets multiplied by `n_segments × weight[seg_id]`
/// where `seg_id = i % 8` (round-robin tag by insert order
/// within the batch). The `× n_segments` factor makes uniform
/// weights (= 1/8 each) a no-op (= 1.0), so the controller only
/// REDISTRIBUTES sampling priority across segments without
/// changing the overall priority scale.
///
/// Round-robin segment tagging: experience-collector batches
/// (typically 512+ tuples per insert) get evenly distributed
/// across the 8 segments. Over time the buffer carries equal
/// representation per segment; E8's weights then redirect
/// sampling pressure toward hard segments at insert time.
///
/// Cold-start: when `isv_signals[CURRICULUM_WEIGHT_0_INDEX]` is
/// at sentinel `0.0` (pre-first-emit), the kernel short-circuits
/// to `boost = 1.0` (uniform no-op). Same
/// `pearl_first_observation_bootstrap` pattern as Phases 5+6+7.
pub const CURRICULUM_WEIGHT_0_INDEX: usize = 528;
pub const CURRICULUM_WEIGHT_1_INDEX: usize = 529;
pub const CURRICULUM_WEIGHT_2_INDEX: usize = 530;
pub const CURRICULUM_WEIGHT_3_INDEX: usize = 531;
pub const CURRICULUM_WEIGHT_4_INDEX: usize = 532;
pub const CURRICULUM_WEIGHT_5_INDEX: usize = 533;
pub const CURRICULUM_WEIGHT_6_INDEX: usize = 534;
pub const CURRICULUM_WEIGHT_7_INDEX: usize = 535;

/// Number of curriculum segments. Matches `EnrichmentState`'s
/// `n_segments` default and the kernel's `seg_id = i % N` mod.
pub const CURRICULUM_N_SEGMENTS: usize = 8;

/// Convenience accessor: segment idx (0..8) → ISV slot index.
#[inline]
pub fn curriculum_weight_index(seg_idx: usize) -> usize {
    assert!(seg_idx < CURRICULUM_N_SEGMENTS,
        "curriculum_weight_index: seg_idx must be 0..{CURRICULUM_N_SEGMENTS} (got {seg_idx})");
    CURRICULUM_WEIGHT_0_INDEX + seg_idx
}

/// Convenience accessor: branch index (0..4) → ISV slot index. Used
/// by the launcher's per-branch sub-launch loop to keep the
/// branch-index ↔ ISV-slot mapping in one place.
#[inline]
pub fn branch_lr_scale_index(branch: usize) -> usize {
    match branch {
        0 => BRANCH_LR_SCALE_DIR_INDEX,
        1 => BRANCH_LR_SCALE_MAG_INDEX,
        2 => BRANCH_LR_SCALE_ORDER_INDEX,
        3 => BRANCH_LR_SCALE_URGENCY_INDEX,
        _ => panic!("branch_lr_scale_index: branch must be 0..4 (got {branch})"),
    }
}

/// First SP21 slot. Used by the bus-bounds compile-time check below
/// to assert SP21's slot range is reachable.
pub const SP21_SLOT_START: usize = 520;

/// One past the last SP21 slot.
pub const SP21_SLOT_END: usize = 536;

#[cfg(test)]
mod tests {
    use super::*;

    /// Compile-time assertion that the ISV bus is wide enough for
    /// SP21's slots. Bumping `ISV_TOTAL_DIM` is a layout-fingerprint
    /// change — every consumer of `ISV_TOTAL_DIM` (pinned alloc,
    /// fingerprint string, seed) must move atomically with the bus
    /// extension per `feedback_no_partial_refactor`.
    #[test]
    fn verify_sp21_slot_end_within_total_dim() {
        use crate::cuda_pipeline::gpu_dqn_trainer::ISV_TOTAL_DIM;
        assert!(
            SP21_SLOT_END <= ISV_TOTAL_DIM,
            "SP21_SLOT_END={SP21_SLOT_END} exceeds ISV_TOTAL_DIM={ISV_TOTAL_DIM} — bus \
             too small for SP21 slots; bump `ISV_TOTAL_DIM` in `gpu_dqn_trainer.rs` and \
             update `layout_fingerprint_seed()` (per `feedback_no_partial_refactor`)."
        );
    }

    #[test]
    fn sp21_slot_indices_are_unique() {
        let slots = [
            Q_CORRECTION_INDEX,
            BRANCH_LR_SCALE_DIR_INDEX,
            BRANCH_LR_SCALE_MAG_INDEX,
            BRANCH_LR_SCALE_ORDER_INDEX,
            BRANCH_LR_SCALE_URGENCY_INDEX,
            WINNER_CONCENTRATION_INDEX,
            HINDSIGHT_MAGNITUDE_INDEX,
            CURRICULUM_CONCENTRATION_INDEX,
            CURRICULUM_WEIGHT_0_INDEX,
            CURRICULUM_WEIGHT_1_INDEX,
            CURRICULUM_WEIGHT_2_INDEX,
            CURRICULUM_WEIGHT_3_INDEX,
            CURRICULUM_WEIGHT_4_INDEX,
            CURRICULUM_WEIGHT_5_INDEX,
            CURRICULUM_WEIGHT_6_INDEX,
            CURRICULUM_WEIGHT_7_INDEX,
        ];
        let mut sorted = slots.to_vec();
        sorted.sort();
        sorted.dedup();
        assert_eq!(slots.len(), sorted.len(), "SP21 ISV slot indices must be unique");
    }

    #[test]
    fn branch_lr_scale_index_matches_constants() {
        assert_eq!(branch_lr_scale_index(0), BRANCH_LR_SCALE_DIR_INDEX);
        assert_eq!(branch_lr_scale_index(1), BRANCH_LR_SCALE_MAG_INDEX);
        assert_eq!(branch_lr_scale_index(2), BRANCH_LR_SCALE_ORDER_INDEX);
        assert_eq!(branch_lr_scale_index(3), BRANCH_LR_SCALE_URGENCY_INDEX);
    }

    #[test]
    fn curriculum_weight_index_matches_constants() {
        assert_eq!(curriculum_weight_index(0), CURRICULUM_WEIGHT_0_INDEX);
        assert_eq!(curriculum_weight_index(7), CURRICULUM_WEIGHT_7_INDEX);
        assert_eq!(CURRICULUM_N_SEGMENTS, 8);
    }
}
