//! Coinbase Advanced Trade WebSocket price feed.
//!
//! Connects to Coinbase's Advanced Trade WebSocket for real-time ticker data.
//! Follows the Alpaca WebSocket feed pattern with reconnect logic.
//!
//! URL: `wss://advanced-trade-ws.coinbase.com`

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

const WS_URL: &str = "wss://advanced-trade-ws.coinbase.com";

/// Coinbase ticker event within a "ticker" channel message.
#[derive(Deserialize, Debug)]
struct CoinbaseTickerEvent {
    /// Product ID (e.g., "BTC-USD")
    product_id: Option<String>,
    /// Last trade price
    price: Option<String>,
    /// Best bid
    best_bid: Option<String>,
    /// Best ask
    best_ask: Option<String>,
    /// Volume in the last 24 hours
    volume_24_h: Option<String>,
}

/// Top-level Coinbase WebSocket message.
#[derive(Deserialize, Debug)]
struct CoinbaseMessage {
    /// Channel name (e.g., "ticker")
    channel: Option<String>,
    /// Events array
    events: Option<Vec<CoinbaseEvent>>,
}

/// Coinbase event within a message.
#[derive(Deserialize, Debug)]
struct CoinbaseEvent {
    /// Event type (e.g., "update", "snapshot")
    #[serde(rename = "type")]
    #[allow(dead_code)]
    event_type: Option<String>,
    /// Tickers array
    tickers: Option<Vec<CoinbaseTickerEvent>>,
}

/// Generate HMAC-SHA256 signature for Coinbase WebSocket auth.
fn sign_ws_auth(api_secret: &str, timestamp: &str, channel: &str, product_ids: &[String]) -> String {
    use hmac::{Hmac, Mac};
    use sha2::Sha256;
    type HmacSha256 = Hmac<Sha256>;

    let products = product_ids.join(",");
    let message = format!("{}{}{}", timestamp, channel, products);

    let mut mac = HmacSha256::new_from_slice(api_secret.as_bytes()).expect("HMAC key length");
    mac.update(message.as_bytes());
    hex::encode(mac.finalize().into_bytes())
}

