//! Phase R6: `RlLobBackend` — narrow device-oriented trait that the
//! integrated RL trainer's `step_with_lobsim` consumes for its
//! interaction with the GPU LOB simulator.
//!
//! This replaces the flawed Phase F's `LobEnv` trait + `MockLobEnv`
//! fixture, both of which encouraged host-side action sampling and
//! host-side reward extraction (a `feedback_cpu_is_read_only`
//! violation that the toy-bandit fixture intrinsically couldn't
//! catch).
//!
//! ### Why a trait at all
//!
//! `ml-alpha` cannot have a regular dep on `ml-backtesting` (cycle:
//! `ml-backtesting → ml-alpha` already exists). The trait gives the
//! trainer a typed device-buffer surface without naming `LobSimCuda`
//! directly. The convergence-gate fixtures + the production CLI
//! (`alpha_rl_train`) consume `ml-backtesting` as a dev-dep / regular
//! dep respectively, where the `impl RlLobBackend for LobSimCuda`
//! lives.
//!
//! ### Why NOT a mocking layer
//!
//! Per `pearl_tests_must_prove_not_lock_observations` and the explicit
//! "no toys" feedback that drove this rebuild: there is NO mock
//! implementation. Convergence-gate fixtures use a real `LobSimCuda`
//! with synthetic book data. A trait with one implementation is
//! abstraction overhead — but here the abstraction earns its keep by
//! breaking the ml-alpha ↔ ml-backtesting dep cycle.
//!
//! ### Device-oriented surface
//!
//! Every method either operates on device buffers or is the unavoidable
//! HtoD boundary (snapshot ingestion from disk):
//!
//!   - `apply_snapshot` (HtoD: snapshots come from the disk loader)
//!   - `market_targets_d_mut` (mutable device buffer for the trainer's
//!     `actions_to_market_targets` kernel to write `(side, size)` pairs)
//!   - `pos_d` / `pos_bytes` (immutable device pointer for the trainer's
//!     `extract_realized_pnl_delta` and `actions_to_market_targets`
//!     kernels to read `Pos` state)
//!   - `step_fill_from_market_targets` (runs the LobSim fill kernel +
//!     pnl_track on whatever's currently in `market_targets_d`)
//!
//! No host loops, no DtoH/HtoD of training-loop data, no per-batch
//! orchestration calls.

use anyhow::Result;
use cudarc::driver::CudaSlice;

use crate::cfc::snap_features::Mbp10RawInput;

/// Narrow device-oriented backend the integrated RL trainer's
/// `step_with_lobsim` invokes for its LOB-simulator interaction.
/// Implemented for `LobSimCuda` in `ml-backtesting`. See module-level
/// docs for the design rationale.
pub trait RlLobBackend {
    /// HtoD-ingest one MBP-10 snapshot's top-10 levels into the
    /// device-resident book. The only unavoidable boundary copy
    /// (snapshots originate on the host from the disk loader).
    /// Implementations route via mapped-pinned per
    /// `feedback_no_htod_htoh_only_mapped_pinned`.
    fn apply_snapshot(&mut self, snapshot: &Mbp10RawInput) -> Result<()>;

    /// Disjoint-field accessor returning both the read-only `pos_d`
    /// byte array AND the mutable `market_targets` buffer in one
    /// `&mut self` call. Required because the trainer's
    /// `actions_to_market_targets` kernel needs BOTH at once (reads
    /// `pos_d` for current `position_lots`, writes `market_targets_d`
    /// with the resolved `(side, size)` pairs). Implementations split
    /// the field borrows via Rust's disjoint-field rule:
    /// `(&self.pos_d, &mut self.market_targets_d)`.
    fn pos_and_market_targets_mut(
        &mut self,
    ) -> (&CudaSlice<u8>, &mut CudaSlice<i32>);

    /// Read-only reference to the device-resident `Pos` byte array
    /// for kernels that only read (e.g., `extract_realized_pnl_delta`
    /// after the fill).
    fn pos_d(&self) -> &CudaSlice<u8>;

    /// Size of one `Pos` struct in bytes (24 for the current
    /// `PosFlat` layout). Passed to device kernels that index into
    /// `pos_d` so they don't need to be rebuilt if `PosFlat` grows.
    fn pos_bytes(&self) -> usize;

    /// Run the LobSim fill kernel + `step_pnl_track` against the
    /// currently-populated `market_targets_d`. Caller is responsible
    /// for ensuring `market_targets_d` has been written this step
    /// (via the trainer's `actions_to_market_targets` launcher).
    fn step_fill_from_market_targets(&mut self, current_ts_ns: u64) -> Result<()>;

    /// Number of parallel backtests (batch dimension).
    fn n_backtests(&self) -> usize;

    /// Read-only accessor for the shared best-bid price array
    /// (`BOOK_LEVELS` floats, lobsim-wide — single book shared across
    /// batches). Used by trail-stop and pyramid-threshold checks.
    fn bid_px_d(&self) -> &CudaSlice<f32>;

    /// Read-only accessor for the shared best-ask price array. Pair to
    /// `bid_px_d`.
    fn ask_px_d(&self) -> &CudaSlice<f32>;

    /// Combined disjoint-field accessor for the actions_to_market_targets
    /// kernel which needs pos (read), market_targets (write), AND book
    /// prices (read) simultaneously. Implementations split via Rust's
    /// disjoint-field rule across four independent struct fields.
    fn pos_book_and_market_targets_mut(
        &mut self,
    ) -> (&CudaSlice<u8>, &CudaSlice<f32>, &CudaSlice<f32>, &mut CudaSlice<i32>);

    /// Device-to-device variant of `apply_snapshot`. Copies 4 ×
    /// `BOOK_LEVELS` floats from the given source device pointers into
    /// the lobsim's internal bid/ask staging buffers, then launches the
    /// `book_update_apply_snapshot` kernel to broadcast into per-backtest
    /// book state — identical to `apply_snapshot` except the source is a
    /// device buffer (e.g. the perception trainer's SoA) rather than
    /// host memory.
    ///
    /// Each source pointer addresses `BOOK_LEVELS` contiguous `f32`
    /// values on the device. The copy is stream-ordered (no sync).
    fn apply_snapshot_from_device(
        &mut self,
        bid_px_src: u64,
        bid_sz_src: u64,
        ask_px_src: u64,
        ask_sz_src: u64,
    ) -> Result<()>;
}
