//! Binance WebSocket price feed.
//!
//! Connects to combined stream:
//!   `wss://stream.binance.com:9443/stream?streams={sym}@trade/{sym}@bookTicker/{sym}@ticker`
//!
//! - `@trade`: real-time trade prices, size, aggressor side
//! - `@bookTicker`: real-time best bid/ask (BBO)
//! - `@ticker`: 24h rolling volume

use crate::feeds::{FeedSnapshot, MetricsStore};
use dashmap::DashMap;
use futures_util::StreamExt;
use serde::Deserialize;
use std::sync::Arc;
use tokio_tungstenite::connect_async;
use tracing::warn;

/// Typed Binance trade message.
#[derive(Deserialize)]
struct BinanceTrade {
    /// Price as string
    p: String,
    /// Quantity as string
    q: String,
    /// Trade time in milliseconds
    #[serde(rename = "T")]
    trade_time: Option<f64>,
    /// Is the buyer the market maker? (true = sell aggressor, false = buy aggressor)
    m: Option<bool>,
}

/// Binance bookTicker message (best bid/ask).
#[derive(Deserialize)]
struct BinanceBookTicker {
    /// Best bid price
    b: String,
    /// Best ask price
    a: String,
}

/// Binance 24hr ticker message (for volume).
#[derive(Deserialize)]
struct BinanceTicker {
    /// 24h base asset volume
    v: String,
}

/// Combined stream wrapper — Binance wraps each message in `{"stream": "...", "data": {...}}`.
#[derive(Deserialize)]
struct CombinedMessage {
    stream: String,
    data: serde_json::Value,
}

/// Run the Binance WebSocket feed until shutdown is signaled.
pub async fn run_binance_ws(
    name: String,
    symbol: String,
    snapshots: Arc<DashMap<String, FeedSnapshot>>,
    metrics: MetricsStore,
    global_shutdown: Arc<tokio::sync::Notify>,
    feed_shutdown: Arc<tokio::sync::Notify>,
) {
    let symbol_lower = symbol.to_lowercase();
    let url = format!(
        "wss://stream.binance.com:9443/stream?streams={}@trade/{}@bookTicker/{}@ticker",
        symbol_lower, symbol_lower, symbol_lower
    );
    // Pre-compute source string to avoid allocation per message
    let source = format!("binance:{}", symbol_lower);

    let trade_stream = format!("{}@trade", symbol_lower);
    let book_stream = format!("{}@bookticker", symbol_lower);
    let ticker_stream = format!("{}@ticker", symbol_lower);

    let mut reconnect_delay_ms: u64 = 1000;

    loop {
        let connect_result = tokio::select! {
            _ = global_shutdown.notified() => return,
            _ = feed_shutdown.notified() => return,
            result = connect_async(&url) => result,
        };

        match connect_result {
            Ok((ws_stream, _)) => {
                reconnect_delay_ms = 1000;
                let (_, mut read) = ws_stream.split();

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

                loop {
                    let msg = tokio::select! {
                        _ = global_shutdown.notified() => return,
                        _ = feed_shutdown.notified() => return,
                        msg = read.next() => msg,
                        _ = tokio::time::sleep(std::time::Duration::from_secs(30)) => {
                            tracing::warn!("binance feed '{}': idle timeout, reconnecting", name);
                            break;
                        }
                    };

                    match msg {
                        Some(Ok(tokio_tungstenite::tungstenite::Message::Text(text))) => {
                            // Try combined stream format first
                            if let Ok(combined) =
                                serde_json::from_str::<CombinedMessage>(&text)
                            {
                                let stream = combined.stream.to_lowercase();
                                if stream == trade_stream {
                                    if let Ok(trade) =
                                        serde_json::from_value::<BinanceTrade>(combined.data)
                                    {
                                        handle_trade(
                                            &name, &source, &trade, &snapshots, &metrics,
                                        );
                                    }
                                } else if stream == book_stream {
                                    if let Ok(bbo) =
                                        serde_json::from_value::<BinanceBookTicker>(combined.data)
                                    {
                                        handle_book_ticker(&name, &bbo, &snapshots, &metrics);
                                    }
                                } else if stream == ticker_stream {
                                    if let Ok(ticker) =
                                        serde_json::from_value::<BinanceTicker>(combined.data)
                                    {
                                        handle_ticker(&name, &ticker, &snapshots, &metrics);
                                    }
                                }
                            } else if let Ok(trade) =
                                serde_json::from_str::<BinanceTrade>(&text)
                            {
                                // Fallback: single-stream format (backward compat)
                                handle_trade(&name, &source, &trade, &snapshots, &metrics);
                            }
                        }
                        Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => break,
                        Some(Err(e)) => {
                            warn!(symbol = %symbol_lower, error = %e, "binance feed: connection error");
                            if let Some(mut m) = metrics.get_mut(&name) {
                                m.error_count += 1;
                                m.last_error = e.to_string();
                            }
                            break;
                        }
                        None => break,
                        _ => {}
                    }
                }

                // Mark disconnected
                if let Some(mut m) = metrics.get_mut(&name) {
                    m.is_connected = false;
                }
            }
            Err(e) => {
                warn!(symbol = %symbol_lower, error = %e, "binance feed: connection failed");
                if let Some(mut m) = metrics.get_mut(&name) {
                    m.error_count += 1;
                    m.last_error = e.to_string();
                }
            }
        }

        // Reconnect
        if let Some(mut m) = metrics.get_mut(&name) {
            m.reconnect_count += 1;
        }
        warn!(symbol = %symbol_lower, delay_ms = reconnect_delay_ms, "binance feed: reconnecting");
        tokio::select! {
            _ = global_shutdown.notified() => return,
            _ = feed_shutdown.notified() => return,
            _ = tokio::time::sleep(tokio::time::Duration::from_millis(reconnect_delay_ms)) => {},
        }
        reconnect_delay_ms = (reconnect_delay_ms * 2).min(30_000);
    }
}

