pub mod binance;
pub mod chainlink;
pub mod espn;
pub mod kalshi_book;
pub mod manifold;
pub mod nws;
pub mod polymarket_book;
pub mod predictit;
pub mod rest_json_path;

use dashmap::DashMap;
use pyo3::prelude::*;
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{debug, warn};

use crate::orderbook::OrderbookSnapshot;

/// Snapshot of a single feed's latest data.
#[pyclass]
#[derive(Debug, Clone)]
pub struct FeedSnapshot {
    #[pyo3(get)]
    pub price: f64,
    #[pyo3(get)]
    pub timestamp: f64,
    #[pyo3(get)]
    pub source: String,
    #[pyo3(get)]
    pub bid: f64,
    #[pyo3(get)]
    pub ask: f64,
    #[pyo3(get)]
    pub volume_24h: f64,
    #[pyo3(get)]
    pub last_trade_size: f64,
    #[pyo3(get)]
    pub last_trade_is_buy: bool,
}

#[pymethods]
impl FeedSnapshot {
    #[new]
    #[pyo3(signature = (price=0.0, timestamp=0.0, source="", bid=0.0, ask=0.0, volume_24h=0.0, last_trade_size=0.0, last_trade_is_buy=false))]
    fn new(
        price: f64,
        timestamp: f64,
        source: &str,
        bid: f64,
        ask: f64,
        volume_24h: f64,
        last_trade_size: f64,
        last_trade_is_buy: bool,
    ) -> Self {
        Self {
            price,
            timestamp,
            source: source.to_string(),
            bid,
            ask,
            volume_24h,
            last_trade_size,
            last_trade_is_buy,
        }
    }

    fn __repr__(&self) -> String {
        format!(
            "FeedSnapshot(price={:.4}, source='{}', bid={:.4}, ask={:.4}, vol={:.0})",
            self.price, self.source, self.bid, self.ask, self.volume_24h
        )
    }
}

impl Default for FeedSnapshot {
    fn default() -> Self {
        Self {
            price: 0.0,
            timestamp: 0.0,
            source: String::new(),
            bid: 0.0,
            ask: 0.0,
            volume_24h: 0.0,
            last_trade_size: 0.0,
            last_trade_is_buy: false,
        }
    }
}

/// Per-feed connection metrics.
#[pyclass(get_all)]
#[derive(Debug, Clone)]
pub struct FeedMetrics {
    pub update_count: u64,
    pub error_count: u64,
    pub last_update_time: f64,
    pub last_error: String,
    pub is_connected: bool,
    pub reconnect_count: u64,
}

#[pymethods]
impl FeedMetrics {
    fn __repr__(&self) -> String {
        format!(
            "FeedMetrics(updates={}, errors={}, connected={}, reconnects={})",
            self.update_count, self.error_count, self.is_connected, self.reconnect_count,
        )
    }
}

impl Default for FeedMetrics {
    fn default() -> Self {
        Self {
            update_count: 0,
            error_count: 0,
            last_update_time: 0.0,
            last_error: String::new(),
            is_connected: false,
            reconnect_count: 0,
        }
    }
}

/// Shared metrics store for all feeds.
pub type MetricsStore = Arc<DashMap<String, FeedMetrics>>;

/// Feed configuration passed from Python.
#[derive(Debug, Clone)]
pub enum FeedConfig {
    BinanceWS { symbol: String },
    PolymarketBook { market_slug: String },
    KalshiBook { ticker: String },
    REST { url: String, interval_secs: f64 },
    PredictIt {
        market_id: u64,
        contract_id: Option<u64>,
        interval_secs: f64,
    },
    Manifold {
        slug: String,
        interval_secs: f64,
    },
    ESPN {
        sport: String,
        league: String,
        event_id: Option<String>,
        interval_secs: f64,
    },
    NWS {
        mode: String,
        office: String,
        grid_x: u32,
        grid_y: u32,
        state: String,
        user_agent: String,
        interval_secs: f64,
    },
    RESTJsonPath {
        url: String,
        price_path: Option<String>,
        bid_path: Option<String>,
        ask_path: Option<String>,
        volume_path: Option<String>,
        interval_secs: f64,
    },
    Chainlink {
        contract_address: String,
        rpc_url: String,
        decimals: u8,
        interval_secs: f64,
    },
}

