// FILE-SIZE-OK: 1184 lines — tests extracted to tests/processor_tests.rs
//! Core open deviation bar processing algorithm
//!
//! Implements non-lookahead bias open deviation bar construction where bars close when
//! price moves ±threshold dbps from the bar's OPEN price.

use crate::checkpoint::{
    AnomalySummary, Checkpoint, CheckpointError, PositionVerification, PriceWindow,
};
use crate::fixed_point::FixedPoint;
use crate::interbar::{InterBarConfig, TradeHistory}; // Issue #59
// Issue #59: Intra-bar features - using compute_intra_bar_features_with_config (Issue #128)
use crate::types::{AggTrade, OpenDeviationBar};
#[cfg(feature = "python")]
use pyo3::prelude::*;
use smallvec::SmallVec; // Issue #119: Trade accumulation with inline buffer (512 slots ≈ 29KB)
// Re-export ProcessingError from errors.rs (Phase 2a extraction)
pub use crate::errors::ProcessingError;
// Re-export ExportOpenDeviationBarProcessor from export_processor.rs (Phase 2d extraction)
pub use crate::export_processor::ExportOpenDeviationBarProcessor;

/// Open deviation bar processor with non-lookahead bias guarantee
pub struct OpenDeviationBarProcessor {
    /// Threshold in decimal basis points (250 = 25bps, v3.0.0+)
    threshold_decimal_bps: u32,

    /// Issue #96 Task #98: Pre-computed threshold ratio for fast delta calculation
    /// Stores (threshold_decimal_bps * SCALE) / BASIS_POINTS_SCALE as fixed-point
    /// This allows compute_range_thresholds() to compute delta = (price * ratio) / SCALE
    /// without repeated division, avoiding i128 arithmetic in hot path.
    /// Performance: Eliminates BASIS_POINTS_SCALE division on every bar creation.
    /// Made public for testing purposes.
    pub threshold_ratio: i64,

    /// Current bar state for streaming processing (Q19)
    /// Enables get_incomplete_bar() and stateful process_single_trade()
    current_bar_state: Option<OpenDeviationBarState>,

    /// Price window for checkpoint hash verification
    price_window: PriceWindow,

    /// Last processed trade ID (for gap detection on resume)
    last_trade_id: Option<i64>,

    /// Last completed bar's trade ID for dedup floor initialization (v1.4)
    ///
    /// Updated ONLY on bar completion (process_single_trade returns Some) and
    /// orphan emission (reset_at_ouroboros). NOT updated on every trade.
    /// This gives committed_floors a floor that excludes the forming bar tail,
    /// preventing suppression of bars at the REST-to-WS junction.
    last_completed_bar_tid: Option<i64>,

    /// Last processed timestamp (for position verification)
    last_timestamp_us: i64,

    /// Anomaly tracking for debugging
    anomaly_summary: AnomalySummary,

    /// Flag indicating this processor was created from a checkpoint
    /// When true, process_agg_trade_records will continue from existing bar state
    resumed_from_checkpoint: bool,

    /// Prevent bars from closing on same timestamp as they opened (Issue #36)
    ///
    /// When true (default): A bar cannot close until a trade arrives with a
    /// different timestamp than the bar's open_time. This prevents "instant bars"
    /// during flash crashes where multiple trades occur at the same millisecond.
    ///
    /// When false: Legacy behavior - bars can close on any breach regardless
    /// of timestamp, which may produce bars with identical timestamps.
    prevent_same_timestamp_close: bool,

    /// Deferred bar open flag (Issue #46)
    ///
    /// When true: The previous trade triggered a threshold breach and closed a bar.
    /// The next trade arriving via `process_single_trade()` should open a new bar
    /// instead of being treated as a continuation.
    ///
    /// This matches the batch path's `defer_open` semantics in
    /// `process_agg_trade_records()` where the breaching trade closes the current
    /// bar and the NEXT trade opens the new bar.
    defer_open: bool,

    /// Trade history for inter-bar feature computation (Issue #59)
    ///
    /// Ring buffer of recent trades for computing lookback-based features.
    /// When Some, features are computed from trades BEFORE each bar's open_time.
    /// When None, inter-bar features are disabled (all lookback_* fields = None).
    trade_history: Option<TradeHistory>,

    /// Configuration for inter-bar features (Issue #59)
    ///
    /// Controls lookback mode (fixed count or time window) and which feature
    /// tiers to compute. When None, inter-bar features are disabled.
    inter_bar_config: Option<InterBarConfig>,

    /// Enable intra-bar feature computation (Issue #59)
    ///
    /// When true, the processor accumulates trades during bar construction
    /// and computes 22 features from trades WITHIN each bar at bar close.
    /// Features include ITH (Investment Time Horizon), statistical, and
    /// complexity metrics. When false, all intra_* fields are None.
    include_intra_bar_features: bool,

    /// Issue #128: Configuration for intra-bar feature computation.
    /// Controls which complexity features (Hurst, PE) are computed.
    intra_bar_config: crate::intrabar::IntraBarConfig,

    /// Issue #112: Maximum timestamp gap in microseconds before discarding a forming bar
    ///
    /// When resuming from checkpoint with a forming bar, if the gap between
    /// the forming bar's close_time and the first incoming trade exceeds this
    /// threshold, the forming bar is discarded as an orphan (same as ouroboros reset).
    /// This prevents "oversized" bars caused by large data gaps (e.g., 38-hour outages).
    ///
    /// Default: 3,600,000,000 μs (1 hour)
    max_gap_us: i64,
}

/// Cold path: scan trades to find first unsorted pair and return error
/// Extracted from validate_trade_ordering() to improve hot-path code layout
#[cold]
#[inline(never)]
fn find_unsorted_trade(trades: &[AggTrade]) -> Result<(), ProcessingError> {
    for i in 1..trades.len() {
        let prev = &trades[i - 1];
        let curr = &trades[i];
        if curr.timestamp < prev.timestamp
            || (curr.timestamp == prev.timestamp && curr.agg_trade_id <= prev.agg_trade_id)
        {
            return Err(ProcessingError::UnsortedTrades {
                index: i,
                prev_time: prev.timestamp,
                prev_id: prev.agg_trade_id,
                curr_time: curr.timestamp,
                curr_id: curr.agg_trade_id,
            });
        }
    }
    Ok(())
}

