// resting_orders.cu — limit + stop + OCO machinery.
//
// Runs on every event (after book_update, before pnl_track). Per-block,
// single-writer (thread 0). Processes:
//
//   1. In-flight → resting promotion when arrival_ts_ns reached.
//      Marketable on arrival? Cross immediately at the just-arrived book.
//   2. Resting limits: queue-decay against trade_signed_vol at level,
//      check marketability against the new book (price moved through us).
//   3. Stop arming/triggering: best ask crossing trigger up → buy stop,
//      best bid crossing trigger down → sell stop. On trigger:
//        - StopMarket: walk book immediately, fill, emit OrderEvent
//        - StopLimit: convert to a resting limit (allocate a LimitSlot)
//   4. OCO mutual exclusion: when one leg fills or stops triggers, free
//      the paired slot via oco_pair index (0..31 = limit, 32..47 = stop).
//
// Per feedback_no_atomicadd.md: no atomics — single writer per block.

#include "lob_state.cuh"

#define KIND_LIMIT       1
#define KIND_IOC         2
#define KIND_FOK         3
#define KIND_STOP_MARKET 4
#define KIND_STOP_LIMIT  5

// Book walking helpers — same shape as order_match.cu but inlined here
// to keep this kernel's symbol set self-contained.
//
// min_px / max_px: per-backtest price-range bounds (from upload_price_range /
// S1.19). Levels with lvl_px outside [min_px, max_px] are MBP-10 sentinels
// for empty depth slots — consuming them writes avg_px=0 or avg_px>21M into
// pos.vwap_entry (194 zero + 216 huge sentinel writes, S1.22 root-cause).
__device__ static float walk_ask_for_buy(
    const Book& book, float size, float& filled,
    float min_px, float max_px
) {
    float cost = 0.0f; filled = 0.0f;
    #pragma unroll
    for (int k = 0; k < 10 && size > 0.0f; ++k) {
        const float lvl_sz = book.ask_sz[k];
        const float lvl_px = book.ask_px[k];
        if (lvl_sz <= 0.0f || !isfinite(lvl_sz) || !isfinite(lvl_px)
            || lvl_px < min_px || lvl_px > max_px) continue;
        const float take = (size < lvl_sz) ? size : lvl_sz;
        cost += take * lvl_px; filled += take; size -= take;
    }
    return cost;
}
__device__ static float walk_bid_for_sell(
    const Book& book, float size, float& filled,
    float min_px, float max_px
) {
    float cost = 0.0f; filled = 0.0f;
    #pragma unroll
    for (int k = 0; k < 10 && size > 0.0f; ++k) {
        const float lvl_sz = book.bid_sz[k];
        const float lvl_px = book.bid_px[k];
        if (lvl_sz <= 0.0f || !isfinite(lvl_sz) || !isfinite(lvl_px)
            || lvl_px < min_px || lvl_px > max_px) continue;
        const float take = (size < lvl_sz) ? size : lvl_sz;
        cost += take * lvl_px; filled += take; size -= take;
    }
    return cost;
}

// Mirror of decision_policy.cu's emit_event — duplicated to avoid
// a cross-kernel header dependency. Layout matches OrderEvent (24 B)
// in crates/ml-backtesting/src/order.rs.
__device__ static void emit_audit(
    unsigned char* audit_base, unsigned int* head_d, int b, unsigned int ring_cap,
    unsigned long long ts_ns, unsigned int slot_tag, unsigned char kind,
    unsigned char side, unsigned short size, int px_ticks, int trigger_px_ticks
) {
    const unsigned int idx = head_d[b] % ring_cap;
    head_d[b] += 1;
    unsigned char* rec = audit_base + ((size_t)b * ring_cap + idx) * 24;
    *reinterpret_cast<unsigned long long*>(rec + 0) = ts_ns;
    *reinterpret_cast<unsigned int*>(rec + 8)       = slot_tag;
    rec[12] = kind;
    rec[13] = side;
    *reinterpret_cast<unsigned short*>(rec + 14)    = size;
    *reinterpret_cast<int*>(rec + 16)               = px_ticks;
    *reinterpret_cast<int*>(rec + 20)               = trigger_px_ticks;
}

// SlotTag pack: kind | (idx << 8) | (gen << 16). See src/order.rs.
__device__ static unsigned int pack_slot_tag(unsigned char kind, unsigned char idx, unsigned short gen) {
    return (unsigned int)kind | ((unsigned int)idx << 8) | ((unsigned int)gen << 16);
}

