#![allow(unsafe_code)] // CUDA kernel launch via cudarc requires unsafe.
//! SP20 Phase 1.3 (2026-05-09) — `sp20_controllers_compute_kernel` launcher.
//!
//! Component 5 / Kernel 2 of the SP20 design — the deterministic
//! derivation step. Reads the EMAs produced by Phase 1.2's
//! [`super::sp20_emas_compute`] (and indirectly Phase 1.1's
//! [`super::sp20_stats_compute`] outputs that Kernel 1 already blended
//! into the same internal scratch) and writes 6 ISV controller outputs:
//!
//! | Output                                                      | Formula                                              |
//! |-------------------------------------------------------------|------------------------------------------------------|
//! | [`crate::cuda_pipeline::sp14_isv_slots::LOSS_CAP_INDEX`]            | `-1 - clamp((wr_ema-0.50)/0.05, 0, 1)`               |
//! | [`crate::cuda_pipeline::sp14_isv_slots::N_STEP_INDEX`]              | `clamp(round(trade_dur_ema), 1, 30)`                 |
//! | [`crate::cuda_pipeline::sp14_isv_slots::AUX_CONF_THRESHOLD_INDEX`]  | `clamp(dir_acc_ema - 0.50, 0.01, 0.20)`              |
//! | [`crate::cuda_pipeline::sp14_isv_slots::AUX_GATE_TEMP_INDEX`]       | `max(aux_std_ema, 0.01)`                             |
//! | [`crate::cuda_pipeline::sp14_isv_slots::TARGET_HOLD_PCT_INDEX`]     | `clamp(0.8 - aux_p50_ema*1.5, 0.1, 0.8)`             |
//! | [`crate::cuda_pipeline::sp14_isv_slots::HOLD_COST_SCALE_INDEX`]     | two-sided ramp around `TARGET_HOLD_PCT` ±0.05        |
//!
//! ## Hard rules covered
//!
//!  - `pearl_controller_anchors_isv_driven` — every output's anchor /
//!    target / cap derives from EMAs; no hardcoded magic constants.
//!  - `feedback_isv_for_adaptive_bounds` — these 6 outputs ARE the
//!    adaptive bounds; downstream consumers read them rather than
//!    embedding the formulas.
//!  - `pearl_no_host_branches_in_captured_graph` — single-block,
//!    single-thread kernel; safe to capture in the per-step CUDA
//!    Graph.
//!  - `feedback_no_atomicadd` — single-thread kernel.
//!  - `feedback_no_cpu_compute_strict` — every formula lives in the
//!    kernel; the launcher passes only pointers + slot-index args.
//!  - `feedback_no_htod_htoh_only_mapped_pinned` — both ISV and
//!    `internal` scratch are mapped-pinned device pointers; kernel
//!    emits `__threadfence_system()` for PCIe-visible coherence.
//!  - `pearl_tests_must_prove_not_lock_observations` — companion
//!    test asserts clamp boundaries, formula correctness, and
//!    bidirectional ramp behavior, NOT specific observed values.
//!
//! ## Phase 1.3 wiring scope
//!
//! Kernel + launcher + tests + build entry **only**. The
//! training-loop call site lands in Phase 1.4 atomically with the
//! rest of the SP20 reward chain (Kernel 1 = EMAs already landed in
//! Phase 1.2; Kernel 3 = stats already landed in Phase 1.1) per
//! `feedback_no_partial_refactor`.
//!
//! ## Stream-ordering contract (Phase 1.4 forward reference)
//!
//! Phase 1.4 must launch this kernel on the SAME stream as
//! `sp20_emas_compute_kernel` and AFTER it (Kernel 1 → Kernel 2),
//! so the in-stream-order invariant guarantees every read here sees
//! the EMAs Kernel 1 just blended this step. The trainer's per-step
//! state-tracker stream is the natural home for the 3-kernel chain
//! (Stats → EMAs → Controllers).