/// Shared storage for orderbook snapshots (separate from price-level FeedSnapshot).
/// Uses DashMap for lock-free concurrent reads/writes (feeds writing at different rates
/// no longer block each other or the strategy reads).
pub type OrderbookStore = Arc<DashMap<String, OrderbookSnapshot>>;

/// Manages all running feeds and their latest snapshots.
/// Uses DashMap instead of RwLock<HashMap> so feed writers and strategy readers
/// don't contend on a single lock.
pub struct FeedManager {
    snapshots: Arc<DashMap<String, FeedSnapshot>>,
    orderbooks: OrderbookStore,
    metrics: MetricsStore,
    runtime: tokio::runtime::Runtime,
    shutdown: Arc<tokio::sync::Notify>,
    /// Per-feed shutdown signals for individual feed stop/restart.
    per_feed_shutdown: Arc<DashMap<String, Arc<tokio::sync::Notify>>>,
    /// Per-feed JoinHandles to check if a feed task is still running.
    feed_handles: Arc<std::sync::Mutex<HashMap<String, tokio::task::JoinHandle<()>>>>,
    /// Store feed configs for restart.
    feed_configs: Arc<std::sync::Mutex<HashMap<String, FeedConfig>>>,
}

impl FeedManager {
    pub fn new() -> Result<Self, String> {
        Self::with_worker_threads(2)
    }

    pub fn with_worker_threads(worker_threads: usize) -> Result<Self, String> {
        let runtime = tokio::runtime::Builder::new_multi_thread()
            .worker_threads(worker_threads)
            .enable_all()
            .build()
            .map_err(|e| format!("failed to create feed runtime: {}", e))?;

        Ok(Self {
            snapshots: Arc::new(DashMap::new()),
            orderbooks: Arc::new(DashMap::new()),
            metrics: Arc::new(DashMap::new()),
            runtime,
            shutdown: Arc::new(tokio::sync::Notify::new()),
            per_feed_shutdown: Arc::new(DashMap::new()),
            feed_handles: Arc::new(std::sync::Mutex::new(HashMap::new())),
            feed_configs: Arc::new(std::sync::Mutex::new(HashMap::new())),
        })
    }