/// Cold path: construct unsorted trade error
/// Extracted to keep error construction out of the hot validation loop
#[cold]
#[inline(never)]
fn unsorted_trade_error(
    index: usize,
    prev: &AggTrade,
    curr: &AggTrade,
) -> Result<(), ProcessingError> {
    Err(ProcessingError::UnsortedTrades {
        index,
        prev_time: prev.timestamp,
        prev_id: prev.agg_trade_id,
        curr_time: curr.timestamp,
        curr_id: curr.agg_trade_id,
    })
}

impl OpenDeviationBarProcessor {
    /// Create new processor with given threshold
    ///
    /// Uses default behavior: `prevent_same_timestamp_close = true` (Issue #36)
    ///
    /// # Arguments
    ///
    /// * `threshold_decimal_bps` - Threshold in **decimal basis points**
    ///   - Example: `250` → 25bps = 0.25%
    ///   - Example: `10` → 1bps = 0.01%
    ///   - Minimum: `1` → 0.1bps = 0.001%
    ///
    /// # Breaking Change (v3.0.0)
    ///
    /// Prior to v3.0.0, `threshold_decimal_bps` was in 1bps units.
    /// **Migration**: Multiply all threshold values by 10.
    pub fn new(threshold_decimal_bps: u32) -> Result<Self, ProcessingError> {
        Self::with_options(threshold_decimal_bps, true)
    }

    /// Create new processor with explicit timestamp gating control
    ///
    /// # Arguments
    ///
    /// * `threshold_decimal_bps` - Threshold in **decimal basis points**
    /// * `prevent_same_timestamp_close` - If true, bars cannot close until
    ///   timestamp advances from open_time. This prevents "instant bars" during
    ///   flash crashes. Set to false for legacy behavior (pre-v9).
    ///
    /// # Example
    ///
    /// ```ignore
    /// // Default behavior (v9+): timestamp gating enabled
    /// let processor = OpenDeviationBarProcessor::new(250)?;
    ///
    /// // Legacy behavior: allow instant bars
    /// let processor = OpenDeviationBarProcessor::with_options(250, false)?;
    /// ```
    pub fn with_options(
        threshold_decimal_bps: u32,
        prevent_same_timestamp_close: bool,
    ) -> Result<Self, ProcessingError> {
        // Validation bounds (v3.0.0: dbps units)
        // Min: 1 dbps = 0.001%
        // Max: 100,000 dbps = 100%
        if threshold_decimal_bps < 1 {
            return Err(ProcessingError::InvalidThreshold {
                threshold_decimal_bps,
            });
        }
        if threshold_decimal_bps > 100_000 {
            return Err(ProcessingError::InvalidThreshold {
                threshold_decimal_bps,
            });
        }

        // Issue #96 Task #98: Pre-compute threshold ratio
        // Ratio = (threshold_decimal_bps * SCALE) / BASIS_POINTS_SCALE
        // This is used in compute_range_thresholds() for fast delta calculation
        let threshold_ratio = ((threshold_decimal_bps as i64) * crate::fixed_point::SCALE)
            / (crate::fixed_point::BASIS_POINTS_SCALE as i64);

        Ok(Self {
            threshold_decimal_bps,
            threshold_ratio,
            current_bar_state: None,
            price_window: PriceWindow::new(),
            last_trade_id: None,
            last_completed_bar_tid: None,
            last_timestamp_us: 0,
            anomaly_summary: AnomalySummary::default(),
            resumed_from_checkpoint: false,
            prevent_same_timestamp_close,
            defer_open: false,
            trade_history: None,               // Issue #59: disabled by default
            inter_bar_config: None,            // Issue #59: disabled by default
            include_intra_bar_features: false, // Issue #59: disabled by default
            intra_bar_config: crate::intrabar::IntraBarConfig::default(), // Issue #128
            max_gap_us: 3_600_000_000,         // Issue #112: 1 hour default
        })
    }

    /// Get the prevent_same_timestamp_close setting
    pub fn prevent_same_timestamp_close(&self) -> bool {
        self.prevent_same_timestamp_close
    }

    /// Enable inter-bar feature computation with the given configuration (Issue #59)
    ///
    /// When enabled, the processor maintains a trade history buffer and computes
    /// lookback-based microstructure features on each bar close. Features are
    /// computed from trades that occurred BEFORE each bar's open_time, ensuring
    /// no lookahead bias.
    ///
    /// Uses a local entropy cache (default behavior, backward compatible).
    /// For multi-symbol workloads, use `with_inter_bar_config_and_cache()` with a global cache.
    ///
    /// # Arguments
    ///
    /// * `config` - Configuration controlling lookback mode and feature tiers
    ///
    /// # Example
    ///
    /// ```ignore
    /// use opendeviationbar_core::processor::OpenDeviationBarProcessor;
    /// use opendeviationbar_core::interbar::{InterBarConfig, LookbackMode};
    ///
    /// let processor = OpenDeviationBarProcessor::new(1000)?
    ///     .with_inter_bar_config(InterBarConfig {
    ///         lookback_mode: LookbackMode::FixedCount(500),
    ///         compute_tier2: true,
    ///         compute_tier3: true,
    ///         ..Default::default()
    ///     });
    /// ```
    pub fn with_inter_bar_config(self, config: InterBarConfig) -> Self {
        self.with_inter_bar_config_and_cache(config, None)
    }

    /// Enable inter-bar feature computation with optional external entropy cache
    ///
    /// Issue #145 Phase 3: Multi-Symbol Entropy Cache Sharing
    ///
    /// # Arguments
    ///
    /// * `config` - Configuration controlling lookback mode and feature tiers
    /// * `external_cache` - Optional shared entropy cache from `get_global_entropy_cache()`
    ///   - If provided: Uses the shared global cache (recommended for multi-symbol)
    ///   - If None: Creates a local 128-entry cache (default, backward compatible)
    ///
    /// # Usage
    ///
    /// ```ignore
    /// use opendeviationbar_core::{processor::OpenDeviationBarProcessor, entropy_cache_global::get_global_entropy_cache, interbar::InterBarConfig};
    ///
    /// // Single-symbol: use local cache (default)
    /// let processor = OpenDeviationBarProcessor::new(1000)?
    ///     .with_inter_bar_config(config);
    ///
    /// // Multi-symbol: share global cache
    /// let global_cache = get_global_entropy_cache();
    /// let processor = OpenDeviationBarProcessor::new(1000)?
    ///     .with_inter_bar_config_and_cache(config, Some(global_cache));
    /// ```
    pub fn with_inter_bar_config_and_cache(
        mut self,
        config: InterBarConfig,
        external_cache: Option<
            std::sync::Arc<parking_lot::RwLock<crate::interbar_math::EntropyCache>>,
        >,
    ) -> Self {
        self.trade_history = Some(TradeHistory::new_with_cache(config.clone(), external_cache));
        self.inter_bar_config = Some(config);
        self
    }

