// =============================================================================
// PyO3 Module Entry Point
// =============================================================================
// This is the thin orchestrator for the opendeviationbar Python extension module.
// All implementation lives in domain-specific submodules.
// Issue #94: Refactored from monolithic 2999-line file.

use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::PyDict;
use pyo3::IntoPyObjectExt;

// PyO3 0.28: PyObject removed from prelude, define as type alias
type PyObject = Py<PyAny>;
use opendeviationbar_core::{
    AggTrade, AnomalySummary, Checkpoint, CheckpointError, FixedPoint, InterBarConfig,
    IntraBarConfig, LookbackMode, OpenDeviationBar, OpenDeviationBarProcessor,
    PositionVerification,
};

// Arrow export support (feature-gated)
#[cfg(feature = "arrow-export")]
use pyo3_arrow::PyRecordBatch;
// Issue #88: Arrow-native input path
#[cfg(feature = "arrow-export")]
use opendeviationbar_core::{
    aggtrades_to_record_batch, opendeviationbar_vec_to_record_batch, record_batch_to_aggtrades,
};

#[cfg(feature = "data-providers")]
use opendeviationbar_providers::exness::{
    ExnessInstrument, ExnessOpenDeviationBar, ExnessOpenDeviationBarBuilder, ExnessTick,
    SpreadStats, ValidationStrictness,
};

#[cfg(feature = "data-providers")]
use opendeviationbar_providers::binance::{HistoricalDataLoader, IntraDayChunkIterator};

// =============================================================================
// Domain Modules
// =============================================================================

mod helpers;
use helpers::*;

mod core_bindings;
use core_bindings::*;

#[cfg(feature = "data-providers")]
mod exness_bindings;

#[cfg(feature = "arrow-export")]
mod arrow_bindings;

#[cfg(feature = "data-providers")]
mod binance_bindings;

#[cfg(feature = "streaming")]
mod streaming_bindings;

// Issue #161: StreamManager bindings (separate module to respect file-size-guard)
#[cfg(feature = "streaming")]
mod stream_manager_bindings;

// Issue #178: OdbEngine bindings (unified hot-path/cold-path engine)
#[cfg(feature = "streaming")]
mod odb_engine_bindings;

// Issue #257: Gap event watching and gap-fill bindings (separate pyclass for borrow safety)
#[cfg(feature = "streaming")]
mod gap_bindings;

mod thread_pool_init;

// =============================================================================
// Python Module Registration
// =============================================================================

/// Python module
#[pymodule]
fn _core(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
    // Initialize Rust tracing → stderr so messages appear in systemd journal.
    // Without this, all tracing::info!/warn!/error! calls are silently dropped.
    use std::sync::Once;
    static TRACING_INIT: Once = Once::new();
    TRACING_INIT.call_once(|| {
        let filter = tracing_subscriber::EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
        tracing_subscriber::fmt()
            .with_env_filter(filter)
            .with_target(true)
            .with_writer(std::io::stderr)
            .init();
    });

    // Initialize rayon global thread pool (Task #90, Issue #96)
    // This eliminates lazy initialization overhead on first parallel operation
    let _ = thread_pool_init::initialize_rayon_pool();

    m.add("__version__", env!("CARGO_PKG_VERSION"))?;
    m.add_class::<PyOpenDeviationBarProcessor>()?;
    m.add_class::<PyPositionVerification>()?;
    m.add_function(wrap_pyfunction!(
        core_bindings::get_feature_manifest_raw,
        m
    )?)?;

    // Add Arrow export functions if feature enabled
    #[cfg(feature = "arrow-export")]
    {
        m.add_function(wrap_pyfunction!(arrow_bindings::bars_to_arrow, m)?)?;
        m.add_function(wrap_pyfunction!(arrow_bindings::trades_to_arrow, m)?)?;
    }

    // Add Exness classes if feature enabled
    #[cfg(feature = "data-providers")]
    {
        m.add_class::<exness_bindings::PyExnessInstrument>()?;
        m.add_class::<exness_bindings::PyValidationStrictness>()?;
        m.add_class::<exness_bindings::PyExnessOpenDeviationBarBuilder>()?;
        m.add_class::<binance_bindings::PyMarketType>()?;
        m.add_class::<binance_bindings::PyBinanceTradeStream>()?;
        m.add_function(wrap_pyfunction!(
            binance_bindings::fetch_binance_aggtrades,
            m
        )?)?;
        m.add_function(wrap_pyfunction!(
            binance_bindings::stream_binance_trades,
            m
        )?)?;
        m.add_function(wrap_pyfunction!(binance_bindings::fetch_aggtrades_rest, m)?)?;
        m.add_function(wrap_pyfunction!(
            binance_bindings::fetch_aggtrades_by_id,
            m
        )?)?;
        m.add_function(wrap_pyfunction!(
            binance_bindings::fetch_latest_aggtrade,
            m
        )?)?;
        m.add_function(wrap_pyfunction!(
            binance_bindings::get_rest_rate_limiter_status,
            m
        )?)?;
        m.add_function(wrap_pyfunction!(
            binance_bindings::fetch_aggtrades_parallel,
            m
        )?)?;

        // Phase 3: Arrow-native stream (requires both data-providers and arrow-export)
        #[cfg(feature = "arrow-export")]
        {
            m.add_class::<binance_bindings::PyBinanceTradeStreamArrow>()?;
            m.add_function(wrap_pyfunction!(
                binance_bindings::stream_binance_trades_arrow,
                m
            )?)?;
        }
    }

    // Add streaming classes if feature enabled
    // ADR: docs/adr/2026-01-31-realtime-streaming-api.md
    #[cfg(feature = "streaming")]
    {
        m.add_class::<streaming_bindings::PyBinanceLiveStream>()?;
        m.add_class::<streaming_bindings::PyLiveBarEngine>()?;
        m.add_class::<streaming_bindings::PyStreamingConfig>()?;
        m.add_class::<streaming_bindings::PyStreamingMetrics>()?;
        m.add_class::<streaming_bindings::PyStreamingOpenDeviationBarProcessor>()?;
        // Issue #161: StreamManager with rate limiting + trade-ID continuity
        m.add_class::<stream_manager_bindings::PyStreamManager>()?;
        m.add_class::<stream_manager_bindings::PyFormingBarWatcher>()?;
        // Issue #178: Unified engine with trait-based source/sink composition
        m.add_class::<odb_engine_bindings::PyOdbEngine>()?;
        // Issue #257: Gap event watching and gap-fill result
        m.add_class::<gap_bindings::PyGapEventWatcher>()?;
        m.add_class::<gap_bindings::PyGapFillResult>()?;
    }

    Ok(())
}