/// Handle a trade message: update price, trade size, and aggressor direction.
fn handle_trade(
    name: &str,
    source: &str,
    trade: &BinanceTrade,
    snapshots: &Arc<DashMap<String, FeedSnapshot>>,
    metrics: &MetricsStore,
) {
    let price = match trade.p.parse::<f64>() {
        Ok(p) if p.is_finite() && p > 0.0 => p,
        _ => return,
    };
    let quantity = match trade.q.parse::<f64>() {
        Ok(q) if q.is_finite() => q,
        _ => 0.0,
    };
    let timestamp = trade.trade_time.unwrap_or(0.0) / 1000.0;
    // m=true means buyer is maker, so the aggressor is a seller
    let is_buy = !trade.m.unwrap_or(false);

    // Atomic read-modify-write using entry API to avoid get-clone-insert race
    let source_owned = source.to_string();
    let name_owned = name.to_string();
    snapshots
        .entry(name_owned)
        .and_modify(|snap| {
            snap.price = price;
            snap.timestamp = timestamp;
            snap.source = source_owned.clone();
            snap.last_trade_size = quantity;
            snap.last_trade_is_buy = is_buy;
            if snap.bid <= 0.0 {
                snap.bid = price;
            }
            if snap.ask <= 0.0 {
                snap.ask = price;
            }
        })
        .or_insert_with(|| {
            let mut snap = FeedSnapshot::default();
            snap.price = price;
            snap.timestamp = timestamp;
            snap.source = source_owned;
            snap.last_trade_size = quantity;
            snap.last_trade_is_buy = is_buy;
            snap.bid = price;
            snap.ask = price;
            snap
        });

    if let Some(mut m) = metrics.get_mut(name) {
        m.update_count += 1;
        m.last_update_time = timestamp;
    }
}