    /// Start a feed. Spawns a background task on the tokio runtime.
    pub fn start_feed(&self, name: String, config: FeedConfig) {
        let snapshots = self.snapshots.clone();
        let orderbooks = self.orderbooks.clone();
        let metrics = self.metrics.clone();
        let global_shutdown = self.shutdown.clone();
        let feed_name = name.clone();

        // Per-feed shutdown signal
        let feed_shutdown = Arc::new(tokio::sync::Notify::new());
        self.per_feed_shutdown
            .insert(name.clone(), feed_shutdown.clone());

        // Initialize with empty snapshot and metrics
        snapshots.insert(name.clone(), FeedSnapshot::default());
        metrics.insert(name.clone(), FeedMetrics::default());

        // Store config for potential restart
        if let Ok(mut configs) = self.feed_configs.lock() {
            configs.insert(name.clone(), config.clone());
        }

        debug!(feed = %name, config = ?config, "starting feed");

        let handle = match config {
            FeedConfig::BinanceWS { symbol } => {
                self.runtime.spawn(async move {
                    binance::run_binance_ws(
                        feed_name, symbol, snapshots, metrics, global_shutdown, feed_shutdown,
                    )
                    .await;
                })
            }
            FeedConfig::PolymarketBook { market_slug } => {
                self.runtime.spawn(async move {
                    polymarket_book::run_polymarket_book(
                        feed_name,
                        market_slug,
                        snapshots,
                        orderbooks,
                        metrics,
                        global_shutdown,
                        feed_shutdown,
                    )
                    .await;
                })
            }
            FeedConfig::KalshiBook { ticker } => {
                self.runtime.spawn(async move {
                    kalshi_book::run_kalshi_book(
                        feed_name,
                        ticker,
                        snapshots,
                        orderbooks,
                        metrics,
                        global_shutdown,
                        feed_shutdown,
                    )
                    .await;
                })
            }
            FeedConfig::REST { url, interval_secs } => {
                self.runtime.spawn(async move {
                    run_rest_feed(
                        feed_name,
                        url,
                        interval_secs,
                        snapshots,
                        metrics,
                        global_shutdown,
                        feed_shutdown,
                    )
                    .await;
                })
            }
            FeedConfig::PredictIt {
                market_id,
                contract_id,
                interval_secs,
            } => self.runtime.spawn(async move {
                predictit::run_predictit_feed(
                    feed_name,
                    market_id,
                    contract_id,
                    interval_secs,
                    snapshots,
                    metrics,
                    global_shutdown,
                    feed_shutdown,
                )
                .await;
            }),
            FeedConfig::Manifold { slug, interval_secs } => {
                self.runtime.spawn(async move {
                    manifold::run_manifold_feed(
                        feed_name,
                        slug,
                        interval_secs,
                        snapshots,
                        metrics,
                        global_shutdown,
                        feed_shutdown,
                    )
                    .await;
                })
            }
            FeedConfig::ESPN {
                sport,
                league,
                event_id,
                interval_secs,
            } => self.runtime.spawn(async move {
                espn::run_espn_feed(
                    feed_name,
                    sport,
                    league,
                    event_id,
                    interval_secs,
                    snapshots,
                    metrics,
                    global_shutdown,
                    feed_shutdown,
                )
                .await;
            }),
            FeedConfig::NWS {
                mode,
                office,
                grid_x,
                grid_y,
                state,
                user_agent,
                interval_secs,
            } => self.runtime.spawn(async move {
                nws::run_nws_feed(
                    feed_name,
                    mode,
                    office,
                    grid_x,
                    grid_y,
                    state,
                    user_agent,
                    interval_secs,
                    snapshots,
                    metrics,
                    global_shutdown,
                    feed_shutdown,
                )
                .await;
            }),
            FeedConfig::RESTJsonPath {
                url,
                price_path,
                bid_path,
                ask_path,
                volume_path,
                interval_secs,
            } => self.runtime.spawn(async move {
                rest_json_path::run_rest_json_path_feed(
                    feed_name,
                    url,
                    price_path,
                    bid_path,
                    ask_path,
                    volume_path,
                    interval_secs,
                    snapshots,
                    metrics,
                    global_shutdown,
                    feed_shutdown,
                )
                .await;
            }),
            FeedConfig::Chainlink {
                contract_address,
                rpc_url,
                decimals,
                interval_secs,
            } => self.runtime.spawn(async move {
                chainlink::run_chainlink_feed(
                    feed_name,
                    contract_address,
                    rpc_url,
                    decimals,
                    interval_secs,
                    snapshots,
                    metrics,
                    global_shutdown,
                    feed_shutdown,
                )
                .await;
            }),
        };

        if let Ok(mut handles) = self.feed_handles.lock() {
            handles.insert(name, handle);
        }
    }

    /// Get the latest snapshot for a feed.
    pub fn snapshot(&self, name: &str) -> Option<FeedSnapshot> {
        self.snapshots.get(name).map(|entry| entry.value().clone())
    }

    /// Get all snapshots.
    pub fn all_snapshots(&self) -> HashMap<String, FeedSnapshot> {
        self.snapshots
            .iter()
            .map(|entry| (entry.key().clone(), entry.value().clone()))
            .collect()
    }