    /// Check if inter-bar features are enabled
    pub fn inter_bar_enabled(&self) -> bool {
        self.inter_bar_config.is_some()
    }

    /// Issue #112: Configure maximum timestamp gap for checkpoint recovery
    ///
    /// When resuming from checkpoint with a forming bar, if the gap between
    /// the forming bar's close_time and the first incoming trade exceeds this
    /// threshold, the forming bar is discarded as an orphan.
    ///
    /// # Arguments
    ///
    /// * `max_gap_us` - Maximum gap in microseconds (default: 3,600,000,000 = 1 hour)
    pub fn with_max_gap(mut self, max_gap_us: i64) -> Self {
        self.max_gap_us = max_gap_us;
        self
    }

    /// Get the maximum gap threshold in microseconds
    pub fn max_gap_us(&self) -> i64 {
        self.max_gap_us
    }

    /// Enable intra-bar feature computation (Issue #59)
    ///
    /// When enabled, the processor accumulates trades during bar construction
    /// and computes 22 features from trades WITHIN each bar at bar close:
    /// - 8 ITH features (Investment Time Horizon)
    /// - 12 statistical features (OFI, intensity, Kyle lambda, etc.)
    /// - 2 complexity features (Hurst exponent, permutation entropy)
    ///
    /// # Memory Note
    ///
    /// Trades are accumulated per-bar and freed when the bar closes.
    /// Typical 1000 dbps bar: ~50-500 trades, ~2-24 KB overhead.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let processor = OpenDeviationBarProcessor::new(1000)?
    ///     .with_intra_bar_features();
    /// ```
    pub fn with_intra_bar_features(mut self) -> Self {
        self.include_intra_bar_features = true;
        self
    }

    /// Check if intra-bar features are enabled
    pub fn intra_bar_enabled(&self) -> bool {
        self.include_intra_bar_features
    }

    /// Re-enable inter-bar features on an existing processor (Issue #97).
    ///
    /// Used after `from_checkpoint()` to restore microstructure config that
    /// is not preserved in checkpoint state. Uses a local entropy cache by default.
    /// For multi-symbol workloads, use `set_inter_bar_config_with_cache()` with a global cache.
    pub fn set_inter_bar_config(&mut self, config: InterBarConfig) {
        self.set_inter_bar_config_with_cache(config, None);
    }

    /// Re-enable inter-bar features with optional external entropy cache (Issue #145 Phase 3).
    ///
    /// Used after `from_checkpoint()` to restore microstructure config that
    /// is not preserved in checkpoint state. Allows specifying a shared entropy cache
    /// for multi-symbol processors.
    pub fn set_inter_bar_config_with_cache(
        &mut self,
        config: InterBarConfig,
        external_cache: Option<
            std::sync::Arc<parking_lot::RwLock<crate::interbar_math::EntropyCache>>,
        >,
    ) {
        self.trade_history = Some(TradeHistory::new_with_cache(config.clone(), external_cache));
        self.inter_bar_config = Some(config);
    }

    /// Re-enable intra-bar features on an existing processor (Issue #97).
    pub fn set_intra_bar_features(&mut self, enabled: bool) {
        self.include_intra_bar_features = enabled;
    }

    /// Issue #128: Set intra-bar feature configuration.
    /// Controls which complexity features (Hurst, PE) are computed.
    pub fn with_intra_bar_config(mut self, config: crate::intrabar::IntraBarConfig) -> Self {
        self.intra_bar_config = config;
        self
    }

    /// Issue #128: Set intra-bar feature configuration on existing processor.
    pub fn set_intra_bar_config(&mut self, config: crate::intrabar::IntraBarConfig) {
        self.intra_bar_config = config;
    }

