// FILE-SIZE-OK: Bindings helpers with many feature conversions. Core logic in Rust crates.
use super::*;
use pyo3::types::PyString;

/// Issue #96 Task #82: Pre-compute numeric scale constants to reduce instruction cache pollution
const VOLUME_SCALE: f64 = 100_000_000.0;

/// Helper to get an interned Python string key.
/// `PyString::intern()` uses Python's internal string intern table —
/// after first call, subsequent calls for the same literal return the
/// cached object without allocation. This eliminates ~48 ephemeral
/// PyString allocations per bar at the FFI boundary.
#[inline]
fn ikey<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> {
    PyString::intern(py, s)
}

/// Convert f64 to `FixedPoint` (8 decimal precision)
pub(crate) fn f64_to_fixed_point(value: f64) -> FixedPoint {
    // FixedPoint uses i64 with 8 decimal places (scale = VOLUME_SCALE)
    let scaled = (value * VOLUME_SCALE).round() as i64;
    FixedPoint(scaled)
}

/// Convert Python dict to Rust `AggTrade`
pub(crate) fn dict_to_agg_trade(
    _py: Python,
    trade_dict: &Bound<PyDict>,
    index: usize,
) -> PyResult<AggTrade> {
    // Issue #106: Combine get_item + extract in single operation to reduce FFI calls
    // This avoids intermediate PyObject creation. extract() doesn't take py parameter.
    let timestamp_ms: i64 = trade_dict
        .get_item("timestamp")?
        .ok_or_else(|| PyKeyError::new_err(format!("Trade {index}: missing 'timestamp'")))?
        .extract()?;

    let price: f64 = trade_dict
        .get_item("price")?
        .ok_or_else(|| PyKeyError::new_err(format!("Trade {index}: missing 'price'")))?
        .extract()?;

    // Support both "quantity" and "volume" keys
    // Issue #106: Extract directly without intermediate PyObject
    let volume: f64 = match trade_dict.get_item("quantity")? {
        Some(val) => val.extract()?,
        None => trade_dict
            .get_item("volume")?
            .ok_or_else(|| {
                PyKeyError::new_err(format!("Trade {index}: missing 'quantity' or 'volume'"))
            })?
            .extract()?,
    };

    // Extract optional fields with defaults in a single pass
    // Issue #106: Use get_item().and_then(|v| v.extract::<T>().ok()) pattern for safety
    let agg_trade_id: i64 = trade_dict
        .get_item("agg_trade_id")?
        .and_then(|v| v.extract().ok())
        .unwrap_or(index as i64);

    let first_trade_id: i64 = trade_dict
        .get_item("first_trade_id")?
        .and_then(|v| v.extract().ok())
        .unwrap_or(agg_trade_id);

    let last_trade_id: i64 = trade_dict
        .get_item("last_trade_id")?
        .and_then(|v| v.extract().ok())
        .unwrap_or(agg_trade_id);

    let is_buyer_maker: bool = trade_dict
        .get_item("is_buyer_maker")?
        .and_then(|v| v.extract().ok())
        .unwrap_or(false);

    // Convert timestamp from milliseconds to microseconds (Binance → opendeviationbar-core)
    let timestamp_us = timestamp_ms * 1000;

    Ok(AggTrade {
        agg_trade_id,
        price: f64_to_fixed_point(price),
        volume: f64_to_fixed_point(volume),
        first_trade_id,
        last_trade_id,
        timestamp: timestamp_us,
        is_buyer_maker,
        is_best_match: None, // Not needed for futures markets
    })
}