    /// Get the latest L2 orderbook snapshot for a feed.
    pub fn orderbook(&self, name: &str) -> Option<OrderbookSnapshot> {
        self.orderbooks.get(name).map(|entry| entry.value().clone())
    }

    /// Get all orderbook snapshots.
    pub fn all_orderbooks(&self) -> HashMap<String, OrderbookSnapshot> {
        self.orderbooks
            .iter()
            .map(|entry| (entry.key().clone(), entry.value().clone()))
            .collect()
    }

    /// Get metrics for a specific feed.
    pub fn feed_metrics(&self, name: &str) -> Option<FeedMetrics> {
        self.metrics.get(name).map(|entry| entry.value().clone())
    }

    /// Get all feed metrics.
    pub fn all_feed_metrics(&self) -> HashMap<String, FeedMetrics> {
        self.metrics
            .iter()
            .map(|entry| (entry.key().clone(), entry.value().clone()))
            .collect()
    }

    /// Stop a single feed by name.
    pub fn stop_feed(&self, name: &str) {
        if let Some(entry) = self.per_feed_shutdown.get(name) {
            entry.value().notify_waiters();
        }
        // Update metrics to show disconnected
        if let Some(mut m) = self.metrics.get_mut(name) {
            m.is_connected = false;
        }
    }

    /// Restart a feed. Stops the old task and starts a new one with the same or new config.
    pub fn restart_feed(&self, name: String, config: Option<FeedConfig>) {
        // Stop existing
        self.stop_feed(&name);

        // Determine config: use provided or look up stored
        let feed_config = config.or_else(|| {
            self.feed_configs
                .lock()
                .ok()
                .and_then(|configs| configs.get(&name).cloned())
        });

        if let Some(cfg) = feed_config {
            self.start_feed(name, cfg);
        } else {
            warn!(feed = %name, "cannot restart: no config available");
        }
    }

    /// Check if a feed task is still running.
    pub fn is_feed_running(&self, name: &str) -> bool {
        if let Ok(handles) = self.feed_handles.lock() {
            if let Some(handle) = handles.get(name) {
                return !handle.is_finished();
            }
        }
        false
    }

    /// Stop all feeds.
    pub fn stop_all(&self) {
        self.shutdown.notify_waiters();
        // Also signal all per-feed shutdowns
        for entry in self.per_feed_shutdown.iter() {
            entry.value().notify_waiters();
        }
    }
}

impl Drop for FeedManager {
    fn drop(&mut self) {
        self.stop_all();
    }
}

