//! Host-side order representation for the LOB backtest harness.
//! See docs/superpowers/specs/2026-05-18-real-lob-integration-design.md §3.
//!
//! - `OrderIntent` is the ergonomic enum used by policy YAML config + the
//!   trade-log audit reconstruction.
//! - `OrderEvent` is the flat 24-byte device-side record written by the
//!   decision-policy kernel into the per-block mapped-pinned audit ring.
//! - `TradeRecord` is the flat 40-byte device-side record written by the
//!   pnl_track kernel into the per-block mapped-pinned trade-log buffer
//!   when a position returns to flat.
//! - `SlotTag` packs `{kind, index, gen_counter}` into a u32 so the
//!   device-side kernel can emit it as a single field; the gen_counter
//!   distinguishes a new slot occupant from stale handles after reuse.

use serde::{Deserialize, Serialize};

#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Side {
    Buy = 0,
    Sell = 1,
}

#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum LimitKind {
    Limit = 0,
    Ioc = 1,
    Fok = 2,
}

#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum SlotKind {
    Limit = 0,
    Stop = 1,
}

/// Stable handle for cancel/modify. Packed into u32 (host) so the
/// device-side decision kernel can emit it as a single field.
///
/// Layout (LSB → MSB):
///   bits  0..  7 : slot_kind   (SlotKind as u8)
///   bits  8.. 15 : slot_index  (u8)
///   bits 16.. 31 : gen_counter (u16) — incremented on every slot reuse
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SlotTag(pub u32);

impl SlotTag {
    pub fn pack(kind: SlotKind, idx: u8, generation: u16) -> Self {
        SlotTag((kind as u32) | ((idx as u32) << 8) | ((generation as u32) << 16))
    }
    pub fn kind(self) -> SlotKind {
        match (self.0 & 0xFF) as u8 {
            0 => SlotKind::Limit,
            1 => SlotKind::Stop,
            other => panic!("SlotTag.kind() out of range: {other}"),
        }
    }
    pub fn index(self) -> u8 {
        ((self.0 >> 8) & 0xFF) as u8
    }
    pub fn generation(self) -> u16 {
        ((self.0 >> 16) & 0xFFFF) as u16
    }
}

/// Limit-order parameters; reused as both legs of an OCO submission.
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct LimitParams {
    pub side: Side,
    pub size: u16,
    pub px: i32, // price in ticks
    pub kind: LimitKind,
}

/// Host-side ergonomic enum. Used for configuration sources (policy YAML)
/// AND for the trade-log audit representation reconstructed from the
/// on-device `OrderEvent` records.
///
/// OCO is two limit legs (not recursive); on-device they are stored as
/// two slots linked by `oco_pair`. See spec §3.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum OrderIntent {
    SubmitMarket {
        side: Side,
        size: u16,
    },
    SubmitLimit {
        side: Side,
        size: u16,
        px: i32,
        kind: LimitKind,
    },
    SubmitStop {
        side: Side,
        size: u16,
        trigger_px: i32,
        limit_px: Option<i32>,
    },
    SubmitOcoLimits {
        leg_a: LimitParams,
        leg_b: LimitParams,
    },
    Cancel {
        slot_tag: SlotTag,
    },
    Modify {
        slot_tag: SlotTag,
        new_size: Option<u16>,
        new_px: Option<i32>,
    },
}

/// Flat 24-byte record emitted by the device-side decision kernel into
/// the per-block mapped-pinned audit ring. Host reads + reconstructs
/// `OrderIntent` for the trade log.
#[repr(C)]
#[derive(
    Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable, Serialize, Deserialize,
)]
pub struct OrderEvent {
    pub ts_ns: u64,       // 8
    pub slot_tag: u32,    // 4  — SlotTag.0
    pub kind: u8,         // 1  — OrderEventKind discriminant
    pub side: u8,         // 1
    pub size: u16,        // 2
    pub px: i32,          // 4  (ticks; 0 for market)
    pub trigger_px: i32,  // 4  (ticks; 0 if not stop)
    // total: 24 bytes
}