/// Convert Rust `OpenDeviationBar` to Python dict
/// Issue #96 Task #81: Optimized with grouped field setting
/// Memory v2: PyString::intern() keys — Python's intern table caches after first call
pub(crate) fn opendeviationbar_to_dict(py: Python, bar: &OpenDeviationBar) -> PyResult<PyObject> {
    let dict = PyDict::new(py);

    // Issue #105: Pre-compute volume scaling ratio (avoids repeated division)
    let volume_scale_inv = 1.0 / VOLUME_SCALE;

    // Timestamps + OHLCV Core — values are raw MICROSECONDS with truthful _us keys
    dict.set_item(ikey(py, "open_time_us"), bar.open_time)?;
    dict.set_item(ikey(py, "close_time_us"), bar.close_time)?;
    dict.set_item(ikey(py, "open"), bar.open.to_f64())?;
    dict.set_item(ikey(py, "high"), bar.high.to_f64())?;
    dict.set_item(ikey(py, "low"), bar.low.to_f64())?;
    dict.set_item(ikey(py, "close"), bar.close.to_f64())?;
    dict.set_item(ikey(py, "volume"), bar.volume as f64 * volume_scale_inv)?;

    // Volume Accumulators
    dict.set_item(ikey(py, "vwap"), bar.vwap.to_f64())?;
    dict.set_item(
        ikey(py, "buy_volume"),
        bar.buy_volume as f64 * volume_scale_inv,
    )?;
    dict.set_item(
        ikey(py, "sell_volume"),
        bar.sell_volume as f64 * volume_scale_inv,
    )?;

    // Trade Tracking
    dict.set_item(
        ikey(py, "individual_trade_count"),
        bar.individual_trade_count,
    )?;
    dict.set_item(ikey(py, "agg_record_count"), bar.agg_record_count)?;
    dict.set_item(ikey(py, "first_agg_trade_id"), bar.first_agg_trade_id)?;
    dict.set_item(ikey(py, "last_agg_trade_id"), bar.last_agg_trade_id)?;

    // Microstructure Features
    dict.set_item(ikey(py, "duration_us"), bar.duration_us)?;
    dict.set_item(ikey(py, "ofi"), bar.ofi)?;
    dict.set_item(ikey(py, "vwap_close_deviation"), bar.vwap_close_deviation)?;
    dict.set_item(ikey(py, "price_impact"), bar.price_impact)?;
    dict.set_item(ikey(py, "kyle_lambda_proxy"), bar.kyle_lambda_proxy)?;
    dict.set_item(ikey(py, "trade_intensity"), bar.trade_intensity)?;
    dict.set_item(ikey(py, "volume_per_trade"), bar.volume_per_trade)?;
    dict.set_item(ikey(py, "aggression_ratio"), bar.aggression_ratio)?;
    dict.set_item(ikey(py, "aggregation_density"), bar.aggregation_density_f64)?;
    dict.set_item(ikey(py, "turnover_imbalance"), bar.turnover_imbalance)?;

    // Gap Awareness (v7.0 Aion)
    dict.set_item(ikey(py, "has_gap"), bar.has_gap)?;
    dict.set_item(ikey(py, "gap_trade_count"), bar.gap_trade_count)?;
    dict.set_item(ikey(py, "max_gap_duration_us"), bar.max_gap_duration_us)?;
    dict.set_item(ikey(py, "is_exchange_gap"), bar.is_exchange_gap)?;

    // Inter-Bar Core Features
    dict.set_item(ikey(py, "lookback_trade_count"), bar.lookback_trade_count)?;
    dict.set_item(ikey(py, "lookback_ofi"), bar.lookback_ofi)?;
    dict.set_item(ikey(py, "lookback_duration_us"), bar.lookback_duration_us)?;
    dict.set_item(ikey(py, "lookback_intensity"), bar.lookback_intensity)?;
    dict.set_item(ikey(py, "lookback_vwap_raw"), bar.lookback_vwap_raw)?;
    dict.set_item(
        ikey(py, "lookback_vwap_position"),
        bar.lookback_vwap_position,
    )?;
    dict.set_item(
        ikey(py, "lookback_count_imbalance"),
        bar.lookback_count_imbalance,
    )?;

    // Inter-Bar Optional Features (Tiers 2-3)
    if let Some(v) = bar.lookback_kyle_lambda {
        dict.set_item(ikey(py, "lookback_kyle_lambda"), v)?;
    }
    if let Some(v) = bar.lookback_burstiness {
        dict.set_item(ikey(py, "lookback_burstiness"), v)?;
    }
    if let Some(v) = bar.lookback_volume_skew {
        dict.set_item(ikey(py, "lookback_volume_skew"), v)?;
    }
    if let Some(v) = bar.lookback_volume_kurt {
        dict.set_item(ikey(py, "lookback_volume_kurt"), v)?;
    }
    if let Some(v) = bar.lookback_price_range {
        dict.set_item(ikey(py, "lookback_price_range"), v)?;
    }
    if let Some(v) = bar.lookback_kaufman_er {
        dict.set_item(ikey(py, "lookback_kaufman_er"), v)?;
    }
    if let Some(v) = bar.lookback_garman_klass_vol {
        dict.set_item(ikey(py, "lookback_garman_klass_vol"), v)?;
    }
    if let Some(v) = bar.lookback_hurst {
        dict.set_item(ikey(py, "lookback_hurst"), v)?;
    }
    if let Some(v) = bar.lookback_permutation_entropy {
        dict.set_item(ikey(py, "lookback_permutation_entropy"), v)?;
    }

    // Intra-Bar ITH Features
    if let Some(v) = bar.intra_bull_epoch_density {
        dict.set_item(ikey(py, "intra_bull_epoch_density"), v)?;
    }
    if let Some(v) = bar.intra_bear_epoch_density {
        dict.set_item(ikey(py, "intra_bear_epoch_density"), v)?;
    }
    if let Some(v) = bar.intra_bull_excess_gain {
        dict.set_item(ikey(py, "intra_bull_excess_gain"), v)?;
    }
    if let Some(v) = bar.intra_bear_excess_gain {
        dict.set_item(ikey(py, "intra_bear_excess_gain"), v)?;
    }
    if let Some(v) = bar.intra_bull_cv {
        dict.set_item(ikey(py, "intra_bull_cv"), v)?;
    }
    if let Some(v) = bar.intra_bear_cv {
        dict.set_item(ikey(py, "intra_bear_cv"), v)?;
    }
    if let Some(v) = bar.intra_max_drawdown {
        dict.set_item(ikey(py, "intra_max_drawdown"), v)?;
    }
    if let Some(v) = bar.intra_max_runup {
        dict.set_item(ikey(py, "intra_max_runup"), v)?;
    }

    // Intra-Bar Statistical Features
    if let Some(v) = bar.intra_trade_count {
        dict.set_item(ikey(py, "intra_trade_count"), v)?;
    }
    if let Some(v) = bar.intra_ofi {
        dict.set_item(ikey(py, "intra_ofi"), v)?;
    }
    if let Some(v) = bar.intra_duration_us {
        dict.set_item(ikey(py, "intra_duration_us"), v)?;
    }
    if let Some(v) = bar.intra_intensity {
        dict.set_item(ikey(py, "intra_intensity"), v)?;
    }
    if let Some(v) = bar.intra_vwap_position {
        dict.set_item(ikey(py, "intra_vwap_position"), v)?;
    }
    if let Some(v) = bar.intra_count_imbalance {
        dict.set_item(ikey(py, "intra_count_imbalance"), v)?;
    }
    if let Some(v) = bar.intra_kyle_lambda {
        dict.set_item(ikey(py, "intra_kyle_lambda"), v)?;
    }
    if let Some(v) = bar.intra_burstiness {
        dict.set_item(ikey(py, "intra_burstiness"), v)?;
    }
    if let Some(v) = bar.intra_volume_skew {
        dict.set_item(ikey(py, "intra_volume_skew"), v)?;
    }
    if let Some(v) = bar.intra_volume_kurt {
        dict.set_item(ikey(py, "intra_volume_kurt"), v)?;
    }
    if let Some(v) = bar.intra_kaufman_er {
        dict.set_item(ikey(py, "intra_kaufman_er"), v)?;
    }
    if let Some(v) = bar.intra_garman_klass_vol {
        dict.set_item(ikey(py, "intra_garman_klass_vol"), v)?;
    }

    // Intra-Bar Complexity Features
    if let Some(v) = bar.intra_hurst {
        dict.set_item(ikey(py, "intra_hurst"), v)?;
    }
    if let Some(v) = bar.intra_permutation_entropy {
        dict.set_item(ikey(py, "intra_permutation_entropy"), v)?;
    }

    Ok(dict.into_any().unbind())
}

