// Issue #178: PyO3 bindings for OdbEngine (unified hot-path/cold-path engine)

use super::*;
use opendeviationbar_providers::binance::AdaptiveRateLimiter;
use opendeviationbar_streaming::live_engine::LiveEngineMetrics;
use std::sync::Arc;

/// Unified streaming engine with trait-based source/sink composition.
///
/// Drop-in replacement for StreamManager with identical Python-facing API.
/// Internally uses OdbEngine with ChannelSink for Python bar delivery.
///
/// Example:
///     >>> engine = OdbEngine(["BTCUSDT"], [250, 500])
///     >>> engine.seed_trade_id("BTCUSDT", 250, 1000000)
///     >>> engine.start()
///     >>> bar = engine.next_bar(timeout_ms=5000)
#[pyclass(name = "OdbEngine")]
pub struct PyOdbEngine {
    /// Tokio runtime for async operations.
    runtime: Option<tokio::runtime::Runtime>,
    /// Receiver end of the ChannelSink — consumed by next_bar().
    bar_rx: Option<tokio::sync::mpsc::Receiver<opendeviationbar_streaming::CompletedBar>>,
    /// LiveBarSource for engine configuration before start().
    source: Option<opendeviationbar_streaming::LiveBarSource>,
    /// Shutdown token shared with the engine loop.
    shutdown: Option<tokio_util::sync::CancellationToken>,
    /// Trade-ID seeds to apply before engine.run().
    trade_id_seeds: Vec<(String, i64)>,
    /// OdbEngine-level metrics (Stathera verification, fan-out).
    odb_metrics: Option<Arc<opendeviationbar_streaming::engine::odb_engine::OdbEngineMetrics>>,
    /// LiveBarEngine-level metrics (trades, bars, reconnections, drops).
    /// Extracted before the engine is moved into the tokio task.
    live_metrics: Option<Arc<LiveEngineMetrics>>,
    /// Checkpoint receiver extracted from LiveBarEngine before spawning.
    /// Drained in collect_checkpoints() after stop().
    checkpoint_rx:
        Option<tokio::sync::mpsc::Receiver<(String, u32, opendeviationbar_core::Checkpoint)>>,
    /// Rate limiter for REST API gap-fill pacing.
    rate_limiter: Arc<AdaptiveRateLimiter>,
    started: bool,
    /// Channel capacity for bar delivery.
    channel_capacity: usize,
}

#[pymethods]
impl PyOdbEngine {
    /// Create a new OdbEngine.
    ///
    /// Args:
    ///     symbols: List of trading pairs (e.g., ["BTCUSDT", "ETHUSDT"])
    ///     thresholds: List of thresholds in decimal basis points (e.g., [250, 500])
    ///     include_microstructure: Whether to compute all 58 columns (default: True)
    ///     channel_capacity: Bounded channel capacity for bar delivery (default: 100)
    ///     max_weight_per_minute: Binance rate limit budget (default: 2400)
    ///     safety_margin: Fraction of budget to reserve (default: 0.20)
    #[new]
    #[pyo3(signature = (symbols, thresholds, include_microstructure=true, compute_tier2=true, compute_tier3=false, compute_hurst=None, compute_permutation_entropy=None, channel_capacity=100, max_weight_per_minute=2400, safety_margin=0.20))]
    pub fn new(
        symbols: Vec<String>,
        thresholds: Vec<u32>,
        include_microstructure: bool,
        compute_tier2: bool,
        compute_tier3: bool,
        compute_hurst: Option<bool>,
        compute_permutation_entropy: Option<bool>,
        channel_capacity: usize,
        max_weight_per_minute: u32,
        safety_margin: f32,
    ) -> PyResult<Self> {
        if symbols.is_empty() {
            return Err(PyValueError::new_err("symbols must not be empty"));
        }
        if thresholds.is_empty() {
            return Err(PyValueError::new_err("thresholds must not be empty"));
        }
        if channel_capacity == 0 {
            return Err(PyValueError::new_err(
                "channel_capacity must be greater than 0",
            ));
        }

        let rate_limiter = Arc::new(AdaptiveRateLimiter::new(
            max_weight_per_minute,
            safety_margin,
        ));

        let mut engine_config =
            opendeviationbar_streaming::LiveEngineConfig::new(symbols, thresholds);
        engine_config.include_microstructure = include_microstructure;
        engine_config.compute_tier2 = compute_tier2;
        engine_config.compute_tier3 = compute_tier3;
        engine_config.compute_hurst = compute_hurst;
        engine_config.compute_permutation_entropy = compute_permutation_entropy;
        engine_config.rate_limiter = Some(rate_limiter.clone());

        let live_engine = opendeviationbar_streaming::LiveBarEngine::new(engine_config);
        let source = opendeviationbar_streaming::LiveBarSource::new(live_engine);

        Ok(Self {
            runtime: None,
            bar_rx: None,
            source: Some(source),
            shutdown: None,
            trade_id_seeds: Vec::new(),
            odb_metrics: None,
            live_metrics: None,
            checkpoint_rx: None,
            rate_limiter,
            started: false,
            channel_capacity,
        })
    }