// Free the slot referenced by an oco_pair byte: 0..31 = limits, 32..47 = stops.
__device__ static void cancel_oco_pair(Orders& orders, unsigned char oco_pair) {
    if (oco_pair == 0xFF) return;
    if (oco_pair < MAX_LIMITS) {
        orders.limits[oco_pair].active = 0;
    } else if (oco_pair - MAX_LIMITS < MAX_STOPS) {
        orders.stops[oco_pair - MAX_LIMITS].active = 0;
    }
}

// Apply a fill to per-backtest position state. Same VWAP-scale-in /
// counter-direction realised-P&L rule as order_match.cu's market-order
// path; isolated here as a __device__ helper.
//
// P4: per-fill cost integration. After the close-leg realized_pnl math
// runs (so the unwind P&L is computed from gross prices), deduct the
// fill cost from realized_pnl AND accumulate into total_fees_per_b[b].
// Net-of-cost semantics: isv_kelly_update_on_close reads realized_pnl
// delta which is now net of cost — Kelly state learns realistic returns.
//
// Plain `+=` for total_fees_per_b is safe under the single-writer-per-block
// convention enforced by `if (threadIdx.x != 0) return;` at the top of
// every block-per-backtest kernel.
//
// S1.20: NaN instrumentation counters.
//   nan_avg_px        — incremented when avg_px = total_cost/filled_lots is NaN/Inf.
//   nan_realised      — incremented when realised P&L in counter-direction branch is NaN/Inf.
//   nan_realized_pnl  — incremented when pos.realized_pnl becomes non-finite after += or -=.
// On NaN-producing events the function returns early WITHOUT poisoning pos state.
//
// S1.21: per-vwap-write-site counters.
//   vwap_zero_open_flat / vwap_huge_open_flat  — open-from-flat branch (line 131).
//   vwap_zero_scale_in  / vwap_huge_scale_in   — scale-in branch (line 136).
//   vwap_zero_flip      / vwap_huge_flip        — flip-beyond branch (line 150).
//   last_bad_vwap / last_bad_path               — capture site + value.
__device__ static void apply_fill_to_pos(
    Pos& pos,
    float filled_lots,
    float total_cost,
    unsigned char side,
    int b,
    const float* __restrict__ cost_per_lot_per_side_per_b,
    float* __restrict__ total_fees_per_b,
    unsigned int* __restrict__ nan_avg_px,
    unsigned int* __restrict__ nan_realised,
    unsigned int* __restrict__ nan_realized_pnl,
    unsigned int* __restrict__ vwap_zero_open_flat,
    unsigned int* __restrict__ vwap_huge_open_flat,
    unsigned int* __restrict__ vwap_zero_scale_in,
    unsigned int* __restrict__ vwap_huge_scale_in,
    unsigned int* __restrict__ vwap_zero_flip,
    unsigned int* __restrict__ vwap_huge_flip,
    float*        __restrict__ last_bad_vwap,
    unsigned int* __restrict__ last_bad_path
) {
    if (filled_lots <= 0.0f) return;
    const float avg_px = total_cost / filled_lots;
    if (!isfinite(avg_px)) {
        nan_avg_px[b] += 1u;
        return;  // do NOT propagate NaN into pos state
    }
    const float sgn = (side == 0) ? 1.0f : -1.0f;
    const float prev_pos_f = (float)pos.position_lots;
    const float new_pos_f  = prev_pos_f + sgn * filled_lots;

    if (pos.position_lots == 0) {
        pos.vwap_entry = avg_px;
        // S1.21: open-from-flat write site.
        if (pos.vwap_entry == 0.0f) {
            vwap_zero_open_flat[b] += 1u;
            last_bad_vwap[b] = 0.0f;
            last_bad_path[b] = 1u;
        } else if (!isfinite(pos.vwap_entry) || fabsf(pos.vwap_entry) > 21000000.0f) {
            vwap_huge_open_flat[b] += 1u;
            last_bad_vwap[b] = pos.vwap_entry;
            last_bad_path[b] = 1u;
        }
    } else if ((prev_pos_f > 0.0f) == (sgn > 0.0f)) {
        const float prev_notional = prev_pos_f * pos.vwap_entry;
        const float new_notional  = prev_notional + sgn * total_cost;
        const float new_abs       = (new_pos_f > 0.0f) ? new_pos_f : -new_pos_f;
        pos.vwap_entry = (new_abs > 0.0f) ? (new_notional / (sgn * new_abs)) : 0.0f;
        // S1.21: scale-in write site.
        if (pos.vwap_entry == 0.0f) {
            vwap_zero_scale_in[b] += 1u;
            last_bad_vwap[b] = 0.0f;
            last_bad_path[b] = 2u;
        } else if (!isfinite(pos.vwap_entry) || fabsf(pos.vwap_entry) > 21000000.0f) {
            vwap_huge_scale_in[b] += 1u;
            last_bad_vwap[b] = pos.vwap_entry;
            last_bad_path[b] = 3u;
        }
    } else {
        const float prev_abs = (prev_pos_f > 0.0f) ? prev_pos_f : -prev_pos_f;
        const float unwind   = (filled_lots < prev_abs) ? filled_lots : prev_abs;
        const float dir_was_long = (prev_pos_f > 0.0f) ? 1.0f : -1.0f;
        const float realised = (avg_px - pos.vwap_entry) * dir_was_long * unwind;
        if (!isfinite(realised)) {
            nan_realised[b] += 1u;
            return;  // do not poison realized_pnl
        }
        pos.realized_pnl += realised;
        if (!isfinite(pos.realized_pnl)) {
            nan_realized_pnl[b] += 1u;
        }
        if (filled_lots > prev_abs) {
            pos.vwap_entry = avg_px;
            // S1.21: flip-beyond write site.
            if (pos.vwap_entry == 0.0f) {
                vwap_zero_flip[b] += 1u;
                last_bad_vwap[b] = 0.0f;
                last_bad_path[b] = 4u;
            } else if (!isfinite(pos.vwap_entry) || fabsf(pos.vwap_entry) > 21000000.0f) {
                vwap_huge_flip[b] += 1u;
                last_bad_vwap[b] = pos.vwap_entry;
                last_bad_path[b] = 5u;
            }
        }
    }
    pos.position_lots = (int)new_pos_f;

    // P4: per-fill cost deduction (single-writer-per-block; plain += safe).
    const float fill_cost = filled_lots * cost_per_lot_per_side_per_b[b];
    pos.realized_pnl       -= fill_cost;
    if (!isfinite(pos.realized_pnl)) {
        nan_realized_pnl[b] += 1u;
    }
    total_fees_per_b[b]    += fill_cost;
}