/// Generic REST feed — polls a URL at an interval.
async fn run_rest_feed(
    name: String,
    url: String,
    interval_secs: f64,
    snapshots: Arc<DashMap<String, FeedSnapshot>>,
    metrics: MetricsStore,
    global_shutdown: Arc<tokio::sync::Notify>,
    feed_shutdown: Arc<tokio::sync::Notify>,
) {
    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(10))
        .connect_timeout(std::time::Duration::from_secs(5))
        .build()
        .unwrap_or_else(|_| reqwest::Client::new());
    // Validate interval: must be finite and positive (from_secs_f64 panics on NaN/Inf/negative)
    let clamped_interval = if interval_secs.is_nan() || interval_secs.is_infinite() || interval_secs <= 0.0 {
        warn!(feed = %name, interval = interval_secs, "invalid REST feed interval, defaulting to 5s");
        5.0
    } else {
        interval_secs
    };
    let interval = tokio::time::Duration::from_secs_f64(clamped_interval);

    // Mark connected
    if let Some(mut m) = metrics.get_mut(&name) {
        m.is_connected = true;
    }

    loop {
        tokio::select! {
            _ = global_shutdown.notified() => break,
            _ = feed_shutdown.notified() => break,
            _ = tokio::time::sleep(interval) => {
                match client.get(&url).send().await {
                    Ok(resp) => {
                        if let Ok(body) = resp.text().await {
                            match serde_json::from_str::<serde_json::Value>(&body) {
                                Ok(json) => {
                                    if let Some(price) = json.get("price").and_then(|v| v.as_f64()) {
                                        let now = std::time::SystemTime::now()
                                            .duration_since(std::time::UNIX_EPOCH)
                                            .unwrap_or_default()
                                            .as_secs_f64();
                                        let snap = FeedSnapshot {
                                            price,
                                            timestamp: now,
                                            source: name.clone(),
                                            bid: json.get("bid").and_then(|v| v.as_f64()).unwrap_or(0.0),
                                            ask: json.get("ask").and_then(|v| v.as_f64()).unwrap_or(0.0),
                                            volume_24h: json.get("volume").and_then(|v| v.as_f64()).unwrap_or(0.0),
                                            last_trade_size: 0.0,
                                            last_trade_is_buy: false,
                                        };
                                        snapshots.insert(name.clone(), snap);
                                        if let Some(mut m) = metrics.get_mut(&name) {
                                            m.update_count += 1;
                                            m.last_update_time = now;
                                        }
                                    } else {
                                        debug!(feed = %name, "REST feed: JSON missing 'price' field");
                                    }
                                }
                                Err(e) => {
                                    warn!(feed = %name, error = %e, "REST feed: JSON parse failure");
                                    if let Some(mut m) = metrics.get_mut(&name) {
                                        m.error_count += 1;
                                        m.last_error = e.to_string();
                                    }
                                }
                            }
                        }
                    }
                    Err(e) => {
                        warn!(feed = %name, error = %e, "REST feed poll failed");
                        if let Some(mut m) = metrics.get_mut(&name) {
                            m.error_count += 1;
                            m.last_error = e.to_string();
                        }
                    }
                }
            }
        }
    }

    // Mark disconnected
    if let Some(mut m) = metrics.get_mut(&name) {
        m.is_connected = false;
    }
}

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

    #[test]
    fn test_feed_snapshot_default_fields() {
        let snap = FeedSnapshot::default();
        assert!((snap.price - 0.0).abs() < f64::EPSILON);
        assert!((snap.timestamp - 0.0).abs() < f64::EPSILON);
        assert!((snap.bid - 0.0).abs() < f64::EPSILON);
        assert!((snap.ask - 0.0).abs() < f64::EPSILON);
        assert!((snap.volume_24h - 0.0).abs() < f64::EPSILON);
        assert!((snap.last_trade_size - 0.0).abs() < f64::EPSILON);
        assert!(!snap.last_trade_is_buy);
        assert_eq!(snap.source, "");
    }

    #[test]
    fn test_feed_snapshot_new_fields() {
        let snap = FeedSnapshot {
            price: 42000.0,
            timestamp: 1700000000.0,
            source: "binance:btcusdt".to_string(),
            bid: 41999.0,
            ask: 42001.0,
            volume_24h: 12345.678,
            last_trade_size: 1.5,
            last_trade_is_buy: true,
        };
        assert!((snap.last_trade_size - 1.5).abs() < 0.01);
        assert!(snap.last_trade_is_buy);
        assert!((snap.volume_24h - 12345.678).abs() < 0.01);
    }

    #[test]
    fn test_feed_metrics_default() {
        let m = FeedMetrics::default();
        assert_eq!(m.update_count, 0);
        assert_eq!(m.error_count, 0);
        assert!((m.last_update_time - 0.0).abs() < f64::EPSILON);
        assert_eq!(m.last_error, "");
        assert!(!m.is_connected);
        assert_eq!(m.reconnect_count, 0);
    }

    #[test]
    fn test_feed_metrics_mutation() {
        let metrics: MetricsStore = Arc::new(DashMap::new());
        metrics.insert("test".to_string(), FeedMetrics::default());

        // Simulate update
        if let Some(mut m) = metrics.get_mut("test") {
            m.update_count += 1;
            m.last_update_time = 1700000000.0;
            m.is_connected = true;
        }

        let m = metrics.get("test").unwrap();
        assert_eq!(m.update_count, 1);
        assert!((m.last_update_time - 1700000000.0).abs() < 0.01);
        assert!(m.is_connected);
    }

    #[test]
    fn test_feed_metrics_error_tracking() {
        let metrics: MetricsStore = Arc::new(DashMap::new());
        metrics.insert("test".to_string(), FeedMetrics::default());

        // Simulate errors
        for i in 0..3 {
            if let Some(mut m) = metrics.get_mut("test") {
                m.error_count += 1;
                m.last_error = format!("error {}", i);
            }
        }

        let m = metrics.get("test").unwrap();
        assert_eq!(m.error_count, 3);
        assert_eq!(m.last_error, "error 2");
    }

    #[test]
    fn test_feed_metrics_reconnect_tracking() {
        let metrics: MetricsStore = Arc::new(DashMap::new());
        metrics.insert("test".to_string(), FeedMetrics::default());

        // Simulate reconnects
        if let Some(mut m) = metrics.get_mut("test") {
            m.reconnect_count += 1;
            m.is_connected = false;
        }
        // Reconnect
        if let Some(mut m) = metrics.get_mut("test") {
            m.reconnect_count += 1;
            m.is_connected = true;
        }

        let m = metrics.get("test").unwrap();
        assert_eq!(m.reconnect_count, 2);
        assert!(m.is_connected);
    }

    #[test]
    fn test_feed_manager_new() {
        let fm = FeedManager::new();
        assert!(fm.is_ok());
    }

    #[test]
    fn test_feed_manager_with_worker_threads() {
        let fm = FeedManager::with_worker_threads(1);
        assert!(fm.is_ok());
    }

    #[test]
    fn test_feed_manager_snapshot_none_before_start() {
        let fm = FeedManager::new().unwrap();
        assert!(fm.snapshot("btc").is_none());
    }

    #[test]
    fn test_feed_manager_all_snapshots_empty() {
        let fm = FeedManager::new().unwrap();
        assert!(fm.all_snapshots().is_empty());
    }

    #[test]
    fn test_feed_manager_metrics_none_before_start() {
        let fm = FeedManager::new().unwrap();
        assert!(fm.feed_metrics("btc").is_none());
    }

    #[test]
    fn test_feed_manager_all_metrics_empty() {
        let fm = FeedManager::new().unwrap();
        assert!(fm.all_feed_metrics().is_empty());
    }

    #[test]
    fn test_feed_manager_is_feed_running_false() {
        let fm = FeedManager::new().unwrap();
        assert!(!fm.is_feed_running("btc"));
    }

    #[test]
    fn test_feed_manager_stop_nonexistent_feed() {
        let fm = FeedManager::new().unwrap();
        // Should not panic
        fm.stop_feed("nonexistent");
    }

    #[test]
    fn test_feed_snapshot_repr() {
        let snap = FeedSnapshot {
            price: 42000.0,
            source: "binance:btcusdt".to_string(),
            bid: 41999.0,
            ask: 42001.0,
            volume_24h: 5000.0,
            ..Default::default()
        };
        let r = snap.__repr__();
        assert!(r.contains("4.200e4") || r.contains("42000") || r.contains("4200"));
        assert!(r.contains("binance"));
        assert!(r.contains("5000"));
    }

    #[test]
    fn test_feed_metrics_repr() {
        let m = FeedMetrics {
            update_count: 100,
            error_count: 2,
            is_connected: true,
            reconnect_count: 1,
            ..Default::default()
        };
        let r = m.__repr__();
        assert!(r.contains("100"));
        assert!(r.contains("2"));
        assert!(r.contains("true"));
        assert!(r.contains("1"));
    }
}