    /// Process a single trade and return completed bar if any
    ///
    /// Maintains internal state for streaming use case. State persists across calls
    /// until a bar completes (threshold breach), enabling get_incomplete_bar().
    ///
    /// # Arguments
    ///
    /// * `trade` - Single aggregated trade to process
    ///
    /// # Returns
    ///
    /// `Some(OpenDeviationBar)` if a bar was completed, `None` otherwise
    ///
    /// # State Management
    ///
    /// - First trade: Initializes new bar state
    /// - Subsequent trades: Updates existing bar or closes on breach
    /// - Breach: Returns completed bar, starts new bar with breaching trade
    ///
    /// Issue #96 Task #78: Accept borrowed AggTrade to eliminate clones in fan-out loops.
    /// Streaming pipelines (4+ thresholds) were cloning ~57 byte trades per processor.
    /// Signature change to `&AggTrade` eliminates 4-8x unnecessary allocations.
    /// Issue #96 Task #84: `#[inline]` — main hot-path entry point called on every trade.
    #[inline]
    pub fn process_single_trade(
        &mut self,
        trade: &AggTrade,
    ) -> Result<Option<OpenDeviationBar>, ProcessingError> {
        // Track price and position for checkpoint
        self.price_window.push(trade.price);
        self.last_trade_id = Some(trade.agg_trade_id);
        self.last_timestamp_us = trade.timestamp;

        // Issue #59: Push trade to history buffer for inter-bar feature computation
        // This must happen BEFORE bar processing so lookback window includes recent trades
        if let Some(ref mut history) = self.trade_history {
            history.push(trade);
        }

        // Issue #46: If previous call triggered a breach, this trade opens the new bar.
        // This matches the batch path's defer_open semantics - the breaching trade
        // closes the current bar, and the NEXT trade opens the new bar.
        if self.defer_open {
            // Issue #68: Notify history that new bar is opening (preserves pre-bar trades)
            if let Some(ref mut history) = self.trade_history {
                history.on_bar_open(trade.timestamp);
            }
            self.current_bar_state = Some(if self.include_intra_bar_features {
                OpenDeviationBarState::new_with_trade_accumulation(trade, self.threshold_ratio)
            } else {
                OpenDeviationBarState::new(trade, self.threshold_ratio)
            });
            self.defer_open = false;
            return Ok(None);
        }

        match &mut self.current_bar_state {
            None => {
                // First trade - initialize new bar
                // Issue #68: Notify history that new bar is opening (preserves pre-bar trades)
                if let Some(ref mut history) = self.trade_history {
                    history.on_bar_open(trade.timestamp);
                }
                self.current_bar_state = Some(if self.include_intra_bar_features {
                    OpenDeviationBarState::new_with_trade_accumulation(trade, self.threshold_ratio)
                } else {
                    OpenDeviationBarState::new(trade, self.threshold_ratio)
                });
                Ok(None)
            }
            Some(bar_state) => {
                // Issue #59 & #96 Task #44: Accumulate trade for intra-bar features (before breach check)
                // Only accumulates if features enabled, avoiding unnecessary clones
                bar_state.accumulate_trade(trade, self.include_intra_bar_features);

                // Check for threshold breach
                let price_breaches = bar_state.bar.is_breach(
                    trade.price,
                    bar_state.upper_threshold,
                    bar_state.lower_threshold,
                );

                // Timestamp gate (Issue #36): prevent bars from closing on same timestamp
                // This eliminates "instant bars" during flash crashes where multiple trades
                // occur at the same millisecond.
                let timestamp_allows_close = !self.prevent_same_timestamp_close
                    || trade.timestamp != bar_state.bar.open_time;

                if price_breaches && timestamp_allows_close {
                    // Breach detected AND timestamp changed - close current bar
                    bar_state.bar.update_with_trade(trade);

                    // Validation: Ensure high/low include open/close extremes
                    debug_assert!(
                        bar_state.bar.high >= bar_state.bar.open.max(bar_state.bar.close)
                    );
                    debug_assert!(bar_state.bar.low <= bar_state.bar.open.min(bar_state.bar.close));

                    // Compute microstructure features at bar finalization (Issue #25)
                    bar_state.bar.compute_microstructure_features();

                    // Issue #59: Compute inter-bar features from lookback window
                    // Features are computed from trades BEFORE bar.open_time (no lookahead)
                    if let Some(ref mut history) = self.trade_history {
                        let inter_bar_features = history.compute_features(bar_state.bar.open_time);
                        bar_state.bar.set_inter_bar_features(&inter_bar_features);
                        // Issue #68: Notify history that bar is closing (resumes normal pruning)
                        history.on_bar_close();
                    }

                    // Issue #59: Compute intra-bar features from accumulated trades
                    if self.include_intra_bar_features {
                        // Issue #96 Task #173: Use reusable scratch buffers from bar_state
                        let intra_bar_features =
                            crate::intrabar::compute_intra_bar_features_with_config(
                                &bar_state.accumulated_trades,
                                &mut bar_state.scratch_prices,
                                &mut bar_state.scratch_volumes,
                                &self.intra_bar_config, // Issue #128
                            );
                        bar_state.bar.set_intra_bar_features(&intra_bar_features);
                    }

                    // Move bar out instead of cloning — bar_state borrow ends after
                    // last use above (NLL), so take() is safe here.
                    let completed_bar = self.current_bar_state.take().unwrap().bar;

                    // v1.4: Track last completed bar's trade ID for dedup floor
                    self.last_completed_bar_tid = Some(completed_bar.last_agg_trade_id);

                    // Issue #46: Don't start new bar with breaching trade.
                    // Next trade will open the new bar via defer_open.
                    self.defer_open = true;

                    Ok(Some(completed_bar))
                } else {
                    // Either no breach OR same timestamp (gate active) - update existing bar
                    bar_state.bar.update_with_trade(trade);
                    Ok(None)
                }
            }
        }
    }

    /// Get any incomplete bar currently being processed
    ///
    /// Returns clone of current bar state for inspection without consuming it.
    /// Useful for final bar at stream end or progress monitoring.
    ///
    /// # Returns
    ///
    /// `Some(OpenDeviationBar)` if bar is in progress, `None` if no active bar
    pub fn get_incomplete_bar(&self) -> Option<OpenDeviationBar> {
        self.current_bar_state.as_ref().map(|state| {
            let mut bar = state.bar.clone();
            // Issue #275: Compute microstructure features for incomplete bars.
            // Without this, duration_us (and derived trade_intensity) remain at
            // their default of 0 when the bar is inspected mid-construction.
            bar.compute_microstructure_features();
            bar
        })
    }

    /// Get the last processed aggregate trade ID (for gap detection on reconnect).
    ///
    /// Returns `None` for fresh processors that have not yet processed any trades.
    /// After checkpoint restore, returns the last trade ID from the checkpoint.
    pub fn last_agg_trade_id(&self) -> Option<i64> {
        self.last_trade_id
    }

    /// Get the last COMPLETED bar's aggregate trade ID.
    ///
    /// Unlike `last_agg_trade_id()` which includes the forming bar tail,
    /// this returns the trade ID from the most recently completed or orphaned bar.
    /// Used by committed_floors initialization to avoid suppressing junction bars.
    ///
    /// Returns `None` for fresh processors that have not yet completed any bar.
    pub fn last_completed_bar_tid(&self) -> Option<i64> {
        self.last_completed_bar_tid
    }

    /// Process AggTrade records into open deviation bars including incomplete bars for analysis
    ///
    /// # Arguments
    ///
    /// * `agg_trade_records` - Slice of AggTrade records sorted by (timestamp, agg_trade_id)
    ///
    /// # Returns
    ///
    /// Vector of open deviation bars including incomplete bars at end of data
    ///
    /// # Warning
    ///
    /// This method is for analysis purposes only. Incomplete bars violate the
    /// fundamental open deviation bar algorithm and should not be used for production trading.
    pub fn process_agg_trade_records_with_incomplete(
        &mut self,
        agg_trade_records: &[AggTrade],
    ) -> Result<Vec<OpenDeviationBar>, ProcessingError> {
        self.process_agg_trade_records_with_options(agg_trade_records, true)
    }