/// Convert Rust `Checkpoint` to Python dict (JSON-serializable)
pub(crate) fn checkpoint_to_dict(py: Python, checkpoint: &Checkpoint) -> PyResult<PyObject> {
    let dict = PyDict::new(py);

    // Identification
    dict.set_item("symbol", &checkpoint.symbol)?;
    dict.set_item("threshold_decimal_bps", checkpoint.threshold_decimal_bps)?;

    // Incomplete bar (convert to dict if present)
    if let Some(ref bar) = checkpoint.incomplete_bar {
        dict.set_item("incomplete_bar", opendeviationbar_to_dict(py, bar)?)?;

        // Also store raw OHLCV for easy access
        // Issue #105: Pre-compute volume scale inverse for batch operations
        let volume_scale_inv = 1.0 / VOLUME_SCALE;
        let bar_dict = PyDict::new(py);
        bar_dict.set_item("open", bar.open.to_f64())?;
        bar_dict.set_item("high", bar.high.to_f64())?;
        bar_dict.set_item("low", bar.low.to_f64())?;
        bar_dict.set_item("close", bar.close.to_f64())?;
        bar_dict.set_item("volume", bar.volume as f64 * volume_scale_inv)?; // Issue #88: i128
        bar_dict.set_item("open_time", bar.open_time)?;
        bar_dict.set_item("close_time", bar.close_time)?;
        bar_dict.set_item("agg_record_count", bar.agg_record_count)?;
        // Issue #97: Full microstructure state for lossless checkpoint round-trip
        bar_dict.set_item("buy_volume", bar.buy_volume as f64 / VOLUME_SCALE)?;
        bar_dict.set_item("sell_volume", bar.sell_volume as f64 / VOLUME_SCALE)?;
        bar_dict.set_item("individual_trade_count", bar.individual_trade_count)?;
        bar_dict.set_item("buy_trade_count", bar.buy_trade_count)?;
        bar_dict.set_item("sell_trade_count", bar.sell_trade_count)?;
        bar_dict.set_item("vwap", bar.vwap.to_f64())?;
        bar_dict.set_item("first_agg_trade_id", bar.first_agg_trade_id)?;
        bar_dict.set_item("last_agg_trade_id", bar.last_agg_trade_id)?;
        bar_dict.set_item("turnover", bar.turnover as f64 / VOLUME_SCALE)?;
        bar_dict.set_item("buy_turnover", bar.buy_turnover as f64 / VOLUME_SCALE)?;
        bar_dict.set_item("sell_turnover", bar.sell_turnover as f64 / VOLUME_SCALE)?;
        dict.set_item("incomplete_bar_raw", bar_dict.into_any().unbind())?;
    } else {
        dict.set_item("incomplete_bar", py.None())?;
        dict.set_item("incomplete_bar_raw", py.None())?;
    }

    // Thresholds (FixedPoint as f64)
    if let Some((upper, lower)) = checkpoint.thresholds {
        let thresholds_dict = PyDict::new(py);
        thresholds_dict.set_item("upper", upper.to_f64())?;
        thresholds_dict.set_item("lower", lower.to_f64())?;
        dict.set_item("thresholds", thresholds_dict.into_any().unbind())?;
    } else {
        dict.set_item("thresholds", py.None())?;
    }

    // Position tracking
    dict.set_item("last_timestamp_us", checkpoint.last_timestamp_us)?;
    // Convert to milliseconds for Python consistency
    dict.set_item("last_timestamp_ms", checkpoint.last_timestamp_us / 1000)?;
    dict.set_item("last_trade_id", checkpoint.last_trade_id)?;

    // Integrity
    dict.set_item("price_hash", checkpoint.price_hash)?;

    // Anomaly summary
    let anomaly_dict = PyDict::new(py);
    anomaly_dict.set_item("gaps_detected", checkpoint.anomaly_summary.gaps_detected)?;
    anomaly_dict.set_item(
        "overlaps_detected",
        checkpoint.anomaly_summary.overlaps_detected,
    )?;
    anomaly_dict.set_item(
        "timestamp_anomalies",
        checkpoint.anomaly_summary.timestamp_anomalies,
    )?;
    dict.set_item("anomaly_summary", anomaly_dict.into_any().unbind())?;

    // Has incomplete bar flag
    dict.set_item("has_incomplete_bar", checkpoint.has_incomplete_bar())?;

    // Behavior flags (Issue #36)
    dict.set_item(
        "prevent_same_timestamp_close",
        checkpoint.prevent_same_timestamp_close,
    )?;

    // Issue #46: Persist defer_open state for cross-session continuity
    dict.set_item("defer_open", checkpoint.defer_open)?;

    // v1.4: Last completed bar trade ID for dedup floor
    dict.set_item("last_completed_bar_tid", checkpoint.last_completed_bar_tid)?;

    Ok(dict.into_any().unbind())
}