/// Run the Coinbase WebSocket feed until shutdown is signaled.
///
/// Subscribes to the ticker channel for the given product IDs.
/// Each product gets its own DashMap entry keyed by "{feed_name}:{PRODUCT_ID}".
pub async fn run_coinbase_feed(
    name: String,
    product_ids: Vec<String>,
    api_key: String,
    api_secret: String,
    snapshots: Arc<DashMap<String, FeedSnapshot>>,
    metrics: MetricsStore,
    global_shutdown: Arc<tokio::sync::Notify>,
    feed_shutdown: Arc<tokio::sync::Notify>,
) {
    let mut reconnect_delay_ms: u64 = 1000;

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

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

                // Build authenticated subscribe message
                let timestamp = std::time::SystemTime::now()
                    .duration_since(std::time::UNIX_EPOCH)
                    .unwrap()
                    .as_secs()
                    .to_string();

                let signature = sign_ws_auth(&api_secret, &timestamp, "ticker", &product_ids);

                let sub_msg = serde_json::json!({
                    "type": "subscribe",
                    "product_ids": product_ids,
                    "channel": "ticker",
                    "api_key": api_key,
                    "timestamp": timestamp,
                    "signature": signature,
                });

                if let Err(e) = write
                    .send(tokio_tungstenite::tungstenite::Message::Text(
                        sub_msg.to_string(),
                    ))
                    .await
                {
                    warn!(error = %e, "coinbase feed: subscribe send failed");
                    continue;
                }

                // Wait for subscription response
                let sub_resp = tokio::select! {
                    _ = global_shutdown.notified() => return,
                    _ = feed_shutdown.notified() => return,
                    msg = read.next() => msg,
                };

                match sub_resp {
                    Some(Ok(tokio_tungstenite::tungstenite::Message::Text(text))) => {
                        if text.contains("\"error\"") || text.contains("\"unauthorized\"") {
                            warn!(
                                response = %text,
                                "coinbase feed: subscription/auth failed"
                            );
                            break; // Don't retry on auth failure
                        }
                        debug!("coinbase feed: subscription confirmed");
                    }
                    Some(Err(e)) => {
                        warn!(error = %e, "coinbase feed: subscription response error");
                        continue;
                    }
                    None => continue,
                    _ => {}
                }

                info!(
                    product_ids = ?product_ids,
                    "coinbase feed: connected and subscribed"
                );

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

                // Message loop
                loop {
                    let msg = tokio::select! {
                        _ = global_shutdown.notified() => return,
                        _ = feed_shutdown.notified() => return,
                        msg = read.next() => msg,
                    };

                    match msg {
                        Some(Ok(tokio_tungstenite::tungstenite::Message::Text(text))) => {
                            if let Ok(ws_msg) =
                                serde_json::from_str::<CoinbaseMessage>(&text)
                            {
                                let channel = ws_msg.channel.as_deref().unwrap_or("");
                                if channel == "ticker" {
                                    if let Some(events) = ws_msg.events {
                                        for event in events {
                                            if let Some(tickers) = event.tickers {
                                                for ticker in tickers {
                                                    handle_ticker(
                                                        &name,
                                                        &ticker,
                                                        &snapshots,
                                                        &metrics,
                                                    );
                                                }
                                            }
                                        }
                                    }
                                }
                                // Ignore other channels (subscriptions, heartbeats)
                            }
                        }
                        Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => {
                            // Mark disconnected immediately on Close
                            if let Some(mut m) = metrics.get_mut(&name) {
                                m.is_connected = false;
                            }
                            break;
                        }
                        Some(Err(e)) => {
                            warn!(error = %e, "coinbase 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!(error = %e, "coinbase feed: connection failed");
                if let Some(mut m) = metrics.get_mut(&name) {
                    m.error_count += 1;
                    m.last_error = e.to_string();
                }
            }
        }

        // Reconnect with exponential backoff
        if let Some(mut m) = metrics.get_mut(&name) {
            m.reconnect_count += 1;
        }
        warn!(
            delay_ms = reconnect_delay_ms,
            "coinbase 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 ticker event: update price, bid, ask for the product.
fn handle_ticker(
    name: &str,
    ticker: &CoinbaseTickerEvent,
    snapshots: &Arc<DashMap<String, FeedSnapshot>>,
    metrics: &MetricsStore,
) {
    let product_id = match &ticker.product_id {
        Some(id) => id.clone(),
        None => return,
    };

    let price = ticker
        .price
        .as_ref()
        .and_then(|s| s.parse::<f64>().ok())
        .unwrap_or(0.0);

    let bid = ticker
        .best_bid
        .as_ref()
        .and_then(|s| s.parse::<f64>().ok())
        .unwrap_or(0.0);

    let ask = ticker
        .best_ask
        .as_ref()
        .and_then(|s| s.parse::<f64>().ok())
        .unwrap_or(0.0);

    let volume = ticker
        .volume_24_h
        .as_ref()
        .and_then(|s| s.parse::<f64>().ok())
        .unwrap_or(0.0);

    // Reject NaN/Inf
    if !price.is_finite() || price <= 0.0 {
        return;
    }
    if !bid.is_finite() || !ask.is_finite() || !volume.is_finite() {
        return;
    }

    let key = format!("{}:{}", name, product_id.to_uppercase());
    let source = format!("coinbase:{}", product_id.to_uppercase());

    snapshots
        .entry(key.clone())
        .and_modify(|snap| {
            snap.price = price;
            snap.source = source.clone();
            if bid > 0.0 {
                snap.bid = bid;
            }
            if ask > 0.0 {
                snap.ask = ask;
            }
            if volume > 0.0 {
                snap.volume_24h = volume;
            }
        })
        .or_insert_with(|| {
            let mut snap = FeedSnapshot::default();
            snap.price = price;
            snap.source = source;
            snap.bid = if bid > 0.0 { bid } else { price };
            snap.ask = if ask > 0.0 { ask } else { price };
            snap.volume_24h = volume;
            snap
        });

    // Also update the feed-level snapshot (keyed by feed name only)
    // so that single-symbol feeds work with ctx.feeds[name]
    snapshots
        .entry(name.to_string())
        .and_modify(|snap| {
            snap.price = price;
            if bid > 0.0 {
                snap.bid = bid;
            }
            if ask > 0.0 {
                snap.ask = ask;
            }
            if volume > 0.0 {
                snap.volume_24h = volume;
            }
        })
        .or_insert_with(|| {
            let mut snap = FeedSnapshot::default();
            snap.price = price;
            snap.bid = if bid > 0.0 { bid } else { price };
            snap.ask = if ask > 0.0 { ask } else { price };
            snap.volume_24h = volume;
            snap
        });

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

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

    #[test]
    fn test_coinbase_ticker_event_deser() {
        let json = r#"{
            "product_id": "BTC-USD",
            "price": "100000.50",
            "best_bid": "100000.00",
            "best_ask": "100001.00",
            "volume_24_h": "12345.67"
        }"#;
        let ticker: CoinbaseTickerEvent = serde_json::from_str(json).unwrap();
        assert_eq!(ticker.product_id.as_deref(), Some("BTC-USD"));
        assert_eq!(ticker.price.as_deref(), Some("100000.50"));
        assert_eq!(ticker.best_bid.as_deref(), Some("100000.00"));
        assert_eq!(ticker.best_ask.as_deref(), Some("100001.00"));
        assert_eq!(ticker.volume_24_h.as_deref(), Some("12345.67"));
    }

    #[test]
    fn test_coinbase_message_deser() {
        let json = r#"{
            "channel": "ticker",
            "events": [{
                "type": "update",
                "tickers": [{
                    "product_id": "ETH-USD",
                    "price": "3500.00",
                    "best_bid": "3499.50",
                    "best_ask": "3500.50",
                    "volume_24_h": "9876.54"
                }]
            }]
        }"#;
        let msg: CoinbaseMessage = serde_json::from_str(json).unwrap();
        assert_eq!(msg.channel.as_deref(), Some("ticker"));
        let events = msg.events.unwrap();
        assert_eq!(events.len(), 1);
        let tickers = events[0].tickers.as_ref().unwrap();
        assert_eq!(tickers.len(), 1);
        assert_eq!(tickers[0].product_id.as_deref(), Some("ETH-USD"));
    }

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

        let ticker = CoinbaseTickerEvent {
            product_id: Some("BTC-USD".to_string()),
            price: Some("100000.50".to_string()),
            best_bid: Some("100000.00".to_string()),
            best_ask: Some("100001.00".to_string()),
            volume_24_h: Some("5000.0".to_string()),
        };

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

        let key = format!("{}:BTC-USD", name);
        let snap = snapshots.get(&key).unwrap();
        assert!((snap.price - 100000.50).abs() < 0.01);
        assert!((snap.bid - 100000.00).abs() < 0.01);
        assert!((snap.ask - 100001.00).abs() < 0.01);
        assert!((snap.volume_24h - 5000.0).abs() < 0.01);

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

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

        let ticker = CoinbaseTickerEvent {
            product_id: Some("BTC-USD".to_string()),
            price: Some("50000.00".to_string()),
            best_bid: Some("49999.00".to_string()),
            best_ask: Some("50001.00".to_string()),
            volume_24_h: None,
        };

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

        // Feed-level snapshot should also be updated
        let snap = snapshots.get(name).unwrap();
        assert!((snap.price - 50000.00).abs() < 0.01);
        assert!((snap.bid - 49999.00).abs() < 0.01);
        assert!((snap.ask - 50001.00).abs() < 0.01);
    }

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

        // Invalid price string -> parse fails -> defaults to 0.0 -> rejected
        let ticker = CoinbaseTickerEvent {
            product_id: Some("BTC-USD".to_string()),
            price: Some("not_a_number".to_string()),
            best_bid: None,
            best_ask: None,
            volume_24_h: None,
        };

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

        // No snapshot should be created
        let key = format!("{}:BTC-USD", name);
        assert!(snapshots.get(&key).is_none());
    }

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

        let ticker = CoinbaseTickerEvent {
            product_id: Some("BTC-USD".to_string()),
            price: Some("0.0".to_string()),
            best_bid: None,
            best_ask: None,
            volume_24_h: None,
        };

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

        let key = format!("{}:BTC-USD", name);
        assert!(snapshots.get(&key).is_none());
    }

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

        let ticker = CoinbaseTickerEvent {
            product_id: None,
            price: Some("50000.00".to_string()),
            best_bid: None,
            best_ask: None,
            volume_24_h: None,
        };

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

        // Should not create any snapshot
        assert!(snapshots.is_empty());
    }

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

        // First ticker sets everything
        let ticker1 = CoinbaseTickerEvent {
            product_id: Some("ETH-USD".to_string()),
            price: Some("3500.00".to_string()),
            best_bid: Some("3499.00".to_string()),
            best_ask: Some("3501.00".to_string()),
            volume_24_h: Some("10000.0".to_string()),
        };
        handle_ticker(name, &ticker1, &snapshots, &metrics);

        // Second ticker updates price, bid/ask remain 0 -> should preserve old values
        let ticker2 = CoinbaseTickerEvent {
            product_id: Some("ETH-USD".to_string()),
            price: Some("3505.00".to_string()),
            best_bid: Some("0".to_string()),  // zero -> won't overwrite
            best_ask: Some("0".to_string()),  // zero -> won't overwrite
            volume_24_h: None,
        };
        handle_ticker(name, &ticker2, &snapshots, &metrics);

        let key = format!("{}:ETH-USD", name);
        let snap = snapshots.get(&key).unwrap();
        assert!((snap.price - 3505.00).abs() < 0.01);
        assert!((snap.bid - 3499.00).abs() < 0.01); // preserved
        assert!((snap.ask - 3501.00).abs() < 0.01); // preserved
    }

    #[test]
    fn test_sign_ws_auth() {
        // Just verify it produces a non-empty hex string of correct length (SHA256 = 64 hex chars)
        let sig = sign_ws_auth("test_secret", "1234567890", "ticker", &["BTC-USD".to_string()]);
        assert_eq!(sig.len(), 64);
        assert!(sig.chars().all(|c| c.is_ascii_hexdigit()));
    }

    #[test]
    fn test_sign_ws_auth_deterministic() {
        let sig1 = sign_ws_auth("secret", "12345", "ticker", &["BTC-USD".to_string(), "ETH-USD".to_string()]);
        let sig2 = sign_ws_auth("secret", "12345", "ticker", &["BTC-USD".to_string(), "ETH-USD".to_string()]);
        assert_eq!(sig1, sig2);
    }

    #[test]
    fn test_sign_ws_auth_different_secrets() {
        let sig1 = sign_ws_auth("secret1", "12345", "ticker", &["BTC-USD".to_string()]);
        let sig2 = sign_ws_auth("secret2", "12345", "ticker", &["BTC-USD".to_string()]);
        assert_ne!(sig1, sig2);
    }
}
