//! Alpaca Markets WebSocket price feed.
//!
//! Connects to Alpaca's real-time market data stream for stock quotes and trades.
//! Follows the Binance WebSocket feed pattern.
//!
//! URL: `wss://stream.data.alpaca.markets/v2/{source}`
//! Sources: "iex" (free), "sip" (paid)

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};

/// Alpaca trade message.
#[derive(Deserialize)]
struct AlpacaTrade {
    /// Message type ("t" for trade)
    #[serde(rename = "T")]
    msg_type: String,
    /// Symbol
    #[serde(rename = "S")]
    symbol: String,
    /// Price
    p: f64,
    /// Size
    s: f64,
    /// Timestamp (RFC3339)
    t: Option<String>,
}

/// Alpaca quote message.
#[derive(Deserialize)]
struct AlpacaQuote {
    /// Message type ("q" for quote)
    #[serde(rename = "T")]
    msg_type: String,
    /// Symbol
    #[serde(rename = "S")]
    symbol: String,
    /// Bid price
    bp: f64,
    /// Ask price
    ap: f64,
    /// Bid size
    #[allow(dead_code)]
    bs: Option<f64>,
    /// Ask size
    #[allow(dead_code)]
    #[serde(rename = "as")]
    ask_size: Option<f64>,
}

/// Generic Alpaca message for type dispatch.
#[derive(Deserialize)]
struct AlpacaMessage {
    #[serde(rename = "T")]
    msg_type: String,
}

