//! Market data collector for persistent PIT (point-in-time) archiving.
//!
//! Captures full L2 orderbooks, trade-by-trade history, and market/event metadata.
//! Two modes: pipeline integration (record_orderbook/record_trades) and standalone daemon (start/stop).

use pyo3::prelude::*;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::{debug, info, warn};

use crate::data_fetcher;
use crate::db::Database;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn now_f64() -> f64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs_f64()
}

fn clamp_interval(interval: f64) -> f64 {
    if interval.is_nan() || interval.is_infinite() || interval <= 0.0 {
        5.0
    } else {
        interval.max(1.0)
    }
}

// ---------------------------------------------------------------------------
// CollectorConfig
// ---------------------------------------------------------------------------

/// Configuration for the market data collector.
#[pyclass]
#[derive(Debug, Clone)]
pub struct CollectorConfig {
    #[pyo3(get, set)]
    pub market_ids: Vec<String>,
    #[pyo3(get, set)]
    pub exchange: String,
    #[pyo3(get, set)]
    pub orderbook_interval_secs: f64,
    #[pyo3(get, set)]
    pub trade_poll_interval_secs: f64,
    #[pyo3(get, set)]
    pub metadata_interval_secs: f64,
    #[pyo3(get, set)]
    pub max_book_levels: usize,
    #[pyo3(get, set)]
    pub db_path: String,
}

#[pymethods]
impl CollectorConfig {
    #[new]
    #[pyo3(signature = (
        market_ids,
        exchange = "polymarket".to_string(),
        orderbook_interval_secs = 5.0,
        trade_poll_interval_secs = 10.0,
        metadata_interval_secs = 300.0,
        max_book_levels = 0,
        db_path = "collector.db".to_string(),
    ))]
    fn new(
        market_ids: Vec<String>,
        exchange: String,
        orderbook_interval_secs: f64,
        trade_poll_interval_secs: f64,
        metadata_interval_secs: f64,
        max_book_levels: usize,
        db_path: String,
    ) -> Self {
        Self {
            market_ids,
            exchange,
            orderbook_interval_secs,
            trade_poll_interval_secs,
            metadata_interval_secs,
            max_book_levels,
            db_path,
        }
    }

    fn __repr__(&self) -> String {
        format!(
            "CollectorConfig(markets={}, exchange='{}', ob_interval={:.1}s)",
            self.market_ids.len(),
            self.exchange,
            self.orderbook_interval_secs,
        )
    }
}

// ---------------------------------------------------------------------------
// CollectorStatus
// ---------------------------------------------------------------------------

/// Runtime status of the collector.
#[pyclass(get_all)]
#[derive(Debug, Clone)]
pub struct CollectorStatus {
    pub is_running: bool,
    pub uptime_secs: f64,
    pub ob_snapshots_total: u64,
    pub trades_total: u64,
    pub metadata_updates_total: u64,
    pub errors_total: u64,
    pub last_error: String,
    pub markets_active: usize,
    pub db_size_bytes: u64,
}

#[pymethods]
impl CollectorStatus {
    fn __repr__(&self) -> String {
        format!(
            "CollectorStatus(running={}, uptime={:.0}s, obs={}, trades={}, errors={})",
            self.is_running,
            self.uptime_secs,
            self.ob_snapshots_total,
            self.trades_total,
            self.errors_total,
        )
    }
}

// ---------------------------------------------------------------------------
// Shared counters
// ---------------------------------------------------------------------------

struct CollectorCounters {
    ob_snapshots: AtomicU64,
    trades: AtomicU64,
    metadata_updates: AtomicU64,
    errors: AtomicU64,
    last_error: Mutex<String>,
}

impl CollectorCounters {
    fn new() -> Self {
        Self {
            ob_snapshots: AtomicU64::new(0),
            trades: AtomicU64::new(0),
            metadata_updates: AtomicU64::new(0),
            errors: AtomicU64::new(0),
            last_error: Mutex::new(String::new()),
        }
    }

    fn record_error(&self, msg: &str) {
        self.errors.fetch_add(1, Ordering::Relaxed);
        if let Ok(mut last) = self.last_error.lock() {
            *last = msg.to_string();
        }
    }
}

// ---------------------------------------------------------------------------
// Async collection loops
// ---------------------------------------------------------------------------