    /// Seed trade-ID continuity tracker for a (symbol, threshold) pair.
    ///
    /// Call before start() to seed from ClickHouse's last_agg_trade_id.
    pub fn seed_trade_id(
        &mut self,
        symbol: &str,
        _threshold: u32,
        last_agg_trade_id: i64,
    ) -> PyResult<()> {
        if self.started {
            return Err(PyRuntimeError::new_err(
                "Cannot seed trade ID after start()",
            ));
        }
        self.trade_id_seeds
            .push((symbol.to_uppercase(), last_agg_trade_id));
        Ok(())
    }

    /// Inject a processor checkpoint before start().
    pub fn set_checkpoint(
        &mut self,
        py: Python,
        symbol: &str,
        threshold: u32,
        checkpoint: &Bound<'_, PyDict>,
    ) -> PyResult<()> {
        if self.started {
            return Err(PyRuntimeError::new_err(
                "Cannot set checkpoint after start()",
            ));
        }

        let cp = helpers::dict_to_checkpoint(py, checkpoint)?;

        if let Some(ref mut source) = self.source {
            source
                .engine_mut()
                .set_initial_checkpoint(symbol, threshold, cp);
        }
        Ok(())
    }

    /// Start the engine (WebSocket connections + processing + fan-out loop).
    pub fn start(&mut self, py: Python) -> PyResult<()> {
        if self.started {
            return Ok(());
        }

        let runtime = tokio::runtime::Builder::new_multi_thread()
            .worker_threads(2)
            .enable_all()
            .build()
            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create runtime: {e}")))?;

        let _guard = runtime.enter();

        // Take ownership of source for the engine
        let mut source = self
            .source
            .take()
            .ok_or_else(|| PyRuntimeError::new_err("Source already consumed"))?;

        // Extract shared metrics and checkpoint receiver BEFORE moving the engine.
        // These remain accessible in PyOdbEngine after the engine task is spawned.
        self.live_metrics = Some(source.engine().shared_metrics());
        self.checkpoint_rx = source.engine_mut().take_checkpoint_receiver();

        // Start the underlying LiveBarEngine (spawns WS tasks)
        source
            .engine_mut()
            .start()
            .map_err(|e| PyValueError::new_err(format!("Failed to start engine: {e}")))?;

        // Create the bounded channel for bar delivery (ChannelSink → next_bar)
        let (tx, rx) = tokio::sync::mpsc::channel(self.channel_capacity);
        self.bar_rx = Some(rx);

        // Build the OdbEngine with ChannelSink
        let shutdown = tokio_util::sync::CancellationToken::new();
        let channel_sink = opendeviationbar_streaming::ChannelSink::new(tx);
        let clock = opendeviationbar_streaming::LiveClock;

        let mut engine = opendeviationbar_streaming::OdbEngine::new(
            Box::new(source),
            vec![Box::new(channel_sink)],
            Box::new(clock),
            shutdown.clone(),
        );

        // Apply trade-ID seeds
        for (symbol, tid) in &self.trade_id_seeds {
            engine.seed_trade_id(symbol, *tid);
        }

        self.odb_metrics = Some(engine.metrics().clone());
        self.shutdown = Some(shutdown);

        // Spawn the engine loop as a background tokio task
        tokio::spawn(async move {
            engine.run().await;
        });

        self.runtime = Some(runtime);
        self.started = true;

        // Brief GIL release to let WS connections initialize
        py.detach(|| {
            std::thread::sleep(std::time::Duration::from_millis(100));
        });

        Ok(())
    }

    /// Get the next completed bar. Blocks until available or timeout.
    ///
    /// Returns a dict with all columns plus `_symbol` and `_threshold` metadata.
    #[pyo3(signature = (timeout_ms=5000))]
    pub fn next_bar(&mut self, py: Python, timeout_ms: u64) -> PyResult<Option<PyObject>> {
        let runtime = self
            .runtime
            .as_ref()
            .ok_or_else(|| PyRuntimeError::new_err("Not started. Call start() first."))?;
        let rx = self
            .bar_rx
            .as_mut()
            .ok_or_else(|| PyRuntimeError::new_err("Channel not available"))?;

        let completed = py.detach(|| {
            runtime.block_on(async {
                tokio::time::timeout(tokio::time::Duration::from_millis(timeout_ms), rx.recv())
                    .await
                    .ok()
                    .flatten()
            })
        });

        match completed {
            Some(bar) => {
                let dict = helpers::opendeviationbar_to_dict(py, &bar.bar)?;
                let py_dict: &Bound<'_, PyDict> = dict.cast_bound(py)?;
                py_dict.set_item(pyo3::types::PyString::intern(py, "_symbol"), &*bar.symbol)?;
                py_dict.set_item(
                    pyo3::types::PyString::intern(py, "_threshold"),
                    bar.threshold_decimal_bps,
                )?;
                Ok(Some(dict))
            }
            None => Ok(None),
        }
    }