/// Handle a bookTicker message: update best bid/ask (BBO).
fn handle_book_ticker(
    name: &str,
    bbo: &BinanceBookTicker,
    snapshots: &Arc<DashMap<String, FeedSnapshot>>,
    metrics: &MetricsStore,
) {
    let bid = match bbo.b.parse::<f64>() {
        Ok(b) if b.is_finite() && b >= 0.0 => b,
        _ => return,
    };
    let ask = match bbo.a.parse::<f64>() {
        Ok(a) if a.is_finite() && a >= 0.0 => a,
        _ => return,
    };

    // Atomic read-modify-write using entry API
    let mid = (bid + ask) / 2.0;
    let name_owned = name.to_string();
    snapshots
        .entry(name_owned)
        .and_modify(|snap| {
            snap.bid = bid;
            snap.ask = ask;
            if snap.price <= 0.0 {
                snap.price = mid;
            }
        })
        .or_insert_with(|| {
            let mut snap = FeedSnapshot::default();
            snap.bid = bid;
            snap.ask = ask;
            snap.price = mid;
            snap
        });

    if let Some(mut m) = metrics.get_mut(name) {
        m.update_count += 1;
    }
}

/// Handle a 24hr ticker message: update volume_24h.
fn handle_ticker(
    name: &str,
    ticker: &BinanceTicker,
    snapshots: &Arc<DashMap<String, FeedSnapshot>>,
    metrics: &MetricsStore,
) {
    let volume = match ticker.v.parse::<f64>() {
        Ok(v) if v.is_finite() && v >= 0.0 => v,
        _ => return,
    };

    // Atomic read-modify-write using entry API
    let name_owned = name.to_string();
    snapshots
        .entry(name_owned)
        .and_modify(|snap| {
            snap.volume_24h = volume;
        })
        .or_insert_with(|| {
            let mut snap = FeedSnapshot::default();
            snap.volume_24h = volume;
            snap
        });

    if let Some(mut m) = metrics.get_mut(name) {
        m.update_count += 1;
    }
}

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

    #[test]
    fn test_binance_trade_deser() {
        let json = r#"{"p":"42000.50","q":"1.5","T":1700000000000,"m":false}"#;
        let trade: BinanceTrade = serde_json::from_str(json).unwrap();
        assert_eq!(trade.p, "42000.50");
        assert_eq!(trade.q, "1.5");
        assert_eq!(trade.trade_time, Some(1700000000000.0));
        assert_eq!(trade.m, Some(false));
    }

    #[test]
    fn test_binance_trade_deser_minimal() {
        let json = r#"{"p":"100.0","q":"0.01"}"#;
        let trade: BinanceTrade = serde_json::from_str(json).unwrap();
        assert_eq!(trade.p, "100.0");
        assert_eq!(trade.q, "0.01");
        assert_eq!(trade.trade_time, None);
        assert_eq!(trade.m, None);
    }

    #[test]
    fn test_binance_book_ticker_deser() {
        let json = r#"{"b":"42000.10","a":"42000.50"}"#;
        let bbo: BinanceBookTicker = serde_json::from_str(json).unwrap();
        assert_eq!(bbo.b, "42000.10");
        assert_eq!(bbo.a, "42000.50");
    }

    #[test]
    fn test_binance_ticker_deser() {
        let json = r#"{"v":"12345.678"}"#;
        let ticker: BinanceTicker = serde_json::from_str(json).unwrap();
        assert_eq!(ticker.v, "12345.678");
    }

    #[test]
    fn test_combined_message_deser() {
        let json = r#"{"stream":"btcusdt@trade","data":{"p":"42000.50","q":"1.5","T":1700000000000,"m":false}}"#;
        let msg: CombinedMessage = serde_json::from_str(json).unwrap();
        assert_eq!(msg.stream, "btcusdt@trade");
    }

    #[test]
    fn test_handle_trade_updates_snapshot() {
        let snapshots = Arc::new(DashMap::new());
        let metrics: MetricsStore = Arc::new(DashMap::new());
        let name = "btc";
        snapshots.insert(name.to_string(), FeedSnapshot::default());
        metrics.insert(name.to_string(), crate::feeds::FeedMetrics::default());

        let trade = BinanceTrade {
            p: "42000.50".to_string(),
            q: "1.5".to_string(),
            trade_time: Some(1700000000000.0),
            m: Some(false),
        };

        handle_trade(name, "binance:btcusdt", &trade, &snapshots, &metrics);

        let snap = snapshots.get(name).unwrap();
        assert!((snap.price - 42000.5).abs() < 0.01);
        assert!((snap.last_trade_size - 1.5).abs() < 0.01);
        assert!(snap.last_trade_is_buy);
        assert_eq!(snap.source, "binance:btcusdt");

        let m = metrics.get(name).unwrap();
        assert_eq!(m.update_count, 1);
    }

    #[test]
    fn test_handle_book_ticker_updates_bbo() {
        let snapshots = Arc::new(DashMap::new());
        let metrics: MetricsStore = Arc::new(DashMap::new());
        let name = "btc";
        snapshots.insert(name.to_string(), FeedSnapshot::default());
        metrics.insert(name.to_string(), crate::feeds::FeedMetrics::default());

        let bbo = BinanceBookTicker {
            b: "42000.10".to_string(),
            a: "42000.50".to_string(),
        };

        handle_book_ticker(name, &bbo, &snapshots, &metrics);

        let snap = snapshots.get(name).unwrap();
        assert!((snap.bid - 42000.10).abs() < 0.01);
        assert!((snap.ask - 42000.50).abs() < 0.01);
        // Price set from BBO since no trade yet
        assert!((snap.price - 42000.30).abs() < 0.01);
    }

    #[test]
    fn test_handle_ticker_updates_volume() {
        let snapshots = Arc::new(DashMap::new());
        let metrics: MetricsStore = Arc::new(DashMap::new());
        let name = "btc";
        snapshots.insert(name.to_string(), FeedSnapshot::default());
        metrics.insert(name.to_string(), crate::feeds::FeedMetrics::default());

        let ticker = BinanceTicker {
            v: "12345.678".to_string(),
        };

        handle_ticker(name, &ticker, &snapshots, &metrics);

        let snap = snapshots.get(name).unwrap();
        assert!((snap.volume_24h - 12345.678).abs() < 0.01);
    }

    #[test]
    fn test_trade_preserves_bbo_from_book_ticker() {
        let snapshots = Arc::new(DashMap::new());
        let metrics: MetricsStore = Arc::new(DashMap::new());
        let name = "btc";
        snapshots.insert(name.to_string(), FeedSnapshot::default());
        metrics.insert(name.to_string(), crate::feeds::FeedMetrics::default());

        // First: bookTicker sets BBO
        let bbo = BinanceBookTicker {
            b: "42000.10".to_string(),
            a: "42000.50".to_string(),
        };
        handle_book_ticker(name, &bbo, &snapshots, &metrics);

        // Then: trade sets price/size but preserves BBO
        let trade = BinanceTrade {
            p: "42000.30".to_string(),
            q: "2.0".to_string(),
            trade_time: Some(1700000000000.0),
            m: Some(true), // seller aggressor
        };
        handle_trade(name, "binance:btcusdt", &trade, &snapshots, &metrics);

        let snap = snapshots.get(name).unwrap();
        assert!((snap.price - 42000.30).abs() < 0.01);
        assert!((snap.bid - 42000.10).abs() < 0.01); // Preserved from bookTicker
        assert!((snap.ask - 42000.50).abs() < 0.01); // Preserved from bookTicker
        assert!(!snap.last_trade_is_buy); // m=true → sell aggressor
    }
}