async fn collect_orderbooks_loop(
    market_id: String,
    exchange: String,
    interval_secs: f64,
    max_levels: usize,
    db: Arc<Mutex<Database>>,
    counters: Arc<CollectorCounters>,
    shutdown: Arc<tokio::sync::Notify>,
) {
    let interval = tokio::time::Duration::from_secs_f64(clamp_interval(interval_secs));

    loop {
        tokio::select! {
            _ = shutdown.notified() => break,
            _ = tokio::time::sleep(interval) => {
                let result = match exchange.as_str() {
                    "kalshi" => data_fetcher::fetch_kalshi_book_async(&market_id).await,
                    _ => data_fetcher::fetch_polymarket_book_async(&market_id).await,
                };

                match result {
                    Ok(ob) => {
                        let mut bids = ob.bids.clone();
                        let mut asks = ob.asks.clone();

                        if max_levels > 0 {
                            bids.truncate(max_levels);
                            asks.truncate(max_levels);
                        }

                        let best_bid = bids.first().map(|b| b.0).unwrap_or(0.0);
                        let best_ask = asks.first().map(|a| a.0).unwrap_or(0.0);
                        let mid = if best_bid > 0.0 && best_ask > 0.0 {
                            (best_bid + best_ask) / 2.0
                        } else {
                            0.0
                        };
                        let spread = if best_ask > best_bid { best_ask - best_bid } else { 0.0 };

                        let bids_json = serde_json::to_string(&bids).unwrap_or_else(|_| "[]".into());
                        let asks_json = serde_json::to_string(&asks).unwrap_or_else(|_| "[]".into());

                        if let Ok(db) = db.lock() {
                            if let Err(e) = db.insert_orderbook_snapshot(
                                &market_id, &exchange,
                                &bids_json, &asks_json,
                                bids.len() as i64, asks.len() as i64,
                                best_bid, best_ask, mid, spread,
                                ob.timestamp,
                            ) {
                                warn!(market = %market_id, error = %e, "failed to insert OB snapshot");
                                counters.record_error(&format!("ob insert: {}", e));
                            } else {
                                counters.ob_snapshots.fetch_add(1, Ordering::Relaxed);
                                debug!(market = %market_id, "OB snapshot recorded");
                            }
                        }
                    }
                    Err(e) => {
                        warn!(market = %market_id, error = %e, "orderbook fetch failed");
                        counters.record_error(&format!("ob fetch {}: {}", market_id, e));
                    }
                }
            }
        }
    }
}

async fn collect_trades_loop(
    market_id: String,
    exchange: String,
    interval_secs: f64,
    db: Arc<Mutex<Database>>,
    counters: Arc<CollectorCounters>,
    shutdown: Arc<tokio::sync::Notify>,
) {
    let interval = tokio::time::Duration::from_secs_f64(clamp_interval(interval_secs));

    loop {
        tokio::select! {
            _ = shutdown.notified() => break,
            _ = tokio::time::sleep(interval) => {
                match exchange.as_str() {
                    "kalshi" => {
                        // Kalshi doesn't expose a public trades endpoint in the same way;
                        // skip trade collection for Kalshi
                        debug!(market = %market_id, "Kalshi trade collection not yet supported");
                    }
                    _ => {
                        // Polymarket trades
                        match data_fetcher::fetch_polymarket_trades_async(&market_id, 100).await {
                            Ok(trades) => {
                                let mut inserted = 0u64;
                                if let Ok(db) = db.lock() {
                                    for trade in &trades {
                                        let trade_id = format!("{:.0}_{:.4}_{:.1}",
                                            trade.timestamp, trade.price, trade.size);
                                        if let Err(e) = db.insert_market_trade(
                                            &trade_id, &market_id, &exchange,
                                            trade.price, trade.size, &trade.side,
                                            "", "", trade.timestamp,
                                        ) {
                                            warn!(error = %e, "failed to insert trade");
                                            counters.record_error(&format!("trade insert: {}", e));
                                        } else {
                                            inserted += 1;
                                        }
                                    }
                                }
                                counters.trades.fetch_add(inserted, Ordering::Relaxed);
                                debug!(market = %market_id, count = inserted, "trades recorded");
                            }
                            Err(e) => {
                                warn!(market = %market_id, error = %e, "trade fetch failed");
                                counters.record_error(&format!("trade fetch {}: {}", market_id, e));
                            }
                        }
                    }
                }
            }
        }
    }
}