    /// Graceful stop (signals all tasks to wind down).
    pub fn stop(&mut self) -> PyResult<()> {
        if let Some(ref shutdown) = self.shutdown {
            shutdown.cancel();
        }
        self.started = false;
        Ok(())
    }

    /// Collect processor checkpoints for state recovery.
    ///
    /// Call after stop(). Each symbol task sends its processor checkpoints
    /// through the channel before exiting. Returns a dict of "SYMBOL:THRESHOLD" → checkpoint.
    #[pyo3(signature = (timeout_ms=5000))]
    pub fn collect_checkpoints(&mut self, py: Python, timeout_ms: u64) -> PyResult<PyObject> {
        let dict = PyDict::new(py);

        let Some(rx) = self.checkpoint_rx.take() else {
            return Ok(dict.into_any().unbind());
        };

        let Some(runtime) = self.runtime.as_ref() else {
            return Ok(dict.into_any().unbind());
        };

        // Drain all available checkpoints within timeout
        let checkpoints = py.detach(|| {
            runtime.block_on(async {
                let mut result = std::collections::HashMap::new();
                let deadline =
                    tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
                let mut rx = rx;
                loop {
                    tokio::select! {
                        item = rx.recv() => {
                            match item {
                                Some((symbol, threshold, cp)) => {
                                    let key = format!("{symbol}:{threshold}");
                                    result.insert(key, cp);
                                }
                                None => break,
                            }
                        }
                        () = tokio::time::sleep_until(deadline) => {
                            break;
                        }
                    }
                }
                result
            })
        });

        for (key, cp) in &checkpoints {
            dict.set_item(key, helpers::checkpoint_to_dict(py, cp)?)?;
        }
        Ok(dict.into_any().unbind())
    }

    /// Fully release engine resources.
    pub fn shutdown(&mut self) -> PyResult<()> {
        if let Some(ref shutdown) = self.shutdown {
            shutdown.cancel();
        }
        self.started = false;
        if let Some(runtime) = self.runtime.take() {
            runtime.shutdown_background();
        }
        Ok(())
    }

    /// Get combined metrics (engine + live engine + rate limiter).
    ///
    /// Returns ALL keys expected by sidecar watchdog subsystems:
    /// - trades_received, bars_emitted, dropped_bars (from LiveBarEngine)
    /// - reconnections, gap_fills, gap_trades_recovered (from LiveBarEngine)
    /// - bars_produced, trade_id_gaps, trade_id_verified (from OdbEngine)
    /// - rate_limiter_utilization, rate_limiter_remaining
    pub fn get_metrics(&self, py: Python) -> PyResult<PyObject> {
        let dict = PyDict::new(py);
        let relaxed = std::sync::atomic::Ordering::Relaxed;

        // LiveBarEngine metrics (sidecar watchdog expects these exact keys)
        if let Some(ref lm) = self.live_metrics {
            dict.set_item("trades_received", lm.trades_received.load(relaxed))?;
            dict.set_item("bars_emitted", lm.bars_emitted.load(relaxed))?;
            dict.set_item("reconnections", lm.reconnections.load(relaxed))?;
            dict.set_item("dropped_bars", lm.dropped_bars.load(relaxed))?;
            dict.set_item("gap_fills", lm.gap_fills.load(relaxed))?;
            dict.set_item(
                "gap_trades_recovered",
                lm.gap_trades_recovered.load(relaxed),
            )?;
        }

        // OdbEngine metrics (Stathera verification, fan-out)
        if let Some(ref om) = self.odb_metrics {
            dict.set_item("bars_produced", om.bars_produced.load(relaxed))?;
            dict.set_item("trade_id_gaps", om.trade_id_gaps.load(relaxed))?;
            dict.set_item("trade_id_verified", om.trade_id_verified.load(relaxed))?;
        }

        // Rate limiter
        dict.set_item("rate_limiter_utilization", self.rate_limiter.utilization())?;
        dict.set_item(
            "rate_limiter_remaining",
            self.rate_limiter.remaining() as u64,
        )?;

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

    /// Whether the engine has been started.
    #[getter]
    pub const fn is_started(&self) -> bool {
        self.started
    }

    fn __repr__(&self) -> String {
        format!(
            "OdbEngine(started={}, channel_capacity={}, rate_limiter_remaining={})",
            self.started,
            self.channel_capacity,
            self.rate_limiter.remaining()
        )
    }
}

impl Drop for PyOdbEngine {
    fn drop(&mut self) {
        if let Some(ref shutdown) = self.shutdown {
            shutdown.cancel();
        }
        if let Some(runtime) = self.runtime.take() {
            runtime.shutdown_background();
        }
    }
}