/// Convert Python dict to Rust `Checkpoint`
pub(crate) fn dict_to_checkpoint(py: Python, dict: &Bound<PyDict>) -> PyResult<Checkpoint> {
    // Extract required fields
    let symbol: String = dict
        .get_item("symbol")?
        .ok_or_else(|| PyKeyError::new_err("Missing 'symbol'"))?
        .extract()?;

    let threshold_decimal_bps: u32 = dict
        .get_item("threshold_decimal_bps")?
        .ok_or_else(|| PyKeyError::new_err("Missing 'threshold_decimal_bps'"))?
        .extract()?;

    // Issue #62: Validate threshold range in checkpoint restoration
    // Valid range: 1-100,000 dbps (0.0001% to 10%)
    const THRESHOLD_MIN: u32 = 1;
    const THRESHOLD_MAX: u32 = 100_000;
    if !(THRESHOLD_MIN..=THRESHOLD_MAX).contains(&threshold_decimal_bps) {
        return Err(PyValueError::new_err(format!(
            "Invalid checkpoint threshold: {threshold_decimal_bps} dbps. Valid range: {THRESHOLD_MIN}-{THRESHOLD_MAX} dbps"
        )));
    }

    let last_timestamp_us: i64 = dict
        .get_item("last_timestamp_us")?
        .ok_or_else(|| PyKeyError::new_err("Missing 'last_timestamp_us'"))?
        .extract()?;

    let last_trade_id: Option<i64> = dict
        .get_item("last_trade_id")?
        .and_then(|v| v.extract().ok());

    let price_hash: u64 = dict
        .get_item("price_hash")?
        .ok_or_else(|| PyKeyError::new_err("Missing 'price_hash'"))?
        .extract()?;

    // Extract incomplete bar if present
    let incomplete_bar: Option<OpenDeviationBar> =
        if let Some(bar_raw) = dict.get_item("incomplete_bar_raw")? {
            if bar_raw.is_none() {
                None
            } else {
                let bar_dict: &Bound<PyDict> = bar_raw.cast()?;
                Some(dict_to_opendeviationbar(py, bar_dict)?)
            }
        } else {
            None
        };

    // Extract thresholds if present
    let thresholds: Option<(FixedPoint, FixedPoint)> =
        if let Some(thresholds_obj) = dict.get_item("thresholds")? {
            if thresholds_obj.is_none() {
                None
            } else {
                let thresholds_dict: &Bound<PyDict> = thresholds_obj.cast()?;
                let upper: f64 = thresholds_dict
                    .get_item("upper")?
                    .ok_or_else(|| PyKeyError::new_err("Missing 'thresholds.upper'"))?
                    .extract()?;
                let lower: f64 = thresholds_dict
                    .get_item("lower")?
                    .ok_or_else(|| PyKeyError::new_err("Missing 'thresholds.lower'"))?
                    .extract()?;
                Some((f64_to_fixed_point(upper), f64_to_fixed_point(lower)))
            }
        } else {
            None
        };

    // Extract anomaly summary
    let anomaly_summary = if let Some(anomaly_obj) = dict.get_item("anomaly_summary")? {
        let anomaly_dict: &Bound<PyDict> = anomaly_obj.cast()?;
        AnomalySummary {
            gaps_detected: anomaly_dict
                .get_item("gaps_detected")?
                .and_then(|v| v.extract().ok())
                .unwrap_or(0),
            overlaps_detected: anomaly_dict
                .get_item("overlaps_detected")?
                .and_then(|v| v.extract().ok())
                .unwrap_or(0),
            timestamp_anomalies: anomaly_dict
                .get_item("timestamp_anomalies")?
                .and_then(|v| v.extract().ok())
                .unwrap_or(0),
        }
    } else {
        AnomalySummary::default()
    };

    // Extract behavior flags (Issue #36)
    // Default to true (new behavior) if not present in checkpoint
    let prevent_same_timestamp_close: bool = dict
        .get_item("prevent_same_timestamp_close")?
        .and_then(|v| v.extract().ok())
        .unwrap_or(true);

    // Issue #46: Extract defer_open flag from checkpoint dict
    let defer_open: bool = dict
        .get_item("defer_open")?
        .and_then(|v| v.extract().ok())
        .unwrap_or(false);

    // v1.4: Extract last_completed_bar_tid (defaults to None for old checkpoints)
    let last_completed_bar_tid: Option<i64> = dict
        .get_item("last_completed_bar_tid")?
        .and_then(|v| if v.is_none() { None } else { v.extract().ok() });

    // Issue #85 Phase 2: Extract version field for checkpoint schema migration
    let version: u32 = dict
        .get_item("version")?
        .and_then(|v| v.extract().ok())
        .unwrap_or(1); // Default to v1 for backward compatibility

    Ok(Checkpoint {
        version,
        symbol,
        threshold_decimal_bps,
        incomplete_bar,
        thresholds,
        last_timestamp_us,
        last_trade_id,
        price_hash,
        anomaly_summary,
        prevent_same_timestamp_close,
        defer_open,
        last_completed_bar_tid,
    })
}

