//! Mapped-pinned ingress slots — the ONLY CPU→GPU pathway for snapshot
//! and fill events on the hot path. Wraps `pinned_mem::MappedF32Buffer`.
//!
//! Per `feedback_no_htod_htoh_only_mapped_pinned.md`: every byte that
//! enters the GPU does so via `cuMemHostAlloc(DEVICEMAP)`. Tests are
//! not exempt.
//!
//! Layout invariant: `SnapshotPayload` and `FillPayload` are `#[repr(C)]`
//! so the kernel-side `struct` matches bit-for-bit. The structs are
//! padded so the byte length is a multiple of `sizeof(f32)` — required
//! because `MappedF32Buffer` allocates f32-element-counted memory.

use anyhow::Result;
use std::sync::atomic::{compiler_fence, Ordering};

use crate::pinned_mem::MappedF32Buffer;

#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
pub struct SnapshotPayload {
    pub features: [f32; 32],
    pub ts_ns: u64,
    pub valid: u32,
    pub _pad: u32, // align to 8-byte for ts_ns; total = 32*4 + 8 + 4 + 4 = 144 bytes
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
pub struct FillPayload {
    pub fill_price: f32,
    pub fill_qty: f32,
    pub side: f32,       // +1.0 buy, -1.0 sell
    pub commission: f32,
    pub ts_ns: u64,      // 8 bytes
    pub _pad: [f32; 3],  // total = 4*4 + 8 + 12 = 32 bytes
}

// Compile-time layout assertions (size_of must be multiple of 4 since we
// back with MappedF32Buffer).
const _: () = assert!(std::mem::size_of::<SnapshotPayload>() % 4 == 0);
const _: () = assert!(std::mem::size_of::<FillPayload>() % 4 == 0);

#[inline(always)]
const fn n_f32<T>() -> usize {
    (std::mem::size_of::<T>() + 3) / 4
}

pub struct MappedPinnedSnapshotSlot {
    backing: MappedF32Buffer,
}

impl MappedPinnedSnapshotSlot {
    pub fn new() -> Result<Self> {
        let backing = unsafe { MappedF32Buffer::new(n_f32::<SnapshotPayload>()) }
            .map_err(|e| anyhow::anyhow!("snapshot slot alloc: {e}"))?;
        Ok(Self { backing })
    }

    /// Hot path. Volatile write of the payload into pinned memory; the
    /// GPU reads the same address via `device_ptr()`.
    pub fn write_volatile(&mut self, payload: &SnapshotPayload) {
        let host = self.backing.host_slice_mut();
        let dst = host.as_mut_ptr().cast::<SnapshotPayload>();
        // Safety: backing has space for `n_f32::<SnapshotPayload>()` f32s,
        // which is sized to hold a `SnapshotPayload` per the static_assert
        // above. `repr(C)` ensures the layout matches the kernel-side
        // struct bit-for-bit.
        unsafe { std::ptr::write_volatile(dst, *payload); }
        compiler_fence(Ordering::Release);
    }

    /// Slow path — inspect the host side (tests, debug).
    pub fn peek(&self) -> SnapshotPayload {
        let src = self.backing.host_ptr.cast::<SnapshotPayload>();
        // Safety: same invariants as write_volatile.
        unsafe { std::ptr::read_volatile(src) }
    }

    pub fn device_ptr(&self) -> cudarc::driver::sys::CUdeviceptr {
        self.backing.dev_ptr
    }
}

pub struct MappedPinnedFillSlot {
    backing: MappedF32Buffer,
}

impl MappedPinnedFillSlot {
    pub fn new() -> Result<Self> {
        let backing = unsafe { MappedF32Buffer::new(n_f32::<FillPayload>()) }
            .map_err(|e| anyhow::anyhow!("fill slot alloc: {e}"))?;
        Ok(Self { backing })
    }

    pub fn write_volatile(&mut self, payload: &FillPayload) {
        let host = self.backing.host_slice_mut();
        let dst = host.as_mut_ptr().cast::<FillPayload>();
        unsafe { std::ptr::write_volatile(dst, *payload); }
        compiler_fence(Ordering::Release);
    }

    pub fn peek(&self) -> FillPayload {
        let src = self.backing.host_ptr.cast::<FillPayload>();
        unsafe { std::ptr::read_volatile(src) }
    }

    pub fn device_ptr(&self) -> cudarc::driver::sys::CUdeviceptr {
        self.backing.dev_ptr
    }
}