    /// Process Binance aggregated trade records into open deviation bars
    ///
    /// This is the primary method for converting AggTrade records (which aggregate
    /// multiple individual trades) into open deviation bars based on price movement thresholds.
    ///
    /// # Parameters
    ///
    /// * `agg_trade_records` - Slice of AggTrade records sorted by (timestamp, agg_trade_id)
    ///   Each record represents multiple individual trades aggregated at same price
    ///
    /// # Returns
    ///
    /// Vector of completed open deviation bars (ONLY bars that breached thresholds).
    /// Each bar tracks both individual trade count and AggTrade record count.
    pub fn process_agg_trade_records(
        &mut self,
        agg_trade_records: &[AggTrade],
    ) -> Result<Vec<OpenDeviationBar>, ProcessingError> {
        self.process_agg_trade_records_with_options(agg_trade_records, false)
    }

    /// Process AggTrade records with options for including incomplete bars
    ///
    /// Batch processing mode: Clears any existing state before processing.
    /// Use process_single_trade() for stateful streaming instead.
    ///
    /// # Parameters
    ///
    /// * `agg_trade_records` - Slice of AggTrade records sorted by (timestamp, agg_trade_id)
    /// * `include_incomplete` - Whether to include incomplete bars at end of processing
    ///
    /// # Returns
    ///
    /// Vector of open deviation bars (completed + incomplete if requested)
    pub fn process_agg_trade_records_with_options(
        &mut self,
        agg_trade_records: &[AggTrade],
        include_incomplete: bool,
    ) -> Result<Vec<OpenDeviationBar>, ProcessingError> {
        if agg_trade_records.is_empty() {
            return Ok(Vec::new());
        }

        // Validate records are sorted
        self.validate_trade_ordering(agg_trade_records)?;

        // Use existing bar state if resuming from checkpoint, otherwise start fresh
        // This is CRITICAL for cross-file continuation (Issues #2, #3)
        let mut current_bar: Option<OpenDeviationBarState> = if self.resumed_from_checkpoint {
            // Continue from checkpoint's incomplete bar
            self.resumed_from_checkpoint = false; // Consume the flag
            let restored_bar = self.current_bar_state.take();

            // Issue #112: Gap-aware checkpoint recovery
            // If the forming bar's close_time is too far from the first incoming trade,
            // discard it as an orphan to prevent oversized bars from data gaps.
            if let Some(ref bar_state) = restored_bar {
                let first_trade_ts = agg_trade_records[0].timestamp;
                let gap = first_trade_ts - bar_state.bar.close_time;
                if gap > self.max_gap_us {
                    self.anomaly_summary.record_gap();
                    // Discard forming bar — same treatment as ouroboros reset
                    None
                } else {
                    restored_bar
                }
            } else {
                restored_bar
            }
        } else {
            // Start fresh for normal batch processing
            self.current_bar_state = None;
            None
        };

        let mut bars = Vec::with_capacity(agg_trade_records.len() / 50); // Heuristic: 50 trades/bar covers consolidation regimes
        let mut defer_open = false;

        for agg_record in agg_trade_records {
            // Track price and position for checkpoint
            self.price_window.push(agg_record.price);
            self.last_trade_id = Some(agg_record.agg_trade_id);
            self.last_timestamp_us = agg_record.timestamp;

            // Issue #59: Push trade to history buffer for inter-bar feature computation
            if let Some(ref mut history) = self.trade_history {
                history.push(agg_record);
            }

            if defer_open {
                // Previous bar closed, this agg_record opens new bar
                // Issue #68: Notify history that new bar is opening (preserves pre-bar trades)
                if let Some(ref mut history) = self.trade_history {
                    history.on_bar_open(agg_record.timestamp);
                }
                current_bar = Some(if self.include_intra_bar_features {
                    OpenDeviationBarState::new_with_trade_accumulation(
                        agg_record,
                        self.threshold_ratio,
                    )
                } else {
                    OpenDeviationBarState::new(agg_record, self.threshold_ratio)
                });
                defer_open = false;
                continue;
            }

            match current_bar {
                None => {
                    // First bar initialization
                    // Issue #68: Notify history that new bar is opening (preserves pre-bar trades)
                    if let Some(ref mut history) = self.trade_history {
                        history.on_bar_open(agg_record.timestamp);
                    }
                    current_bar = Some(if self.include_intra_bar_features {
                        OpenDeviationBarState::new_with_trade_accumulation(
                            agg_record,
                            self.threshold_ratio,
                        )
                    } else {
                        OpenDeviationBarState::new(agg_record, self.threshold_ratio)
                    });
                }
                Some(ref mut bar_state) => {
                    // Issue #59 & #96 Task #44: Accumulate trade for intra-bar features (before breach check)
                    // Only accumulates if features enabled, avoiding unnecessary clones
                    bar_state.accumulate_trade(agg_record, self.include_intra_bar_features);

                    // Check if this AggTrade record breaches the threshold
                    let price_breaches = bar_state.bar.is_breach(
                        agg_record.price,
                        bar_state.upper_threshold,
                        bar_state.lower_threshold,
                    );

                    // Timestamp gate (Issue #36): prevent bars from closing on same timestamp
                    // This eliminates "instant bars" during flash crashes where multiple trades
                    // occur at the same millisecond.
                    let timestamp_allows_close = !self.prevent_same_timestamp_close
                        || agg_record.timestamp != bar_state.bar.open_time;

                    if price_breaches && timestamp_allows_close {
                        // Breach detected AND timestamp changed - update bar with breaching record
                        bar_state.bar.update_with_trade(agg_record);

                        // Validation: Ensure high/low include open/close extremes
                        debug_assert!(
                            bar_state.bar.high >= bar_state.bar.open.max(bar_state.bar.close)
                        );
                        debug_assert!(
                            bar_state.bar.low <= bar_state.bar.open.min(bar_state.bar.close)
                        );

                        // Compute microstructure features at bar finalization (Issue #34)
                        bar_state.bar.compute_microstructure_features();

                        // Issue #59: Compute inter-bar features from lookback window
                        if let Some(ref mut history) = self.trade_history {
                            let inter_bar_features =
                                history.compute_features(bar_state.bar.open_time);
                            bar_state.bar.set_inter_bar_features(&inter_bar_features);
                            // Issue #68: Notify history that bar is closing (resumes normal pruning)
                            history.on_bar_close();
                        }

                        // Issue #59: Compute intra-bar features from accumulated trades
                        if self.include_intra_bar_features {
                            // Issue #96 Task #173: Use reusable scratch buffers from bar_state
                            let intra_bar_features =
                                crate::intrabar::compute_intra_bar_features_with_config(
                                    &bar_state.accumulated_trades,
                                    &mut bar_state.scratch_prices,
                                    &mut bar_state.scratch_volumes,
                                    &self.intra_bar_config, // Issue #128
                                );
                            bar_state.bar.set_intra_bar_features(&intra_bar_features);
                        }

                        // Move bar out instead of cloning — bar_state borrow ends
                        // after last use above (NLL), so take() is safe here.
                        bars.push(current_bar.take().unwrap().bar);
                        // v1.4: Track last completed bar's trade ID for dedup floor
                        self.last_completed_bar_tid = Some(bars.last().unwrap().last_agg_trade_id);
                        defer_open = true; // Next record will open new bar
                    } else {
                        // Either no breach OR same timestamp (gate active) - normal update
                        bar_state.bar.update_with_trade(agg_record);
                    }
                }
            }
        }

        // Save current bar state for checkpoint and optionally append incomplete bar.
        // When include_incomplete=true, clone for checkpoint then consume for output.
        // When include_incomplete=false, move directly (no clone needed).
        if include_incomplete {
            // Issue #96 Task #95: Optimize checkpoint cloning with take() to avoid Vec allocation
            // (accumulated_trades not needed after intra-bar features computed).
            // Using std::mem::take() instead of clone+clear reduces allocation overhead.
            if let Some(ref state) = current_bar {
                // Construct checkpoint state without cloning accumulated_trades/scratch buffers
                // (they're not needed for checkpoint restoration). Avoids cloning ~3.5KB inline SmallVec.
                self.current_bar_state = Some(OpenDeviationBarState {
                    bar: state.bar.clone(),
                    upper_threshold: state.upper_threshold,
                    lower_threshold: state.lower_threshold,
                    accumulated_trades: SmallVec::new(),
                    scratch_prices: SmallVec::new(),
                    scratch_volumes: SmallVec::new(),
                });
            }

            // Add final partial bar only if explicitly requested
            // This preserves algorithm integrity: bars should only close on threshold breach
            if let Some(mut bar_state) = current_bar {
                // Compute microstructure features for incomplete bar (Issue #34)
                bar_state.bar.compute_microstructure_features();

                // Issue #59: Compute inter-bar features from lookback window
                if let Some(ref history) = self.trade_history {
                    let inter_bar_features = history.compute_features(bar_state.bar.open_time);
                    bar_state.bar.set_inter_bar_features(&inter_bar_features);
                }

                // Issue #59: Compute intra-bar features from accumulated trades
                if self.include_intra_bar_features {
                    // Issue #96 Task #173: Use reusable scratch buffers from bar_state
                    let intra_bar_features =
                        crate::intrabar::compute_intra_bar_features_with_config(
                            &bar_state.accumulated_trades,
                            &mut bar_state.scratch_prices,
                            &mut bar_state.scratch_volumes,
                            &self.intra_bar_config, // Issue #128
                        );
                    bar_state.bar.set_intra_bar_features(&intra_bar_features);
                }

                bars.push(bar_state.bar);
            }
        } else {
            // No incomplete bar appended — move ownership directly, no clone needed
            self.current_bar_state = current_bar;
        }

        Ok(bars)
    }