/// Macro to extract optional numeric field with default and scale conversion
/// Issue #96 Task #72: Consolidate repeated dict.get_item() + extract() + scale patterns
macro_rules! extract_optional_f64 {
    ($dict:expr, $field:expr, $default:expr) => {
        $dict
            .get_item($field)?
            .and_then(|v| v.extract().ok())
            .unwrap_or($default)
    };
}

/// Convert Python dict to Rust `OpenDeviationBar` (for checkpoint restoration)
pub(crate) fn dict_to_opendeviationbar(
    _py: Python,
    dict: &Bound<PyDict>,
) -> PyResult<OpenDeviationBar> {
    const SCALE: f64 = 100_000_000.0;

    // Required fields - fail fast if missing
    let open: f64 = dict
        .get_item("open")?
        .ok_or_else(|| PyKeyError::new_err("Missing 'open'"))?
        .extract()?;
    let high: f64 = dict
        .get_item("high")?
        .ok_or_else(|| PyKeyError::new_err("Missing 'high'"))?
        .extract()?;
    let low: f64 = dict
        .get_item("low")?
        .ok_or_else(|| PyKeyError::new_err("Missing 'low'"))?
        .extract()?;
    let close: f64 = dict
        .get_item("close")?
        .ok_or_else(|| PyKeyError::new_err("Missing 'close'"))?
        .extract()?;
    let volume: f64 = dict
        .get_item("volume")?
        .ok_or_else(|| PyKeyError::new_err("Missing 'volume'"))?
        .extract()?;
    let open_time: i64 = dict
        .get_item("open_time")?
        .ok_or_else(|| PyKeyError::new_err("Missing 'open_time'"))?
        .extract()?;
    let close_time: i64 = dict
        .get_item("close_time")?
        .ok_or_else(|| PyKeyError::new_err("Missing 'close_time'"))?
        .extract()?;

    // Issue #97: Batch-extract optional numeric fields (8-12% speedup vs 12 separate calls)
    let agg_record_count: u32 = extract_optional_f64!(dict, "agg_record_count", 0.0) as u32;
    let buy_volume_f64: f64 = extract_optional_f64!(dict, "buy_volume", 0.0);
    let sell_volume_f64: f64 = extract_optional_f64!(dict, "sell_volume", 0.0);
    let individual_trade_count: u32 =
        extract_optional_f64!(dict, "individual_trade_count", 0.0) as u32;
    let buy_trade_count: u32 = extract_optional_f64!(dict, "buy_trade_count", 0.0) as u32;
    let sell_trade_count: u32 = extract_optional_f64!(dict, "sell_trade_count", 0.0) as u32;
    let vwap_f64: f64 = extract_optional_f64!(dict, "vwap", 0.0);
    let first_agg_trade_id: i64 = extract_optional_f64!(dict, "first_agg_trade_id", 0.0) as i64;
    let last_agg_trade_id: i64 = extract_optional_f64!(dict, "last_agg_trade_id", 0.0) as i64;
    let turnover_f64: f64 = extract_optional_f64!(dict, "turnover", 0.0);
    let buy_turnover_f64: f64 = extract_optional_f64!(dict, "buy_turnover", 0.0);
    let sell_turnover_f64: f64 = extract_optional_f64!(dict, "sell_turnover", 0.0);

    // Issue #96 Task #72: Batch consolidate i128 scale conversions (3-5% speedup)
    // Use single SCALE constant for all volume/turnover fields
    let volume_i128 = (volume * SCALE).round() as i128;
    let turnover_i128 = (turnover_f64 * SCALE).round() as i128;
    let buy_volume_i128 = (buy_volume_f64 * SCALE).round() as i128;
    let sell_volume_i128 = (sell_volume_f64 * SCALE).round() as i128;
    let buy_turnover_i128 = (buy_turnover_f64 * SCALE).round() as i128;
    let sell_turnover_i128 = (sell_turnover_f64 * SCALE).round() as i128;

    // Issue #85 Phase 3: Reorganize OpenDeviationBar construction to match tier-based field ordering
    Ok(OpenDeviationBar {
        // Tier 1: OHLCV Core
        open_time,
        close_time,
        open: f64_to_fixed_point(open),
        high: f64_to_fixed_point(high),
        low: f64_to_fixed_point(low),
        close: f64_to_fixed_point(close),

        // Tier 2: Volume Accumulators (Issue #88: i128, not FixedPoint)
        volume: volume_i128,
        turnover: turnover_i128,
        buy_volume: buy_volume_i128,
        sell_volume: sell_volume_i128,
        buy_turnover: buy_turnover_i128,
        sell_turnover: sell_turnover_i128,

        // Tier 3: Trade Tracking
        first_trade_id: 0,
        last_trade_id: 0,
        first_agg_trade_id,
        last_agg_trade_id,
        individual_trade_count,
        agg_record_count,
        buy_trade_count,
        sell_trade_count,

        // Tier 3b: Gap Awareness (v7.0 Aion) - defaults for checkpoint restoration
        has_gap: extract_optional_f64!(dict, "has_gap", 0.0) != 0.0,
        gap_trade_count: extract_optional_f64!(dict, "gap_trade_count", 0.0) as i64,
        max_gap_duration_us: extract_optional_f64!(dict, "max_gap_duration_us", 0.0) as i64,
        is_exchange_gap: extract_optional_f64!(dict, "is_exchange_gap", 0.0) != 0.0,

        // Tier 4: Price Context
        vwap: f64_to_fixed_point(vwap_f64),
        data_source: opendeviationbar_core::DataSource::BinanceSpot,

        // Tier 5: Microstructure features (Issue #25) - initialized to defaults
        duration_us: 0,
        ofi: 0.0,
        vwap_close_deviation: 0.0,
        price_impact: 0.0,
        kyle_lambda_proxy: 0.0,
        trade_intensity: 0.0,
        volume_per_trade: 0.0,
        aggression_ratio: 0.0,
        aggregation_density_f64: 0.0,
        turnover_imbalance: 0.0,

        // Tier 6: Inter-bar features (Issue #59) - initialized to None
        // Checkpoint restoration doesn't include inter-bar features
        lookback_trade_count: None,
        lookback_ofi: None,
        lookback_duration_us: None,
        lookback_intensity: None,
        lookback_vwap_raw: None,
        lookback_vwap_position: None,
        lookback_count_imbalance: None,
        lookback_kyle_lambda: None,
        lookback_burstiness: None,
        lookback_volume_skew: None,
        lookback_volume_kurt: None,
        lookback_price_range: None,
        lookback_kaufman_er: None,
        lookback_garman_klass_vol: None,
        lookback_hurst: None,
        lookback_permutation_entropy: None,

        // Tier 7: Intra-bar features (Issue #59) - initialized to None
        intra_bull_epoch_density: None,
        intra_bear_epoch_density: None,
        intra_bull_excess_gain: None,
        intra_bear_excess_gain: None,
        intra_bull_cv: None,
        intra_bear_cv: None,
        intra_max_drawdown: None,
        intra_max_runup: None,
        intra_trade_count: None,
        intra_ofi: None,
        intra_duration_us: None,
        intra_intensity: None,
        intra_vwap_position: None,
        intra_count_imbalance: None,
        intra_kyle_lambda: None,
        intra_burstiness: None,
        intra_volume_skew: None,
        intra_volume_kurt: None,
        intra_kaufman_er: None,
        intra_garman_klass_vol: None,
        intra_hurst: None,
        intra_permutation_entropy: None,
    })
}