// Free-slot search. Returns slot index 0..31 or -1 if all in use.
__device__ static int find_free_limit_slot(Orders& orders) {
    #pragma unroll
    for (int i = 0; i < MAX_LIMITS; ++i) {
        if (orders.limits[i].active == 0) return i;
    }
    return -1;
}

// Main resting-orders processing kernel.
//
// `tick_size` is the price-to-tick conversion for audit-event recording.
extern "C" __global__ void resting_orders_step(
    const Book* __restrict__       books,             // [n_backtests]
    unsigned char*                  orders_base,       // [n_backtests * ORDERS_BYTES]
    unsigned char*                  pos_base,
    unsigned char*                  audit_base,
    unsigned int*                   audit_head,
    unsigned long long              current_ts_ns,
    float                           trade_signed_vol,
    int                             orders_bytes,
    int                             pos_bytes,
    int                             ring_cap_events,
    float                           tick_size,
    // P4: per-fill cost integration (per-backtest arrays).
    const float*       __restrict__ cost_per_lot_per_side_per_b,
    float*             __restrict__ total_fees_per_b,
    // Fix 3: session-gap force-close. Tracks last event ts per backtest.
    unsigned long long* __restrict__ last_event_ts,    // [n_backtests]
    // S2.2: max_hold force-close at event rate.
    const unsigned long long* __restrict__ max_hold_ns_per_b,  // [n_backtests]
    const unsigned char* __restrict__      open_trade_state,   // [n_backtests * 64] (CRT.1 §7)
    // S1.20: NaN instrumentation counters (per-backtest).
    unsigned int*      __restrict__ nan_avg_px,
    unsigned int*      __restrict__ nan_realised,
    unsigned int*      __restrict__ nan_realized_pnl,
    // S1.21: per-vwap-write-site counters + last-bad-vwap capture.
    unsigned int*      __restrict__ vwap_zero_open_flat,
    unsigned int*      __restrict__ vwap_huge_open_flat,
    unsigned int*      __restrict__ vwap_zero_scale_in,
    unsigned int*      __restrict__ vwap_huge_scale_in,
    unsigned int*      __restrict__ vwap_zero_flip,
    unsigned int*      __restrict__ vwap_huge_flip,
    unsigned int*      __restrict__ vwap_session_gap_was_bad,
    float*             __restrict__ last_bad_vwap,
    unsigned int*      __restrict__ last_bad_path,
    // S1.22: per-level price-range validation in walk_ask_for_buy/walk_bid_for_sell.
    const float*       __restrict__ min_reasonable_px, // [n_backtests]
    const float*       __restrict__ max_reasonable_px, // [n_backtests]
    int                             n_backtests
) {
    int b = blockIdx.x;
    if (b >= n_backtests || threadIdx.x != 0) return;

    const Book& book = books[b];
    Orders& orders = *reinterpret_cast<Orders*>(orders_base + (size_t)b * orders_bytes);
    Pos&    pos    = *reinterpret_cast<Pos*>(pos_base + (size_t)b * pos_bytes);

    // S1.22: per-backtest price bounds for walk_* level filtering.
    const float min_px = min_reasonable_px[b];
    const float max_px = max_reasonable_px[b];

    // Session-gap force-close: when event timestamps jump > 1 hour (weekend
    // halt, session boundary, gap-fill), max_hold can't fire because no
    // intervening events advance current_ts. Force-close all open positions
    // before processing the new event's fills.
    const unsigned long long SESSION_GAP_NS = 3600000000000ull;  // 1 hour
    const unsigned long long last_ts = last_event_ts[b];
    if (last_ts > 0ull && current_ts_ns > last_ts
        && (current_ts_ns - last_ts) >= SESSION_GAP_NS
        && pos.position_lots != 0)
    {
        // Force-flat: zero position_lots directly. pnl_track_step (called by
        // step_resting_orders after this kernel) will detect the prev!=0 && now==0
        // transition via open_trade_state scratch and emit a close TradeRecord.
        // No synthetic P&L is added — records show realised_pnl from whatever
        // was accumulated before the gap (honest: cannot fill across a halt).
        // S1.21: capture pre-close vwap_entry corruption if already bad.
        if (pos.vwap_entry == 0.0f || !isfinite(pos.vwap_entry)
            || fabsf(pos.vwap_entry) > 21000000.0f)
        {
            vwap_session_gap_was_bad[b] += 1u;
            last_bad_vwap[b] = pos.vwap_entry;
            last_bad_path[b] = 6u;
        }
        pos.position_lots = 0;
    }
    last_event_ts[b] = current_ts_ns;

    // S2.2: max_hold force-close at event rate.
    // Runs every event so the 60s cap is actually enforceable.
    // A1: decision_stride removed; all enforcement is event-rate.
    const unsigned long long max_hold = max_hold_ns_per_b[b];
    if (max_hold > 0ull && pos.position_lots != 0) {
        const unsigned long long entry_ts =
            *reinterpret_cast<const unsigned long long*>(
                open_trade_state + (size_t)b * 64);  // OPEN_TRADE_STATE_BYTES = 64 (CRT.1 §7)
        if (entry_ts > 0ull && current_ts_ns >= entry_ts
            && (current_ts_ns - entry_ts) >= max_hold) {
            // S2.3: realize PnL on the force-close. apply_fill_to_pos's
            // counter-direction branch handles the unwind:
            //   realised = (avg_px − vwap) × dir × unwind
            // Synthesize the closing fill at top-of-book on the closing side:
            // long → sell at bid[0], short → buy at ask[0].
            // book[b]'s top-of-book is already validated by book_update's skip
            // (book_update_apply_snapshot rejects bad-top snapshots), so
            // close_px is guaranteed in [min_px, max_px] and > 0.
            const int  pos_lots   = pos.position_lots;
            const bool is_long    = pos_lots > 0;
            const float close_px  = is_long ? book.bid_px[0] : book.ask_px[0];
            const float lots_abs  = is_long ? (float)pos_lots : -(float)pos_lots;
            const float total_cost = lots_abs * close_px;
            const unsigned char close_side = is_long ? 1 : 0;  // sell to close long, buy to close short
            apply_fill_to_pos(
                pos, lots_abs, total_cost, close_side,
                b, cost_per_lot_per_side_per_b, total_fees_per_b,
                nan_avg_px, nan_realised, nan_realized_pnl,
                vwap_zero_open_flat, vwap_huge_open_flat,
                vwap_zero_scale_in,  vwap_huge_scale_in,
                vwap_zero_flip,      vwap_huge_flip,
                last_bad_vwap,       last_bad_path
            );
            // After apply_fill_to_pos with unwind == prev_abs, position_lots = 0.
        }
    }

    // (1) Promote in-flight limits to resting; (2) queue-decay; (3) marketability check.
    const float trade_abs = (trade_signed_vol < 0.0f) ? -trade_signed_vol : trade_signed_vol;
    for (int i = 0; i < MAX_LIMITS; ++i) {
        LimitSlot& s = orders.limits[i];
        if (s.active == 2 && current_ts_ns >= s.arrival_ts_ns) {
            s.active = 1;
            // Initialise queue position pessimistically: full level depth
            // ahead of us at the just-arrived book (per spec §9 queue-decay
            // risk row).
            int level_idx = -1;
            if (s.side == 0) {
                #pragma unroll
                for (int k = 0; k < 10; ++k) if (book.bid_px[k] == s.price) { level_idx = k; break; }
                s.queue_position = (level_idx >= 0) ? book.bid_sz[level_idx] : 0.0f;
            } else {
                #pragma unroll
                for (int k = 0; k < 10; ++k) if (book.ask_px[k] == s.price) { level_idx = k; break; }
                s.queue_position = (level_idx >= 0) ? book.ask_sz[level_idx] : 0.0f;
            }
        }
        if (s.active != 1) continue;

        // Queue decay: trade flow on our side at our price level eats ahead-of-us first,
        // then us. Only count trades on the SAME side as our resting order.
        if (trade_abs > 0.0f) {
            const bool same_side_trade = (s.side == 0 && trade_signed_vol < 0.0f)  // resting bid + seller hits us
                                       || (s.side == 1 && trade_signed_vol > 0.0f); // resting ask + buyer lifts
            if (same_side_trade) {
                if (s.queue_position >= trade_abs) {
                    s.queue_position -= trade_abs;
                } else {
                    // Sentinel-priced (market-like) orders use s.price = 1e9 (buy)
                    // or 0 (sell) so the marketability check below always crosses.
                    // The queue-decay arm MUST NOT use s.price for cost because it
                    // would inject the sentinel into avg_px (S1.23 root cause:
                    // zero_flat=194, huge_flat=216). Absorb the queue depletion
                    // only; the marketability check fires this same iteration via
                    // walk_* with book-derived prices.
                    const bool is_sentinel_price = (s.price < min_px) || (s.price > max_px);
                    if (is_sentinel_price) {
                        s.queue_position = 0.0f;
                    } else {
                        const float over = trade_abs - s.queue_position;
                        s.queue_position = 0.0f;
                        const float take = (over < s.size_remaining) ? over : s.size_remaining;
                        if (take > 0.0f) {
                            // Filled `take` lots at our limit price (queue position reached us).
                            const float cost = take * s.price;
                            apply_fill_to_pos(pos, take, cost, s.side,
                                              b, cost_per_lot_per_side_per_b, total_fees_per_b,
                                              nan_avg_px, nan_realised, nan_realized_pnl,
                                              vwap_zero_open_flat, vwap_huge_open_flat,
                                              vwap_zero_scale_in,  vwap_huge_scale_in,
                                              vwap_zero_flip,      vwap_huge_flip,
                                              last_bad_vwap,       last_bad_path);
                            s.size_remaining -= take;
                            emit_audit(audit_base, audit_head, b, (unsigned int)ring_cap_events,
                                       current_ts_ns,
                                       pack_slot_tag(0 /*SlotKind::Limit*/, (unsigned char)i, s.gen_counter),
                                       1 /*SubmitLimit*/, s.side, (unsigned short)take,
                                       (int)(s.price / tick_size + 0.5f), 0);
                            if (s.size_remaining <= 0.0f) {
                                cancel_oco_pair(orders, s.oco_pair);
                                s.active = 0;
                                continue;
                            }
                        }
                    }
                }
            }
        }

        // Marketability check: book moved through our price → cross at the
        // arrival book (slippage-aware fill). Triggers on:
        //   buy limit @ p ≥ ask_px[0]   — we'd be taking liquidity at our price or better
        //   sell limit @ p ≤ bid_px[0]  — same on opposite side
        const bool marketable = (s.side == 0 && s.price >= book.ask_px[0])
                             || (s.side == 1 && s.price <= book.bid_px[0]);
        if (marketable) {
            float filled = 0.0f;
            const float cost = (s.side == 0)
                ? walk_ask_for_buy (book, s.size_remaining, filled, min_px, max_px)
                : walk_bid_for_sell(book, s.size_remaining, filled, min_px, max_px);
            if (filled > 0.0f) {
                apply_fill_to_pos(pos, filled, cost, s.side,
                                  b, cost_per_lot_per_side_per_b, total_fees_per_b,
                                  nan_avg_px, nan_realised, nan_realized_pnl,
                                  vwap_zero_open_flat, vwap_huge_open_flat,
                                  vwap_zero_scale_in,  vwap_huge_scale_in,
                                  vwap_zero_flip,      vwap_huge_flip,
                                  last_bad_vwap,       last_bad_path);
                s.size_remaining -= filled;
                emit_audit(audit_base, audit_head, b, (unsigned int)ring_cap_events,
                           current_ts_ns,
                           pack_slot_tag(0, (unsigned char)i, s.gen_counter),
                           s.kind /*Limit|Ioc|Fok*/, s.side, (unsigned short)filled,
                           (int)(s.price / tick_size + 0.5f), 0);
            }
            // IOC: any unfilled remainder cancelled immediately.
            // FOK: if not fully filled, cancel everything (no partial leaves resting).
            // Limit: leave residual resting at our price.
            if (s.kind == KIND_IOC || (s.kind == KIND_FOK && s.size_remaining > 0.0f)) {
                if (s.kind == KIND_FOK && filled > 0.0f) {
                    // FOK partial fill is forbidden — should not happen; defensive guard
                    // documents intent but the actual prevention belongs in submission code.
                }
                cancel_oco_pair(orders, s.oco_pair);
                s.active = 0;
                continue;
            }
            if (s.size_remaining <= 0.0f) {
                cancel_oco_pair(orders, s.oco_pair);
                s.active = 0;
                continue;
            }
        }
    }

    // (4) Stop trigger detection.
    for (int j = 0; j < MAX_STOPS; ++j) {
        StopSlot& st = orders.stops[j];
        if (st.active == 2 && current_ts_ns >= st.arrival_ts_ns) st.active = 1;
        if (st.active != 1) continue;

        const bool triggered = (st.side == 0 && book.ask_px[0] >= st.trigger_price)
                            || (st.side == 1 && book.bid_px[0] <= st.trigger_price);
        if (!triggered) continue;
        st.active = 0;

        if (st.kind == KIND_STOP_MARKET) {
            float filled = 0.0f;
            const float cost = (st.side == 0)
                ? walk_ask_for_buy (book, st.size, filled, min_px, max_px)
                : walk_bid_for_sell(book, st.size, filled, min_px, max_px);
            if (filled > 0.0f) {
                apply_fill_to_pos(pos, filled, cost, st.side,
                                  b, cost_per_lot_per_side_per_b, total_fees_per_b,
                                  nan_avg_px, nan_realised, nan_realized_pnl,
                                  vwap_zero_open_flat, vwap_huge_open_flat,
                                  vwap_zero_scale_in,  vwap_huge_scale_in,
                                  vwap_zero_flip,      vwap_huge_flip,
                                  last_bad_vwap,       last_bad_path);
                emit_audit(audit_base, audit_head, b, (unsigned int)ring_cap_events,
                           current_ts_ns,
                           pack_slot_tag(1 /*SlotKind::Stop*/, (unsigned char)j, st.gen_counter),
                           4 /*SubmitStopMarket*/, st.side, (unsigned short)filled,
                           0, (int)(st.trigger_price / tick_size + 0.5f));
            }
            cancel_oco_pair(orders, st.oco_pair);
        } else if (st.kind == KIND_STOP_LIMIT) {
            // Convert to a resting limit at st.limit_price (may not fill immediately
            // if the limit is not marketable at current book).
            const int free_idx = find_free_limit_slot(orders);
            if (free_idx < 0) {
                pos.submission_overflow += 1u;
                cancel_oco_pair(orders, st.oco_pair);
                continue;
            }
            LimitSlot& ns = orders.limits[free_idx];
            ns.active = 1;
            ns.side = st.side;
            ns.kind = KIND_LIMIT;
            ns.oco_pair = st.oco_pair;
            ns.price = st.limit_price;
            ns.size_remaining = st.size;
            ns.queue_position = 0.0f;          // marketable check next iter will fire if applicable
            ns.arrival_ts_ns = current_ts_ns;
            ns.gen_counter = (unsigned short)(ns.gen_counter + 1);
            emit_audit(audit_base, audit_head, b, (unsigned int)ring_cap_events,
                       current_ts_ns,
                       pack_slot_tag(1 /*SlotKind::Stop*/, (unsigned char)j, st.gen_counter),
                       5 /*SubmitStopLimit*/, st.side, (unsigned short)st.size,
                       (int)(st.limit_price / tick_size + 0.5f),
                       (int)(st.trigger_price / tick_size + 0.5f));
        }
    }
}