    // === CHECKPOINT METHODS ===

    /// Create checkpoint for cross-file continuation
    ///
    /// Captures current processing state for seamless continuation:
    /// - Incomplete bar (if any) with FIXED thresholds
    /// - Position tracking (timestamp, trade_id if available)
    /// - Price hash for verification
    ///
    /// # Arguments
    ///
    /// * `symbol` - Symbol being processed (e.g., "BTCUSDT", "EURUSD")
    ///
    /// # Example
    ///
    /// ```ignore
    /// let bars = processor.process_agg_trade_records(&trades)?;
    /// let checkpoint = processor.create_checkpoint("BTCUSDT");
    /// let json = serde_json::to_string(&checkpoint)?;
    /// std::fs::write("checkpoint.json", json)?;
    /// ```
    pub fn create_checkpoint(&self, symbol: &str) -> Checkpoint {
        let (incomplete_bar, thresholds) = match &self.current_bar_state {
            Some(state) => (
                Some(state.bar.clone()),
                Some((state.upper_threshold, state.lower_threshold)),
            ),
            None => (None, None),
        };

        let mut checkpoint = Checkpoint::new(
            symbol.to_string(),
            self.threshold_decimal_bps,
            incomplete_bar,
            thresholds,
            self.last_timestamp_us,
            self.last_trade_id,
            self.price_window.compute_hash(),
            self.prevent_same_timestamp_close,
        );
        // Issue #46: Persist defer_open state for cross-session continuity
        checkpoint.defer_open = self.defer_open;
        // v1.4: Persist last_completed_bar_tid for dedup floor initialization
        checkpoint.last_completed_bar_tid = self.last_completed_bar_tid;
        checkpoint
    }

