// Issue #257: PyO3 bindings for gap detection and fill (separate pyclass for borrow safety)

use super::*;

/// Separate pyclass for receiving gap events (Issue #257).
///
/// Extracted from PyStreamManager so that `next_event(&self)` on this object
/// doesn't conflict with `next_bar(&mut self)` on the manager. Same pattern
/// as `PyFormingBarWatcher` (Issue #214).
#[pyclass(name = "GapEventWatcher")]
pub struct PyGapEventWatcher {
    rx: Option<tokio::sync::mpsc::Receiver<opendeviationbar_streaming::GapEvent>>,
    runtime_handle: Option<tokio::runtime::Handle>,
}

impl PyGapEventWatcher {
    /// Create a new watcher from the receiver and optional runtime handle.
    /// Called from `PyStreamManager::take_gap_event_watcher()`.
    /// If no handle is provided (pre-start), non-blocking `next_event(0)` still works.
    /// The blocking path requires a handle and will error if missing.
    pub fn new(
        rx: tokio::sync::mpsc::Receiver<opendeviationbar_streaming::GapEvent>,
        runtime_handle: Option<tokio::runtime::Handle>,
    ) -> Self {
        Self {
            rx: Some(rx),
            runtime_handle,
        }
    }

    /// Set the runtime handle (called after start() when handle becomes available).
    pub fn set_runtime_handle(&mut self, handle: tokio::runtime::Handle) {
        self.runtime_handle = Some(handle);
    }
}

#[pymethods]
impl PyGapEventWatcher {
    /// Poll for the next gap event.
    ///
    /// Args:
    ///     timeout_ms: Maximum time to wait (0 = non-blocking poll).
    ///
    /// Returns:
    ///     dict with gap event fields, or None if no event within timeout.
    ///     Fields: symbol, expected_tid, actual_tid, gap_size, detected_at_ms, filled_inline
    #[pyo3(signature = (timeout_ms=0))]
    pub fn next_event(&mut self, py: Python, timeout_ms: u64) -> PyResult<Option<PyObject>> {
        let rx = self
            .rx
            .as_mut()
            .ok_or_else(|| PyRuntimeError::new_err("GapEventWatcher receiver not available"))?;

        let event = if timeout_ms == 0 {
            // Non-blocking poll — no runtime handle needed
            rx.try_recv().ok()
        } else {
            // Blocking with timeout, release GIL — requires runtime handle
            let handle = self
                .runtime_handle
                .as_ref()
                .ok_or_else(|| PyRuntimeError::new_err(
                    "Runtime handle not available. Use timeout_ms=0 for non-blocking poll, \
                     or call take_gap_event_watcher() after start()."
                ))?;
            let timeout = tokio::time::Duration::from_millis(timeout_ms);
            py.detach(|| {
                handle.block_on(async {
                    tokio::time::timeout(timeout, rx.recv()).await.ok().flatten()
                })
            })
        };

        match event {
            Some(gap_event) => {
                let dict = PyDict::new(py);
                dict.set_item(
                    pyo3::types::PyString::intern(py, "symbol"),
                    &*gap_event.symbol,
                )?;
                dict.set_item(
                    pyo3::types::PyString::intern(py, "expected_tid"),
                    gap_event.expected_tid,
                )?;
                dict.set_item(
                    pyo3::types::PyString::intern(py, "actual_tid"),
                    gap_event.actual_tid,
                )?;
                dict.set_item(
                    pyo3::types::PyString::intern(py, "gap_size"),
                    gap_event.gap_size,
                )?;
                dict.set_item(
                    pyo3::types::PyString::intern(py, "detected_at_ms"),
                    gap_event.detected_at_ms,
                )?;
                dict.set_item(
                    pyo3::types::PyString::intern(py, "filled_inline"),
                    gap_event.filled_inline,
                )?;
                Ok(Some(dict.into_any().unbind()))
            }
            None => Ok(None),
        }
    }
}

/// Result of a gap-fill operation, returned by `StreamManager.fill_gap()`.
#[pyclass(name = "GapFillResult")]
pub struct PyGapFillResult {
    /// Number of trades fetched via REST.
    #[pyo3(get)]
    pub trades_fetched: usize,
    /// Number of bars produced from the fetched trades.
    #[pyo3(get)]
    pub bars_produced: usize,
    /// Wall-clock duration in milliseconds.
    #[pyo3(get)]
    pub duration_ms: u64,
    /// Whether the fill was partial.
    #[pyo3(get)]
    pub partial: bool,
}

#[pymethods]
impl PyGapFillResult {
    fn __repr__(&self) -> String {
        format!(
            "GapFillResult(trades={}, bars={}, duration_ms={}, partial={})",
            self.trades_fetched, self.bars_produced, self.duration_ms, self.partial
        )
    }
}