/// Run the Alpaca WebSocket feed until shutdown is signaled.
///
/// Subscribes to trades and quotes for the given symbols.
/// Each symbol gets its own DashMap entry keyed by "{feed_name}:{symbol}".
pub async fn run_alpaca_feed(
    name: String,
    symbols: Vec<String>,
    api_key: String,
    api_secret: String,
    data_source: String,
    snapshots: Arc<DashMap<String, FeedSnapshot>>,
    metrics: MetricsStore,
    global_shutdown: Arc<tokio::sync::Notify>,
    feed_shutdown: Arc<tokio::sync::Notify>,
) {
    let source = if data_source.is_empty() {
        "iex".to_string()
    } else {
        data_source
    };

    // Validate data_source contains only lowercase alpha characters
    if !source.chars().all(|c| c.is_ascii_lowercase()) {
        warn!(feed = %name, data_source = %source, "Alpaca feed: invalid data_source, must match [a-z]+");
        return;
    }

    let url = format!("wss://stream.data.alpaca.markets/v2/{}", source);

    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 write, mut read) = ws_stream.split();

                // Authenticate
                let auth_msg = serde_json::json!({
                    "action": "auth",
                    "key": api_key,
                    "secret": api_secret,
                });
                if let Err(e) = write
                    .send(tokio_tungstenite::tungstenite::Message::Text(
                        auth_msg.to_string(),
                    ))
                    .await
                {
                    warn!(error = %e, "alpaca feed: auth send failed");
                    continue;
                }

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

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

                // Subscribe to trades and quotes
                let sub_msg = serde_json::json!({
                    "action": "subscribe",
                    "trades": symbols,
                    "quotes": symbols,
                });
                if let Err(e) = write
                    .send(tokio_tungstenite::tungstenite::Message::Text(
                        sub_msg.to_string(),
                    ))
                    .await
                {
                    warn!(error = %e, "alpaca feed: subscribe send failed");
                    continue;
                }

                info!(
                    symbols = ?symbols,
                    source = %source,
                    "alpaca 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))) => {
                            // Alpaca sends JSON arrays
                            if let Ok(messages) =
                                serde_json::from_str::<Vec<serde_json::Value>>(&text)
                            {
                                for msg_val in messages {
                                    let msg_type = msg_val
                                        .get("T")
                                        .and_then(|v| v.as_str())
                                        .unwrap_or("");

                                    match msg_type {
                                        "t" => {
                                            if let Ok(trade) =
                                                serde_json::from_value::<AlpacaTrade>(msg_val)
                                            {
                                                handle_trade(
                                                    &name,
                                                    &trade,
                                                    &snapshots,
                                                    &metrics,
                                                );
                                            }
                                        }
                                        "q" => {
                                            if let Ok(quote) =
                                                serde_json::from_value::<AlpacaQuote>(msg_val)
                                            {
                                                handle_quote(
                                                    &name,
                                                    &quote,
                                                    &snapshots,
                                                    &metrics,
                                                );
                                            }
                                        }
                                        _ => {} // subscription confirmations, heartbeats, etc.
                                    }
                                }
                            }
                        }
                        Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => break,
                        Some(Err(e)) => {
                            warn!(error = %e, "alpaca 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, "alpaca 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!(
            delay_ms = reconnect_delay_ms,
            "alpaca 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 and trade info for the symbol.
fn handle_trade(
    name: &str,
    trade: &AlpacaTrade,
    snapshots: &Arc<DashMap<String, FeedSnapshot>>,
    metrics: &MetricsStore,
) {
    let price = trade.p;
    let size = trade.s;
    // Reject NaN/Inf from deserialized floats
    if !price.is_finite() || price <= 0.0 || !size.is_finite() {
        return;
    }
    let key = format!("{}:{}", name, trade.symbol.to_uppercase());
    let source = format!("alpaca:{}", trade.symbol.to_uppercase());

    snapshots
        .entry(key.clone())
        .and_modify(|snap| {
            snap.price = price;
            snap.source = source.clone();
            snap.last_trade_size = size;
            snap.last_trade_is_buy = true; // Alpaca doesn't provide aggressor side
            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.source = source;
            snap.last_trade_size = size;
            snap.bid = price;
            snap.ask = price;
            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;
            snap.last_trade_size = size;
        })
        .or_insert_with(|| {
            let mut snap = FeedSnapshot::default();
            snap.price = price;
            snap.last_trade_size = size;
            snap
        });

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

/// Handle a quote message: update bid/ask for the symbol.
fn handle_quote(
    name: &str,
    quote: &AlpacaQuote,
    snapshots: &Arc<DashMap<String, FeedSnapshot>>,
    metrics: &MetricsStore,
) {
    let bid = quote.bp;
    let ask = quote.ap;
    // Reject NaN/Inf from deserialized floats
    if !bid.is_finite() || !ask.is_finite() || bid < 0.0 || ask < 0.0 {
        return;
    }
    let key = format!("{}:{}", name, quote.symbol.to_uppercase());
    let mid = (bid + ask) / 2.0;

    snapshots
        .entry(key.clone())
        .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
        });

    // Also update feed-level snapshot
    snapshots
        .entry(name.to_string())
        .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;
    }
}

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

    #[test]
    fn test_alpaca_trade_deser() {
        let json = r#"{"T":"t","S":"AAPL","p":150.25,"s":100,"t":"2024-01-15T12:00:00Z"}"#;
        let trade: AlpacaTrade = serde_json::from_str(json).unwrap();
        assert_eq!(trade.msg_type, "t");
        assert_eq!(trade.symbol, "AAPL");
        assert!((trade.p - 150.25).abs() < 0.01);
        assert!((trade.s - 100.0).abs() < 0.01);
    }

    #[test]
    fn test_alpaca_quote_deser() {
        let json = r#"{"T":"q","S":"AAPL","bp":150.10,"ap":150.30,"bs":200,"as":300}"#;
        let quote: AlpacaQuote = serde_json::from_str(json).unwrap();
        assert_eq!(quote.msg_type, "q");
        assert_eq!(quote.symbol, "AAPL");
        assert!((quote.bp - 150.10).abs() < 0.01);
        assert!((quote.ap - 150.30).abs() < 0.01);
    }

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

        let trade = AlpacaTrade {
            msg_type: "t".to_string(),
            symbol: "AAPL".to_string(),
            p: 150.50,
            s: 100.0,
            t: None,
        };

        handle_trade(name, &trade, &snapshots, &metrics);

        let key = format!("{}:AAPL", name);
        let snap = snapshots.get(&key).unwrap();
        assert!((snap.price - 150.50).abs() < 0.01);
        assert!((snap.last_trade_size - 100.0).abs() < 0.01);

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

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

        let quote = AlpacaQuote {
            msg_type: "q".to_string(),
            symbol: "SPY".to_string(),
            bp: 450.10,
            ap: 450.30,
            bs: Some(500.0),
            ask_size: Some(600.0),
        };

        handle_quote(name, &quote, &snapshots, &metrics);

        let key = format!("{}:SPY", name);
        let snap = snapshots.get(&key).unwrap();
        assert!((snap.bid - 450.10).abs() < 0.01);
        assert!((snap.ask - 450.30).abs() < 0.01);
        assert!((snap.price - 450.20).abs() < 0.01); // mid
    }

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

        // First: quote sets BBO
        let quote = AlpacaQuote {
            msg_type: "q".to_string(),
            symbol: "AAPL".to_string(),
            bp: 150.10,
            ap: 150.30,
            bs: None,
            ask_size: None,
        };
        handle_quote(name, &quote, &snapshots, &metrics);

        // Then: trade sets price but preserves BBO
        let trade = AlpacaTrade {
            msg_type: "t".to_string(),
            symbol: "AAPL".to_string(),
            p: 150.20,
            s: 50.0,
            t: None,
        };
        handle_trade(name, &trade, &snapshots, &metrics);

        let key = format!("{}:AAPL", name);
        let snap = snapshots.get(&key).unwrap();
        assert!((snap.price - 150.20).abs() < 0.01);
        assert!((snap.bid - 150.10).abs() < 0.01);
        assert!((snap.ask - 150.30).abs() < 0.01);
    }
}