    /// Resume processing from checkpoint
    ///
    /// Restores incomplete bar state with IMMUTABLE thresholds.
    /// Next trade continues building the bar until threshold breach.
    ///
    /// # Errors
    ///
    /// - `CheckpointError::MissingThresholds` - Checkpoint has bar but no thresholds
    ///
    /// # Example
    ///
    /// ```ignore
    /// let json = std::fs::read_to_string("checkpoint.json")?;
    /// let checkpoint: Checkpoint = serde_json::from_str(&json)?;
    /// let mut processor = OpenDeviationBarProcessor::from_checkpoint(checkpoint)?;
    /// let bars = processor.process_agg_trade_records(&next_file_trades)?;
    /// ```
    pub fn from_checkpoint(checkpoint: Checkpoint) -> Result<Self, CheckpointError> {
        // Issue #85 Phase 2: Apply checkpoint schema migration if needed
        let checkpoint = Self::migrate_checkpoint(checkpoint);

        // Issue #62: Validate threshold range before restoring from checkpoint
        // Valid range: 1-100,000 dbps (0.0001% to 10%)
        const THRESHOLD_MIN: u32 = 1;
        const THRESHOLD_MAX: u32 = 100_000;
        if checkpoint.threshold_decimal_bps < THRESHOLD_MIN
            || checkpoint.threshold_decimal_bps > THRESHOLD_MAX
        {
            return Err(CheckpointError::InvalidThreshold {
                threshold: checkpoint.threshold_decimal_bps,
                min_threshold: THRESHOLD_MIN,
                max_threshold: THRESHOLD_MAX,
            });
        }

        // Validate checkpoint consistency
        if checkpoint.incomplete_bar.is_some() && checkpoint.thresholds.is_none() {
            return Err(CheckpointError::MissingThresholds);
        }

        // Restore bar state if there's an incomplete bar
        // Note: accumulated_trades is reset to empty - intra-bar features won't be
        // accurate for bars resumed from checkpoint (partial trade history lost)
        let current_bar_state = match (checkpoint.incomplete_bar, checkpoint.thresholds) {
            (Some(bar), Some((upper, lower))) => Some(OpenDeviationBarState {
                bar,
                upper_threshold: upper,
                lower_threshold: lower,
                accumulated_trades: SmallVec::new(), // Lost on checkpoint - features may be partial
                scratch_prices: SmallVec::new(),
                scratch_volumes: SmallVec::new(),
            }),
            _ => None,
        };

        // Issue #96 Task #98: Pre-compute threshold ratio (same as with_options)
        let threshold_ratio = ((checkpoint.threshold_decimal_bps as i64)
            * crate::fixed_point::SCALE)
            / (crate::fixed_point::BASIS_POINTS_SCALE as i64);

        Ok(Self {
            threshold_decimal_bps: checkpoint.threshold_decimal_bps,
            threshold_ratio,
            current_bar_state,
            price_window: PriceWindow::new(), // Reset - will be rebuilt from new trades
            last_trade_id: checkpoint.last_trade_id,
            last_timestamp_us: checkpoint.last_timestamp_us,
            anomaly_summary: checkpoint.anomaly_summary,
            resumed_from_checkpoint: true, // Signal to continue from existing bar state
            prevent_same_timestamp_close: checkpoint.prevent_same_timestamp_close,
            defer_open: checkpoint.defer_open, // Issue #46: Restore deferred open state
            last_completed_bar_tid: checkpoint.last_completed_bar_tid, // v1.4: Restore dedup floor
            trade_history: None,               // Issue #59: Must be re-enabled after restore
            inter_bar_config: None,            // Issue #59: Must be re-enabled after restore
            include_intra_bar_features: false, // Issue #59: Must be re-enabled after restore
            intra_bar_config: crate::intrabar::IntraBarConfig::default(), // Issue #128
            max_gap_us: 3_600_000_000,         // Issue #112: 1 hour default
        })
    }

    /// Migrate checkpoint between schema versions
    /// Issue #85 Phase 2: Handle v1 → v2 migration
    /// Safe: JSON deserialization is field-name-based, so old v1 checkpoints load correctly
    fn migrate_checkpoint(mut checkpoint: Checkpoint) -> Checkpoint {
        match checkpoint.version {
            1 => {
                // v1 → v2: OpenDeviationBar struct field reordering (no behavioral changes)
                // JSON serialization is position-independent, so no transformation needed
                checkpoint.version = 2;
                checkpoint
            }
            2 => {
                // Already current version
                checkpoint
            }
            _ => {
                // Unknown version - log warning and continue with best effort
                eprintln!(
                    "Warning: Checkpoint has unknown version {}, treating as v2",
                    checkpoint.version
                );
                checkpoint.version = 2;
                checkpoint
            }
        }
    }

    /// Verify we're at the right position in the data stream
    ///
    /// Call with first trade of new file to verify continuity.
    /// Returns verification result indicating if there's a gap or exact match.
    ///
    /// # Arguments
    ///
    /// * `first_trade` - First trade of the new file/chunk
    ///
    /// # Example
    ///
    /// ```ignore
    /// let processor = OpenDeviationBarProcessor::from_checkpoint(checkpoint)?;
    /// let verification = processor.verify_position(&next_file_trades[0]);
    /// match verification {
    ///     PositionVerification::Exact => println!("Perfect continuation!"),
    ///     PositionVerification::Gap { missing_count, .. } => {
    ///         println!("Warning: {} trades missing", missing_count);
    ///     }
    ///     PositionVerification::TimestampOnly { gap_ms } => {
    ///         println!("Exness data: {}ms gap", gap_ms);
    ///     }
    /// }
    /// ```
    pub fn verify_position(&self, first_trade: &AggTrade) -> PositionVerification {
        match self.last_trade_id {
            Some(last_id) => {
                // Binance: has trade IDs - check for gaps
                let expected_id = last_id + 1;
                if first_trade.agg_trade_id == expected_id {
                    PositionVerification::Exact
                } else {
                    let missing_count = first_trade.agg_trade_id - expected_id;
                    PositionVerification::Gap {
                        expected_id,
                        actual_id: first_trade.agg_trade_id,
                        missing_count,
                    }
                }
            }
            None => {
                // Exness: no trade IDs - use timestamp only
                let gap_us = first_trade.timestamp - self.last_timestamp_us;
                let gap_ms = gap_us / 1000;
                PositionVerification::TimestampOnly { gap_ms }
            }
        }
    }

    /// Get the current anomaly summary
    pub fn anomaly_summary(&self) -> &AnomalySummary {
        &self.anomaly_summary
    }

    /// Get the threshold in decimal basis points
    pub fn threshold_decimal_bps(&self) -> u32 {
        self.threshold_decimal_bps
    }

    /// Validate that trades are properly sorted for deterministic processing
    ///
    /// Issue #96 Task #62: Early-exit optimization for sorted data
    /// For typical workloads (95%+ sorted), quick first/last check identifies
    /// unsorted batches immediately without full O(n) validation.
    fn validate_trade_ordering(&self, trades: &[AggTrade]) -> Result<(), ProcessingError> {
        if trades.is_empty() {
            return Ok(());
        }

        // Issue #96 Task #62: Fast-path check for obviously unsorted data
        // If first and last trades are not ordered, data is definitely unsorted
        // This early-exit catches common failures without full validation
        let first = &trades[0];
        let last = &trades[trades.len() - 1];

        if last.timestamp < first.timestamp
            || (last.timestamp == first.timestamp && last.agg_trade_id <= first.agg_trade_id)
        {
            // Definitely unsorted - find exact error location (cold path)
            return find_unsorted_trade(trades);
        }

        // Full validation for typical sorted case
        for i in 1..trades.len() {
            let prev = &trades[i - 1];
            let curr = &trades[i];

            // Check ordering: (timestamp, agg_trade_id) ascending
            if curr.timestamp < prev.timestamp
                || (curr.timestamp == prev.timestamp && curr.agg_trade_id <= prev.agg_trade_id)
            {
                return unsorted_trade_error(i, prev, curr);
            }
        }

        Ok(())
    }