async fn collect_metadata_loop(
    market_ids: Vec<String>,
    exchange: String,
    interval_secs: f64,
    db: Arc<Mutex<Database>>,
    counters: Arc<CollectorCounters>,
    shutdown: Arc<tokio::sync::Notify>,
) {
    let interval = tokio::time::Duration::from_secs_f64(clamp_interval(interval_secs));

    loop {
        tokio::select! {
            _ = shutdown.notified() => break,
            _ = tokio::time::sleep(interval) => {
                for market_id in &market_ids {
                    let result = match exchange.as_str() {
                        "kalshi" => data_fetcher::fetch_kalshi_market_async(market_id).await,
                        _ => data_fetcher::fetch_polymarket_market_async(market_id).await,
                    };

                    match result {
                        Ok(meta) => {
                            if let Ok(db) = db.lock() {
                                let name = meta.get("question")
                                    .or_else(|| meta.get("title"))
                                    .and_then(|v| v.as_str())
                                    .unwrap_or("");
                                let question = meta.get("question")
                                    .and_then(|v| v.as_str())
                                    .unwrap_or("");
                                let description = meta.get("description")
                                    .and_then(|v| v.as_str())
                                    .unwrap_or("");
                                let slug = meta.get("slug")
                                    .and_then(|v| v.as_str())
                                    .unwrap_or("");
                                let condition_id = meta.get("condition_id")
                                    .and_then(|v| v.as_str())
                                    .unwrap_or("");
                                let status = meta.get("status")
                                    .or_else(|| meta.get("active"))
                                    .and_then(|v| v.as_str())
                                    .unwrap_or("");
                                let volume = meta.get("volume")
                                    .and_then(|v| v.as_f64().or_else(|| v.as_str().and_then(|s| s.parse().ok())))
                                    .unwrap_or(0.0);
                                let liquidity = meta.get("liquidity")
                                    .and_then(|v| v.as_f64().or_else(|| v.as_str().and_then(|s| s.parse().ok())))
                                    .unwrap_or(0.0);
                                let neg_risk = meta.get("neg_risk")
                                    .and_then(|v| v.as_bool())
                                    .unwrap_or(false);

                                // Extract token IDs from tokens array
                                let (yes_tok, no_tok) = extract_token_ids(&meta);

                                let expiry = meta.get("end_date_iso")
                                    .or_else(|| meta.get("expiration_time"))
                                    .and_then(|v| v.as_str())
                                    .unwrap_or("");
                                let created = meta.get("created_at")
                                    .and_then(|v| v.as_str())
                                    .unwrap_or("");
                                let rewards_json = meta.get("rewards")
                                    .map(|v| v.to_string())
                                    .unwrap_or_default();

                                if let Err(e) = db.upsert_market_metadata(
                                    market_id, &exchange,
                                    name, question, description, slug,
                                    &yes_tok, &no_tok, condition_id,
                                    neg_risk, status, volume, liquidity,
                                    expiry, &rewards_json, created,
                                ) {
                                    warn!(market = %market_id, error = %e, "metadata upsert failed");
                                    counters.record_error(&format!("metadata upsert: {}", e));
                                } else {
                                    counters.metadata_updates.fetch_add(1, Ordering::Relaxed);
                                }
                            }
                        }
                        Err(e) => {
                            debug!(market = %market_id, error = %e, "metadata fetch failed");
                            counters.record_error(&format!("metadata {}: {}", market_id, e));
                        }
                    }
                }
            }
        }
    }
}

fn extract_token_ids(meta: &serde_json::Value) -> (String, String) {
    if let Some(tokens) = meta.get("tokens").and_then(|v| v.as_array()) {
        let mut yes = String::new();
        let mut no = String::new();
        for tok in tokens {
            let outcome = tok.get("outcome").and_then(|v| v.as_str()).unwrap_or("");
            let token_id = tok
                .get("token_id")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            match outcome {
                "Yes" => yes = token_id.to_string(),
                "No" => no = token_id.to_string(),
                _ => {}
            }
        }
        (yes, no)
    } else {
        (String::new(), String::new())
    }
}