/// Launch `sp20_controllers_compute_kernel` on the producer's stream.
///
/// Single-block, single-thread launch. `kernel` MUST be the loaded
/// `sp20_controllers_compute_kernel` `CudaFunction` (from the cubin
/// emitted by `build.rs` — see manifest entry
/// `sp20_controllers_compute_kernel.cu`).
///
/// Buffer pointers MUST point to mapped-pinned device-visible
/// allocations:
///   - `isv_dev`: ISV bus, at least
///     [`crate::cuda_pipeline::sp14_isv_slots::SP20_ISV_SLOT_END`] f32s.
///   - `internal_dev`: 4-long f32 scratch laid out per
///     [`super::sp20_emas_compute::EMA_INTERNAL_SLOT_COUNT`] +
///     `EMA_INTERNAL_*` indices.
///
/// # Safety
///
/// Both buffer pointers MUST be valid mapped-pinned device pointers
/// obtained via `MappedF32Buffer::dev_ptr`. The caller MUST issue
/// this launch on the SAME stream as the upstream
/// `sp20_emas_compute` (so the stream-ordering invariant guarantees
/// every read here sees the EMAs Kernel 1 just blended this step).
/// Phase 1.4 lands the production wire site with that ordering
/// enforced.
pub unsafe fn launch_sp20_controllers_compute(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    isv_dev: u64,
    internal_dev: u64,
) -> Result<(), crate::MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(
        isv_dev != 0,
        "launch_sp20_controllers_compute: isv_dev must be a valid device pointer"
    );
    debug_assert!(
        internal_dev != 0,
        "launch_sp20_controllers_compute: internal_dev must be a valid device pointer"
    );

    // Slot indices passed in as kernel args so the kernel stays
    // decoupled from SP14 slot-number drift (the contract is enforced
    // by `sp14_isv_slots.rs` slot-locked tests).
    let isv_idx_loss_cap: i32 =
        crate::cuda_pipeline::sp14_isv_slots::LOSS_CAP_INDEX as i32;
    let isv_idx_wr_ema: i32 =
        crate::cuda_pipeline::sp14_isv_slots::WR_EMA_INDEX as i32;
    let isv_idx_hold_cost_scale: i32 =
        crate::cuda_pipeline::sp14_isv_slots::HOLD_COST_SCALE_INDEX as i32;
    let isv_idx_hold_pct_ema: i32 =
        crate::cuda_pipeline::sp14_isv_slots::HOLD_PCT_EMA_INDEX as i32;
    let isv_idx_target_hold_pct: i32 =
        crate::cuda_pipeline::sp14_isv_slots::TARGET_HOLD_PCT_INDEX as i32;
    let isv_idx_n_step: i32 =
        crate::cuda_pipeline::sp14_isv_slots::N_STEP_INDEX as i32;
    let isv_idx_aux_conf_threshold: i32 =
        crate::cuda_pipeline::sp14_isv_slots::AUX_CONF_THRESHOLD_INDEX as i32;
    let isv_idx_aux_gate_temp: i32 =
        crate::cuda_pipeline::sp14_isv_slots::AUX_GATE_TEMP_INDEX as i32;

    let cfg = LaunchConfig {
        grid_dim:         (1, 1, 1),
        block_dim:        (1, 1, 1),
        shared_mem_bytes: 0,
    };

    stream
        .launch_builder(kernel)
        .arg(&isv_dev)
        .arg(&internal_dev)
        .arg(&isv_idx_loss_cap)
        .arg(&isv_idx_wr_ema)
        .arg(&isv_idx_hold_cost_scale)
        .arg(&isv_idx_hold_pct_ema)
        .arg(&isv_idx_target_hold_pct)
        .arg(&isv_idx_n_step)
        .arg(&isv_idx_aux_conf_threshold)
        .arg(&isv_idx_aux_gate_temp)
        .launch(cfg)
        .map_err(|e| {
            crate::MLError::ModelError(format!(
                "sp20_controllers_compute_kernel launch: {e}"
            ))
        })?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::cuda_pipeline::sp14_isv_slots::{
        AUX_CONF_THRESHOLD_INDEX, AUX_GATE_TEMP_INDEX, HOLD_COST_SCALE_INDEX,
        HOLD_PCT_EMA_INDEX, LOSS_CAP_INDEX, N_STEP_INDEX, TARGET_HOLD_PCT_INDEX,
        WR_EMA_INDEX,
    };

    /// All 6 output slots are inside the SP20 reserved range
    /// [510..520). Locks the slot-layout contract against silent
    /// changes that would put a controller output outside the range
    /// reserved by SP20's spec.
    #[test]
    fn output_slots_within_sp20_reserved_range() {
        const BASE: usize = 510;
        const END: usize = 520;
        for &slot in &[
            LOSS_CAP_INDEX,
            HOLD_COST_SCALE_INDEX,
            TARGET_HOLD_PCT_INDEX,
            N_STEP_INDEX,
            AUX_CONF_THRESHOLD_INDEX,
            AUX_GATE_TEMP_INDEX,
        ] {
            assert!(
                (BASE..END).contains(&slot),
                "controller output slot {slot} must be inside [{BASE}..{END})"
            );
        }
    }

    /// All 6 output slots are unique. Locks against silent slot-index
    /// collisions that would have one controller silently overwrite
    /// another's output.
    #[test]
    fn output_slots_unique() {
        let slots = [
            LOSS_CAP_INDEX,
            HOLD_COST_SCALE_INDEX,
            TARGET_HOLD_PCT_INDEX,
            N_STEP_INDEX,
            AUX_CONF_THRESHOLD_INDEX,
            AUX_GATE_TEMP_INDEX,
        ];
        for i in 0..slots.len() {
            for j in (i + 1)..slots.len() {
                assert_ne!(
                    slots[i], slots[j],
                    "output slots {} and {} collide at index {}",
                    i, j, slots[i]
                );
            }
        }
    }

    /// The 2 ISV inputs (WR_EMA, HOLD_PCT_EMA) are also inside the
    /// SP20 reserved range — they are written by Phase 1.2 and read
    /// by this kernel in the same range.
    #[test]
    fn input_isv_slots_within_sp20_reserved_range() {
        const BASE: usize = 510;
        const END: usize = 520;
        for &slot in &[WR_EMA_INDEX, HOLD_PCT_EMA_INDEX] {
            assert!(
                (BASE..END).contains(&slot),
                "ISV input slot {slot} must be inside [{BASE}..{END})"
            );
        }
    }
}