    /// Reset processor state at a UTC-midnight ouroboros boundary (day mode only).
    ///
    /// Clears the incomplete bar and position tracking while preserving
    /// the threshold configuration. Use this when starting fresh at a
    /// known boundary for reproducibility.
    ///
    /// # Returns
    ///
    /// The orphaned incomplete bar (if any) so caller can decide
    /// whether to include it in results with `is_orphan=True` flag.
    ///
    /// # Example
    ///
    /// ```ignore
    /// // At year boundary (Jan 1 00:00:00 UTC)
    /// let orphaned = processor.reset_at_ouroboros();
    /// if let Some(bar) = orphaned {
    ///     // Handle incomplete bar from previous year
    /// }
    /// // Continue processing new year's data with clean state
    /// ```
    pub fn reset_at_ouroboros(&mut self) -> Option<OpenDeviationBar> {
        let orphaned = self.current_bar_state.take().map(|mut state| {
            // Issue #275: Compute microstructure features for orphan bars.
            // Without this, duration_us (and derived trade_intensity) remain at
            // their default of 0, even though close_time − open_time > 0.
            state.bar.compute_microstructure_features();
            state.bar
        });
        // v1.4: Track orphan bar's trade ID for dedup floor (orphan IS a completed bar)
        // Do NOT reset last_completed_bar_tid to None — it persists across day boundaries
        if let Some(ref bar) = orphaned {
            self.last_completed_bar_tid = Some(bar.last_agg_trade_id);
        }
        self.price_window = PriceWindow::new();
        self.last_trade_id = None;
        self.last_timestamp_us = 0;
        self.resumed_from_checkpoint = false;
        self.defer_open = false;
        // Issue #81: Clear bar boundary tracking at ouroboros reset.
        // Trades are preserved — still valid lookback for first bar of new segment.
        if let Some(ref mut history) = self.trade_history {
            history.reset_bar_boundaries();
        }
        orphaned
    }
}

/// Internal state for a open deviation bar being built
#[derive(Clone)]
struct OpenDeviationBarState {
    /// The open deviation bar being constructed
    pub bar: OpenDeviationBar,

    /// Upper breach threshold (FIXED from bar open)
    pub upper_threshold: FixedPoint,

    /// Lower breach threshold (FIXED from bar open)
    pub lower_threshold: FixedPoint,

    /// Accumulated trades for intra-bar feature computation (Issue #59)
    ///
    /// When intra-bar features are enabled, trades are accumulated here
    /// during bar construction and used to compute features at bar close.
    /// Cleared when bar closes to free memory.
    /// Issue #136: Optimized from 512→64→48 slots.
    /// Profile data: max trades/bar = 26 (P99 = 14), so 64 slots provides
    /// 2.46x safety margin. SmallVec transparently spills to heap if exceeded.
    pub accumulated_trades: SmallVec<[AggTrade; 64]>,

    /// Issue #96: Scratch buffer for intra-bar price extraction
    /// SmallVec<[f64; 64]> keeps 95%+ of bars on stack (P99 trades/bar = 14, max = 26)
    /// Eliminates heap allocation for typical bars, spills transparently for large ones
    pub scratch_prices: SmallVec<[f64; 64]>,

    /// Issue #96: Scratch buffer for intra-bar volume extraction
    /// Same sizing rationale as scratch_prices
    pub scratch_volumes: SmallVec<[f64; 64]>,
}

impl OpenDeviationBarState {
    /// Create new open deviation bar state from opening trade
    /// Issue #96 Task #98: Accept pre-computed threshold_ratio for fast threshold calculation
    #[inline]
    fn new(trade: &AggTrade, threshold_ratio: i64) -> Self {
        let bar = OpenDeviationBar::new(trade);

        // Issue #96 Task #98: Use cached ratio instead of repeated division
        // This avoids BASIS_POINTS_SCALE division on every bar creation
        let (upper_threshold, lower_threshold) =
            bar.open.compute_range_thresholds_cached(threshold_ratio);

        Self {
            bar,
            upper_threshold,
            lower_threshold,
            accumulated_trades: SmallVec::new(),
            scratch_prices: SmallVec::new(),
            scratch_volumes: SmallVec::new(),
        }
    }

    /// Create new open deviation bar state with intra-bar feature accumulation
    /// Issue #96 Task #98: Accept pre-computed threshold_ratio for fast threshold calculation
    #[inline]
    fn new_with_trade_accumulation(trade: &AggTrade, threshold_ratio: i64) -> Self {
        let bar = OpenDeviationBar::new(trade);

        // Issue #96 Task #98: Use cached ratio instead of repeated division
        // This avoids BASIS_POINTS_SCALE division on every bar creation
        let (upper_threshold, lower_threshold) =
            bar.open.compute_range_thresholds_cached(threshold_ratio);

        Self {
            bar,
            upper_threshold,
            lower_threshold,
            accumulated_trades: {
                let mut sv = SmallVec::new();
                sv.push(*trade);
                sv
            },
            scratch_prices: SmallVec::new(),
            scratch_volumes: SmallVec::new(),
        }
    }

    /// Accumulate a trade for intra-bar feature computation
    ///
    /// Issue #96 Task #44: Only accumulates if intra-bar features are enabled,
    /// avoiding unnecessary clones for the majority of use cases where they're disabled.
    /// Issue #96 Task #79: #[inline] allows compiler to fold invariant branch
    /// (include_intra is constant for processor lifetime)
    #[inline]
    fn accumulate_trade(&mut self, trade: &AggTrade, include_intra: bool) {
        if include_intra {
            self.accumulated_trades.push(*trade);
        }
    }
}