// ---------------------------------------------------------------------------
// Collector (pyclass)
// ---------------------------------------------------------------------------

/// Market data collector: captures orderbooks, trades, and metadata to SQLite.
#[pyclass]
pub struct Collector {
    config: CollectorConfig,
    db: Arc<Mutex<Database>>,
    runtime: Option<tokio::runtime::Runtime>,
    shutdown: Arc<tokio::sync::Notify>,
    is_running: Arc<AtomicBool>,
    started_at: f64,
    counters: Arc<CollectorCounters>,
    run_id: String,
}

#[pymethods]
impl Collector {
    #[new]
    fn new(config: CollectorConfig) -> PyResult<Self> {
        let db = Database::open(&config.db_path).map_err(|e| {
            pyo3::exceptions::PyRuntimeError::new_err(format!("failed to open database: {}", e))
        })?;

        let run_id = uuid::Uuid::new_v4().to_string();

        Ok(Self {
            config,
            db: Arc::new(Mutex::new(db)),
            runtime: None,
            shutdown: Arc::new(tokio::sync::Notify::new()),
            is_running: Arc::new(AtomicBool::new(false)),
            started_at: 0.0,
            counters: Arc::new(CollectorCounters::new()),
            run_id,
        })
    }

    /// Start the collector daemon (spawns async loops for each market).
    fn start(&mut self) -> PyResult<()> {
        if self.is_running.load(Ordering::Relaxed) {
            return Err(pyo3::exceptions::PyRuntimeError::new_err(
                "collector is already running",
            ));
        }

        if self.config.market_ids.is_empty() {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "market_ids must not be empty",
            ));
        }

        let rt = tokio::runtime::Builder::new_multi_thread()
            .worker_threads(2)
            .enable_all()
            .build()
            .map_err(|e| {
                pyo3::exceptions::PyRuntimeError::new_err(format!("runtime creation failed: {}", e))
            })?;

        self.started_at = now_f64();
        self.is_running.store(true, Ordering::Relaxed);

        // Record collector run start
        let config_json =
            serde_json::to_string(&serde_json::json!({
                "market_ids": self.config.market_ids,
                "exchange": self.config.exchange,
                "ob_interval": self.config.orderbook_interval_secs,
                "trade_interval": self.config.trade_poll_interval_secs,
                "metadata_interval": self.config.metadata_interval_secs,
            }))
            .unwrap_or_default();

        if let Ok(db) = self.db.lock() {
            let _ = db.start_collector_run(&self.run_id, &config_json);
        }

        // Spawn per-market orderbook + trade loops
        for market_id in &self.config.market_ids {
            let db = Arc::clone(&self.db);
            let counters = Arc::clone(&self.counters);
            let shutdown = Arc::clone(&self.shutdown);
            let mid = market_id.clone();
            let exchange = self.config.exchange.clone();
            let ob_interval = self.config.orderbook_interval_secs;
            let max_levels = self.config.max_book_levels;

            rt.spawn(collect_orderbooks_loop(
                mid.clone(),
                exchange.clone(),
                ob_interval,
                max_levels,
                Arc::clone(&db),
                Arc::clone(&counters),
                Arc::clone(&shutdown),
            ));

            rt.spawn(collect_trades_loop(
                mid,
                exchange,
                self.config.trade_poll_interval_secs,
                db,
                counters,
                shutdown,
            ));
        }

        // Spawn metadata loop (one for all markets)
        let db = Arc::clone(&self.db);
        let counters = Arc::clone(&self.counters);
        let shutdown = Arc::clone(&self.shutdown);
        let market_ids = self.config.market_ids.clone();
        let exchange = self.config.exchange.clone();
        let meta_interval = self.config.metadata_interval_secs;

        rt.spawn(collect_metadata_loop(
            market_ids,
            exchange,
            meta_interval,
            db,
            counters,
            shutdown,
        ));

        self.runtime = Some(rt);
        info!(run_id = %self.run_id, markets = self.config.market_ids.len(), "collector started");
        Ok(())
    }

    /// Stop the collector daemon.
    fn stop(&mut self) -> PyResult<()> {
        if !self.is_running.load(Ordering::Relaxed) {
            return Ok(());
        }

        self.shutdown.notify_waiters();
        self.is_running.store(false, Ordering::Relaxed);

        // Record collector run end
        if let Ok(db) = self.db.lock() {
            let _ = db.end_collector_run(
                &self.run_id,
                self.config.market_ids.len() as i64,
                self.counters.ob_snapshots.load(Ordering::Relaxed) as i64,
                self.counters.trades.load(Ordering::Relaxed) as i64,
            );
        }

        // Drop runtime (waits for tasks to finish)
        if let Some(rt) = self.runtime.take() {
            rt.shutdown_timeout(std::time::Duration::from_secs(5));
        }

        info!(run_id = %self.run_id, "collector stopped");
        Ok(())
    }

    /// Get current collector status.
    fn status(&self) -> CollectorStatus {
        let db_size = if self.config.db_path != ":memory:" {
            std::fs::metadata(&self.config.db_path)
                .map(|m| m.len())
                .unwrap_or(0)
        } else {
            0
        };

        let last_error = self
            .counters
            .last_error
            .lock()
            .map(|e| e.clone())
            .unwrap_or_default();

        CollectorStatus {
            is_running: self.is_running.load(Ordering::Relaxed),
            uptime_secs: if self.started_at > 0.0 {
                now_f64() - self.started_at
            } else {
                0.0
            },
            ob_snapshots_total: self.counters.ob_snapshots.load(Ordering::Relaxed),
            trades_total: self.counters.trades.load(Ordering::Relaxed),
            metadata_updates_total: self.counters.metadata_updates.load(Ordering::Relaxed),
            errors_total: self.counters.errors.load(Ordering::Relaxed),
            last_error,
            markets_active: self.config.market_ids.len(),
            db_size_bytes: db_size,
        }
    }

    /// Direct insert of an orderbook snapshot (pipeline mode).
    #[pyo3(signature = (market_id, exchange, bids_json, asks_json, best_bid, best_ask, mid_price, spread, timestamp))]
    fn record_orderbook(
        &self,
        market_id: &str,
        exchange: &str,
        bids_json: &str,
        asks_json: &str,
        best_bid: f64,
        best_ask: f64,
        mid_price: f64,
        spread: f64,
        timestamp: f64,
    ) -> PyResult<()> {
        let db = self.db.lock().map_err(|e| {
            pyo3::exceptions::PyRuntimeError::new_err(format!("db lock failed: {}", e))
        })?;

        // Count levels from JSON
        let bid_count = serde_json::from_str::<serde_json::Value>(bids_json)
            .ok()
            .and_then(|v| v.as_array().map(|a| a.len()))
            .unwrap_or(0) as i64;
        let ask_count = serde_json::from_str::<serde_json::Value>(asks_json)
            .ok()
            .and_then(|v| v.as_array().map(|a| a.len()))
            .unwrap_or(0) as i64;

        db.insert_orderbook_snapshot(
            market_id, exchange, bids_json, asks_json, bid_count, ask_count, best_bid, best_ask,
            mid_price, spread, timestamp,
        )
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("insert failed: {}", e)))?;

        self.counters.ob_snapshots.fetch_add(1, Ordering::Relaxed);
        Ok(())
    }

    /// Batch insert trade records (pipeline mode). trades_json is a JSON array.
    fn record_trades(&self, trades_json: &str, market_id: &str, exchange: &str) -> PyResult<u64> {
        let trades: Vec<serde_json::Value> = serde_json::from_str(trades_json).map_err(|e| {
            pyo3::exceptions::PyValueError::new_err(format!("invalid JSON: {}", e))
        })?;

        let db = self.db.lock().map_err(|e| {
            pyo3::exceptions::PyRuntimeError::new_err(format!("db lock failed: {}", e))
        })?;

        let mut count = 0u64;
        for trade in &trades {
            let trade_id = trade
                .get("trade_id")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            let price = trade
                .get("price")
                .and_then(|v| v.as_f64())
                .unwrap_or(0.0);
            let size = trade
                .get("size")
                .and_then(|v| v.as_f64())
                .unwrap_or(0.0);
            let side = trade.get("side").and_then(|v| v.as_str()).unwrap_or("");
            let ts = trade
                .get("timestamp")
                .and_then(|v| v.as_f64())
                .unwrap_or(0.0);

            if db
                .insert_market_trade(trade_id, market_id, exchange, price, size, side, "", "", ts)
                .is_ok()
            {
                count += 1;
            }
        }

        self.counters.trades.fetch_add(count, Ordering::Relaxed);
        Ok(count)
    }

    /// Query orderbook snapshots.
    #[pyo3(signature = (market_id, start_ts = 0.0, end_ts = f64::MAX, limit = 1000))]
    fn query_orderbooks(
        &self,
        market_id: &str,
        start_ts: f64,
        end_ts: f64,
        limit: u32,
    ) -> PyResult<String> {
        let db = self.db.lock().map_err(|e| {
            pyo3::exceptions::PyRuntimeError::new_err(format!("db lock failed: {}", e))
        })?;

        let rows = db
            .query_orderbook_snapshots(market_id, start_ts, end_ts, limit)
            .map_err(|e| {
                pyo3::exceptions::PyRuntimeError::new_err(format!("query failed: {}", e))
            })?;

        let results: Vec<serde_json::Value> = rows
            .into_iter()
            .map(|(mid, bids, asks, bb, ba, mp, sp, ts)| {
                serde_json::json!({
                    "market_id": mid,
                    "bids": serde_json::from_str::<serde_json::Value>(&bids).unwrap_or_default(),
                    "asks": serde_json::from_str::<serde_json::Value>(&asks).unwrap_or_default(),
                    "best_bid": bb,
                    "best_ask": ba,
                    "mid_price": mp,
                    "spread": sp,
                    "timestamp": ts,
                })
            })
            .collect();

        serde_json::to_string(&results).map_err(|e| {
            pyo3::exceptions::PyRuntimeError::new_err(format!("JSON serialize failed: {}", e))
        })
    }

    /// Query market trades.
    #[pyo3(signature = (market_id, start_ts = 0.0, end_ts = f64::MAX, limit = 1000))]
    fn query_trades(
        &self,
        market_id: &str,
        start_ts: f64,
        end_ts: f64,
        limit: u32,
    ) -> PyResult<String> {
        let db = self.db.lock().map_err(|e| {
            pyo3::exceptions::PyRuntimeError::new_err(format!("db lock failed: {}", e))
        })?;

        let rows = db
            .query_market_trades(market_id, start_ts, end_ts, limit)
            .map_err(|e| {
                pyo3::exceptions::PyRuntimeError::new_err(format!("query failed: {}", e))
            })?;

        let results: Vec<serde_json::Value> = rows
            .into_iter()
            .map(|(tid, mid, price, size, side, ts)| {
                serde_json::json!({
                    "trade_id": tid,
                    "market_id": mid,
                    "price": price,
                    "size": size,
                    "side": side,
                    "timestamp": ts,
                })
            })
            .collect();

        serde_json::to_string(&results).map_err(|e| {
            pyo3::exceptions::PyRuntimeError::new_err(format!("JSON serialize failed: {}", e))
        })
    }

    /// Query market metadata as JSON string.
    fn query_metadata(&self, market_id: &str) -> PyResult<Option<String>> {
        let db = self.db.lock().map_err(|e| {
            pyo3::exceptions::PyRuntimeError::new_err(format!("db lock failed: {}", e))
        })?;

        db.query_market_metadata(market_id).map_err(|e| {
            pyo3::exceptions::PyRuntimeError::new_err(format!("query failed: {}", e))
        })
    }

    /// Purge data older than max_age_secs. Returns total rows deleted.
    #[pyo3(signature = (max_age_secs = 86400.0))]
    fn purge(&self, max_age_secs: f64) -> PyResult<usize> {
        let db = self.db.lock().map_err(|e| {
            pyo3::exceptions::PyRuntimeError::new_err(format!("db lock failed: {}", e))
        })?;

        let ob_deleted = db.purge_old_ob_snapshots(max_age_secs).unwrap_or(0);
        let trade_deleted = db.purge_old_market_trades(max_age_secs).unwrap_or(0);

        Ok(ob_deleted + trade_deleted)
    }
}

/// Register collector types in the Python module.
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<CollectorConfig>()?;
    m.add_class::<CollectorStatus>()?;
    m.add_class::<Collector>()?;
    Ok(())
}