// Host-callable seed helpers — write a single slot. Single-block,
// single-thread. Used for fixture testing + as the kernel side of
// LobSimCuda::submit_limit. SlotKind::Limit only (Stop seed below).

extern "C" __global__ void seed_limit_slot(
    unsigned char* orders_base,
    int            orders_bytes,
    int            b,
    int            slot_idx,
    unsigned char  side,
    unsigned char  kind,
    unsigned char  active_state,    // 1=resting immediately, 2=in-flight
    unsigned char  oco_pair,
    float          price,
    float          size,
    float          queue_position_init,
    unsigned long long arrival_ts_ns,
    unsigned int*  overflow_flag    // [1] — set to 1 if slot_idx already in use
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;
    Orders* orders = reinterpret_cast<Orders*>(orders_base + (size_t)b * orders_bytes);
    if (slot_idx < 0 || slot_idx >= MAX_LIMITS) { *overflow_flag = 1u; return; }
    LimitSlot& s = orders->limits[slot_idx];
    if (s.active != 0) { *overflow_flag = 1u; return; }
    s.active = active_state;
    s.side = side;
    s.kind = kind;
    s.oco_pair = oco_pair;
    s.price = price;
    s.size_remaining = size;
    s.queue_position = queue_position_init;
    s.arrival_ts_ns = arrival_ts_ns;
    s.gen_counter = (unsigned short)(s.gen_counter + 1);
}