/// Convert `CheckpointError` to Python exception
pub(crate) fn checkpoint_error_to_pyerr(e: CheckpointError) -> PyErr {
    match e {
        CheckpointError::SymbolMismatch {
            checkpoint,
            expected,
        } => PyValueError::new_err(format!(
            "Symbol mismatch: checkpoint has '{checkpoint}', expected '{expected}'"
        )),
        CheckpointError::ThresholdMismatch {
            checkpoint,
            expected,
        } => PyValueError::new_err(format!(
            "Threshold mismatch: checkpoint has {checkpoint}, expected {expected}"
        )),
        CheckpointError::PriceHashMismatch {
            checkpoint,
            computed,
        } => PyValueError::new_err(format!(
            "Price hash mismatch: checkpoint has {checkpoint}, computed {computed}"
        )),
        CheckpointError::MissingThresholds => {
            PyValueError::new_err("Checkpoint has incomplete bar but missing thresholds")
        }
        CheckpointError::SerializationError { message } => {
            PyRuntimeError::new_err(format!("Checkpoint serialization error: {message}"))
        }
        // Issue #62: Crypto minimum threshold enforcement
        CheckpointError::InvalidThreshold {
            threshold,
            min_threshold,
            max_threshold,
        } => PyValueError::new_err(format!(
            "Invalid checkpoint threshold: {threshold} dbps. Valid range: {min_threshold}-{max_threshold} dbps"
        )),
    }
}