// =============================================================================
// Tests
// =============================================================================

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

    #[test]
    fn test_f64_to_fixed_point() {
        let fp = f64_to_fixed_point(42_000.123_456_78);
        assert!((fp.to_f64() - 42_000.123_456_78).abs() < 1e-8);

        let fp2 = f64_to_fixed_point(1.5);
        assert!((fp2.to_f64() - 1.5).abs() < 1e-8);
    }

    #[test]
    fn test_processor_creation() {
        let processor = PyOpenDeviationBarProcessor::new(
            250, None, true, None, false, None, true, false, None, None,
        );
        assert!(processor.is_ok());
        assert_eq!(processor.unwrap().threshold_decimal_bps, 250);
    }

    #[test]
    fn test_invalid_threshold() {
        let processor = PyOpenDeviationBarProcessor::new(
            0, None, true, None, false, None, true, false, None, None,
        );
        assert!(processor.is_err());

        let processor = PyOpenDeviationBarProcessor::new(
            200_000, None, true, None, false, None, true, false, None, None,
        );
        assert!(processor.is_err());
    }

    #[test]
    fn test_f64_to_fixed_point_extremes() {
        // Test zero
        let fp_zero = f64_to_fixed_point(0.0);
        assert!((fp_zero.to_f64()).abs() < 1e-8);

        // Test negative values
        let fp_neg = f64_to_fixed_point(-42_000.123_456_78);
        assert!((fp_neg.to_f64() - (-42_000.123_456_78)).abs() < 1e-8);

        // Test very small values (precision boundary)
        let fp_small = f64_to_fixed_point(0.000_000_01);
        assert!((fp_small.to_f64() - 0.000_000_01).abs() < 1e-8);

        // Test very large values
        let fp_large = f64_to_fixed_point(1_000_000.123_456_78);
        assert!((fp_large.to_f64() - 1_000_000.123_456_78).abs() < 1e-8);

        // Test maximum precision (8 decimal places)
        let fp_precision = f64_to_fixed_point(123.456_789_01);
        let rounded = fp_precision.to_f64();
        assert!((rounded - 123.456_789_01).abs() < 0.000_001);
    }

    #[test]
    fn test_processor_boundary_thresholds() {
        // Test minimum valid threshold (1 = 0.1 basis points)
        let processor_min = PyOpenDeviationBarProcessor::new(
            1, None, true, None, false, None, true, false, None, None,
        );
        assert!(processor_min.is_ok());
        assert_eq!(processor_min.unwrap().threshold_decimal_bps, 1);

        // Test maximum valid threshold (100_000 = 10,000 basis points = 100%)
        let processor_max = PyOpenDeviationBarProcessor::new(
            100_000, None, true, None, false, None, true, false, None, None,
        );
        assert!(processor_max.is_ok());
        assert_eq!(processor_max.unwrap().threshold_decimal_bps, 100_000);

        // Test common valid thresholds
        for threshold in [10, 100, 250, 500, 1000, 10_000] {
            let processor = PyOpenDeviationBarProcessor::new(
                threshold, None, true, None, false, None, true, false, None, None,
            );
            assert!(processor.is_ok(), "Threshold {} should be valid", threshold);
        }
    }
}