#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OrderEventKind {
    SubmitMarket = 0,
    SubmitLimit = 1,
    SubmitIoc = 2,
    SubmitFok = 3,
    SubmitStopMarket = 4,
    SubmitStopLimit = 5,
    Cancel = 6,
    Modify = 7,
}

/// Closed-trade record emitted when a position returns to flat (by
/// counter-fill or stop). Written by the pnl_track kernel into the
/// per-block trade-log mapped-pinned buffer. Pinned at 40 bytes.
///
/// USD amounts are fixed-point ×100 (e.g. $1.23 → 123) to keep the
/// device-side write purely integer-aligned.
#[repr(C)]
#[derive(
    Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable, Serialize, Deserialize,
)]
pub struct TradeRecord {
    pub entry_ts_ns: u64,         // 8
    pub exit_ts_ns: u64,          // 8
    pub entry_px_ticks: i32,      // 4
    pub exit_px_ticks: i32,       // 4
    pub size_lots: i32,           // 4  signed (+long, −short)
    pub fees_usd_fp: i32,         // 4
    pub realised_pnl_usd_fp: i32, // 4
    pub horizon_idx: u8,          // 1
    pub strategy_id: u8,          // 1
    pub _pad: [u8; 2],
    // total: 40 bytes
}

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

    #[test]
    fn slot_tag_roundtrips() {
        let t = SlotTag::pack(SlotKind::Stop, 7, 1234);
        assert_eq!(t.kind(), SlotKind::Stop);
        assert_eq!(t.index(), 7);
        assert_eq!(t.generation(), 1234);
    }

    #[test]
    fn slot_tag_max_gen_fits() {
        let t = SlotTag::pack(SlotKind::Limit, 31, u16::MAX);
        assert_eq!(t.generation(), u16::MAX);
        assert_eq!(t.index(), 31);
        assert_eq!(t.kind(), SlotKind::Limit);
    }

    #[test]
    fn slot_tag_packs_distinct_kinds() {
        let a = SlotTag::pack(SlotKind::Limit, 0, 0);
        let b = SlotTag::pack(SlotKind::Stop, 0, 0);
        assert_ne!(a.0, b.0);
    }

    #[test]
    fn order_event_is_24_bytes_pod() {
        assert_eq!(std::mem::size_of::<OrderEvent>(), 24);
        // Pod requires the type be Copy + 'static + no-padding; bytemuck
        // already enforces this at derive time. This test pins the size
        // so a future field-add forces the kernel-side struct to change too.
    }

    #[test]
    fn trade_record_is_40_bytes_pod() {
        assert_eq!(std::mem::size_of::<TradeRecord>(), 40);
    }

    #[test]
    fn intent_serde_roundtrip_for_oco() {
        let oco = OrderIntent::SubmitOcoLimits {
            leg_a: LimitParams {
                side: Side::Buy,
                size: 1,
                px: 550_000,
                kind: LimitKind::Limit,
            },
            leg_b: LimitParams {
                side: Side::Sell,
                size: 1,
                px: 551_000,
                kind: LimitKind::Limit,
            },
        };
        let j = serde_json::to_string(&oco).expect("ser");
        let back: OrderIntent = serde_json::from_str(&j).expect("de");
        match back {
            OrderIntent::SubmitOcoLimits { leg_a, leg_b } => {
                assert_eq!(leg_a.px, 550_000);
                assert_eq!(leg_b.px, 551_000);
            }
            _ => panic!("wrong variant after roundtrip"),
        }
    }

    #[test]
    fn intent_serde_roundtrip_for_cancel() {
        let tag = SlotTag::pack(SlotKind::Stop, 5, 42);
        let intent = OrderIntent::Cancel { slot_tag: tag };
        let j = serde_json::to_string(&intent).expect("ser");
        let back: OrderIntent = serde_json::from_str(&j).expect("de");
        match back {
            OrderIntent::Cancel { slot_tag } => {
                assert_eq!(slot_tag.kind(), SlotKind::Stop);
                assert_eq!(slot_tag.index(), 5);
                assert_eq!(slot_tag.generation(), 42);
            }
            _ => panic!("wrong variant"),
        }
    }
}