extern "C" __global__ void seed_stop_slot(
    unsigned char* orders_base,
    int            orders_bytes,
    int            b,
    int            slot_idx,
    unsigned char  side,
    unsigned char  kind,
    unsigned char  active_state,
    unsigned char  oco_pair,
    float          trigger_price,
    float          limit_price,
    float          size,
    unsigned long long arrival_ts_ns,
    unsigned int*  overflow_flag
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;
    Orders* orders = reinterpret_cast<Orders*>(orders_base + (size_t)b * orders_bytes);
    if (slot_idx < 0 || slot_idx >= MAX_STOPS) { *overflow_flag = 1u; return; }
    StopSlot& st = orders->stops[slot_idx];
    if (st.active != 0) { *overflow_flag = 1u; return; }
    st.active = active_state;
    st.side = side;
    st.kind = kind;
    st.oco_pair = oco_pair;
    st.trigger_price = trigger_price;
    st.limit_price = limit_price;
    st.size = size;
    st.arrival_ts_ns = arrival_ts_ns;
    st.gen_counter = (unsigned short)(st.gen_counter + 1);
}

// Try to allocate a limit slot from device-side submission code (e.g. C7
// decision kernel later) — returns slot index or -1 if all in use.
// Bumps pos.submission_overflow on failure. Single-block, single-thread.
extern "C" __global__ void submit_limit_alloc(
    unsigned char* orders_base,
    unsigned char* pos_base,
    int            orders_bytes,
    int            pos_bytes,
    int            b,
    unsigned char  side,
    unsigned char  kind,
    unsigned char  oco_pair,
    float          price,
    float          size,
    unsigned long long arrival_ts_ns,
    int*           allocated_slot_out  // [1] — written: slot idx or -1
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;
    Orders* orders = reinterpret_cast<Orders*>(orders_base + (size_t)b * orders_bytes);
    Pos*    pos    = reinterpret_cast<Pos*>(pos_base + (size_t)b * pos_bytes);
    const int idx = find_free_limit_slot(*orders);
    if (idx < 0) {
        pos->submission_overflow += 1u;
        *allocated_slot_out = -1;
        return;
    }
    LimitSlot& s = orders->limits[idx];
    s.active = 2;   // in-flight
    s.side = side;
    s.kind = kind;
    s.oco_pair = oco_pair;
    s.price = price;
    s.size_remaining = size;
    s.queue_position = 0.0f;
    s.arrival_ts_ns = arrival_ts_ns;
    s.gen_counter = (unsigned short)(s.gen_counter + 1);
    *allocated_slot_out = idx;
}

// P2: GPU-side seeder for in-flight LimitSlots driven by market_targets[b].
// Replaces the host-roundtrip loop in sim.rs::dispatch_latent_market_orders.
// Per-backtest single-writer (threadIdx.x == 0).
//
// §7 truth table:
//   side=0, abs_sz=N  → target signed position = +N (long)
//   side=1, abs_sz=N  → target signed position = -N (short)
//   side=2            → no-op: preserve current position
//   side=3            → force flat: target signed position = 0
//
// effective_position = pos.position_lots + Σ unfilled signed slot sizes
//   where unfilled = active∈{1,2} (resting or in-flight).
// delta = target_signed − effective_position.
// If delta == 0: nothing to do (idempotent re-submission under latency).
// If delta != 0: seed an in-flight order for |delta| lots on the
//   appropriate side. If all 32 slots are occupied, increments
//   pos.submission_overflow.
extern "C" __global__ void seed_inflight_limits_batched(
    unsigned char*              __restrict__ orders_base,                   // [n_backtests * orders_bytes]
    int                                       orders_bytes,
    Pos*                        __restrict__ positions,                     // [n_backtests]
    const int*                  __restrict__ market_targets,                // [n_backtests * 2]
    const unsigned int*         __restrict__ latency_ns_per_b,             // [n_backtests]
    unsigned long long                       current_ts_ns,
    // S2.1: max_hold enforcement diagnostic counter — incremented when this
    // kernel reads market_targets[b*2+0] == 3 (force-flat from stop_check_isv).
    unsigned int*               __restrict__ mh_force_flat_seen_by_seed,   // [n_backtests]
    // CRT.1 C1.3: no-trade band in lots. Seeding is skipped when
    // |target_signed − effective| < delta_floor_per_b[b].
    const float*                __restrict__ delta_floor_per_b,             // [n_backtests]
    int                                      n_backtests
) {
    int b = blockIdx.x;
    if (b >= n_backtests || threadIdx.x != 0) return;

    const int side   = market_targets[b * 2 + 0];
    const int abs_sz = market_targets[b * 2 + 1];

    // S2.1: count every time this kernel sees a force-flat target, regardless
    // of whether the delta calculation produces a non-zero order (delta==0 means
    // position is already flat, but the force-flat signal still arrived here).
    if (side == 3) {
        mh_force_flat_seen_by_seed[b] += 1u;
    }

    if (side == 2) return;   // §7: no-op = preserve position

    int target_signed;
    if      (side == 0) target_signed = +abs_sz;
    else if (side == 1) target_signed = -abs_sz;
    else                target_signed = 0;       // side == 3: force flat

    Orders* orders = reinterpret_cast<Orders*>(orders_base + (size_t)b * orders_bytes);

    // Effective position = filled position + all unfilled signed lots.
    int effective = positions[b].position_lots;
    #pragma unroll
    for (int s_idx = 0; s_idx < MAX_LIMITS; ++s_idx) {
        const LimitSlot& s = orders->limits[s_idx];
        if (s.active == 0) continue;
        const int slot_lots = (int)lroundf(s.size_remaining);
        effective += (s.side == 0) ? slot_lots : -slot_lots;
    }

    const int delta = target_signed - effective;

    // CRT.1 C1.3: no-trade band. Skip seeding when |delta| is below the
    // configured floor. Most events produce fractional target changes that
    // aren't worth the spread; the band absorbs them so the controller can
    // fire every event without generating an order on every fractional move.
    // Per spec §4.0 (v3 architecture reset). Without this gate, the v2
    // Gate-1 catastrophe (155k trades from 2M events) recurs regardless of
    // conviction smoothing.
    const float abs_delta_f = (delta > 0) ? (float)delta : (float)(-delta);
    if (abs_delta_f < delta_floor_per_b[b]) return;

    if (delta == 0) return;

    const int order_lots = (delta > 0) ? delta : -delta;
    const int order_side = (delta > 0) ? 0 : 1;

    // Find a free slot and seed an in-flight order.
    for (int s_idx = 0; s_idx < MAX_LIMITS; ++s_idx) {
        LimitSlot& s = orders->limits[s_idx];
        if (s.active == 0) {
            s.active           = 2;          // in-flight
            s.side             = (unsigned char)order_side;
            s.kind             = 1;          // Limit
            s.oco_pair         = 0xFF;
            s.price            = (order_side == 0) ? 1.0e9f : 0.0f;   // always crosses
            s.size_remaining   = (float)order_lots;
            s.queue_position   = 0.0f;
            s.arrival_ts_ns    = current_ts_ns + (unsigned long long)latency_ns_per_b[b];
            s.gen_counter      = (unsigned short)(s.gen_counter + 1);
            return;
        }
    }
    positions[b].submission_overflow += 1u;
}
