//! Robinhood Crypto REST polling feed.
//!
//! Polls Robinhood's crypto trading pairs quote endpoint for price data.
//! Uses REST polling since Robinhood does not provide a public WebSocket API for crypto.

use crate::feeds::{FeedSnapshot, MetricsStore};
use dashmap::DashMap;
use std::sync::Arc;
use tracing::{debug, info, warn};

const QUOTE_API_URL: &str = "https://trading.robinhood.com/api/v1";

/// Run the Robinhood REST polling feed until shutdown is signaled.
///
/// Polls quotes for each symbol at the configured interval.
/// Each symbol gets its own DashMap entry keyed by "{feed_name}:{SYMBOL}".
pub async fn run_robinhood_feed(
    name: String,
    symbols: Vec<String>,
    api_key: String,
    api_url: Option<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());

    let clamped = clamp_interval(interval_secs);
    let interval = tokio::time::Duration::from_secs_f64(clamped);
    let base_url = api_url.unwrap_or_else(|| QUOTE_API_URL.to_string());

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

    info!(
        symbols = ?symbols,
        interval_secs = clamped,
        "robinhood feed: starting REST poll"
    );

    let mut backoff_ms: u64 = 0;

    loop {
        for symbol in &symbols {
            let url = format!(
                "{}/crypto/trading/pairs/{}/quote/",
                base_url,
                symbol.to_uppercase()
            );

            let result = tokio::select! {
                _ = global_shutdown.notified() => {
                    mark_disconnected(&name, &metrics);
                    return;
                },
                _ = feed_shutdown.notified() => {
                    mark_disconnected(&name, &metrics);
                    return;
                },
                result = client
                    .get(&url)
                    .header("Authorization", format!("Bearer {}", api_key))
                    .send() => result,
            };

            match result {
                Ok(resp) => {
                    let status_code = resp.status().as_u16();

                    if status_code == 429 {
                        warn!(feed = %name, "robinhood feed: rate limited (429)");
                        backoff_ms = (backoff_ms.max(1000) * 2).min(30_000);
                        if let Some(mut m) = metrics.get_mut(&name) {
                            m.error_count += 1;
                            m.last_error = "rate limited (429)".to_string();
                        }
                        break; // break inner loop, sleep with backoff
                    }

                    if !resp.status().is_success() {
                        debug!(
                            feed = %name,
                            symbol = %symbol,
                            status = status_code,
                            "robinhood feed: non-200 response"
                        );
                        if let Some(mut m) = metrics.get_mut(&name) {
                            m.error_count += 1;
                            m.last_error = format!("HTTP {}", status_code);
                        }
                        continue;
                    }

                    // Reset backoff on success
                    backoff_ms = 0;

                    match resp.json::<serde_json::Value>().await {
                        Ok(json) => {
                            if let Some(snap) =
                                parse_robinhood_quote(&json, &name, symbol)
                            {
                                let key =
                                    format!("{}:{}", name, symbol.to_uppercase());
                                let source = snap.source.clone();
                                let price = snap.price;
                                let bid = snap.bid;
                                let ask = snap.ask;

                                snapshots.insert(key, snap.clone());

                                // Also update feed-level snapshot (keyed by feed
                                // name only) so single-symbol feeds work with
                                // ctx.feeds[name]
                                snapshots
                                    .entry(name.clone())
                                    .and_modify(|s| {
                                        s.price = price;
                                        s.bid = bid;
                                        s.ask = ask;
                                        s.source = source.clone();
                                        s.timestamp = snap.timestamp;
                                    })
                                    .or_insert_with(|| snap);

                                if let Some(mut m) = metrics.get_mut(&name) {
                                    m.update_count += 1;
                                    m.last_update_time = now_secs();
                                }
                            }
                        }
                        Err(e) => {
                            warn!(
                                feed = %name,
                                symbol = %symbol,
                                error = %e,
                                "robinhood feed: JSON parse failed"
                            );
                            if let Some(mut m) = metrics.get_mut(&name) {
                                m.error_count += 1;
                                m.last_error = e.to_string();
                            }
                        }
                    }
                }
                Err(e) => {
                    warn!(
                        feed = %name,
                        symbol = %symbol,
                        error = %e,
                        "robinhood feed: request failed"
                    );
                    if let Some(mut m) = metrics.get_mut(&name) {
                        m.error_count += 1;
                        m.last_error = e.to_string();
                    }
                }
            }
        }

        // Sleep interval (use backoff if rate-limited, otherwise normal interval)
        let sleep_dur = if backoff_ms > 0 {
            tokio::time::Duration::from_millis(backoff_ms)
        } else {
            interval
        };

        tokio::select! {
            _ = global_shutdown.notified() => {
                mark_disconnected(&name, &metrics);
                return;
            },
            _ = feed_shutdown.notified() => {
                mark_disconnected(&name, &metrics);
                return;
            },
            _ = tokio::time::sleep(sleep_dur) => {},
        }
    }
}

/// Parse a Robinhood crypto quote JSON response into a FeedSnapshot.
///
/// Expected fields: mark_price, last_trade_price, bid_price, ask_price, symbol, volume.
fn parse_robinhood_quote(
    json: &serde_json::Value,
    feed_name: &str,
    symbol: &str,
) -> Option<FeedSnapshot> {
    // Robinhood returns prices as strings
    let price = json
        .get("mark_price")
        .or_else(|| json.get("last_trade_price"))
        .and_then(|v| v.as_str().and_then(|s| s.parse::<f64>().ok()).or_else(|| v.as_f64()))?;

    // Validate NaN/Inf/non-positive price
    if !price.is_finite() || price <= 0.0 {
        return None;
    }

    let bid = json
        .get("bid_price")
        .and_then(|v| v.as_str().and_then(|s| s.parse::<f64>().ok()).or_else(|| v.as_f64()))
        .unwrap_or(0.0);

    let ask = json
        .get("ask_price")
        .and_then(|v| v.as_str().and_then(|s| s.parse::<f64>().ok()).or_else(|| v.as_f64()))
        .unwrap_or(0.0);

    let volume = json
        .get("volume")
        .and_then(|v| v.as_str().and_then(|s| s.parse::<f64>().ok()).or_else(|| v.as_f64()))
        .unwrap_or(0.0);

    // Validate bid/ask: reject NaN/Inf
    let bid = if bid.is_finite() && bid >= 0.0 { bid } else { 0.0 };
    let ask = if ask.is_finite() && ask >= 0.0 { ask } else { 0.0 };
    let volume = if volume.is_finite() && volume >= 0.0 {
        volume
    } else {
        0.0
    };

    Some(FeedSnapshot {
        price,
        timestamp: now_secs(),
        source: format!("robinhood:{}", symbol.to_uppercase()),
        bid,
        ask,
        volume_24h: volume,
        last_trade_size: 0.0,
        last_trade_is_buy: false,
    })
}

fn now_secs() -> f64 {
    std::time::SystemTime::now()
        .duration_since(std::time::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)
    }
}

fn mark_disconnected(name: &str, metrics: &MetricsStore) {
    if let Some(mut m) = metrics.get_mut(name) {
        m.is_connected = false;
    }
}

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

    #[test]
    fn test_parse_robinhood_quote_basic() {
        let json = serde_json::json!({
            "mark_price": "42000.50",
            "bid_price": "41999.00",
            "ask_price": "42001.00",
            "volume": "1234.56",
            "symbol": "BTC-USD"
        });

        let snap = parse_robinhood_quote(&json, "rh_feed", "BTC-USD").unwrap();
        assert!((snap.price - 42000.50).abs() < 0.01);
        assert!((snap.bid - 41999.0).abs() < 0.01);
        assert!((snap.ask - 42001.0).abs() < 0.01);
        assert!((snap.volume_24h - 1234.56).abs() < 0.01);
        assert_eq!(snap.source, "robinhood:BTC-USD");
    }

    #[test]
    fn test_parse_robinhood_quote_last_trade_price_fallback() {
        let json = serde_json::json!({
            "last_trade_price": "150.25",
            "bid_price": "150.10",
            "ask_price": "150.40"
        });

        let snap = parse_robinhood_quote(&json, "rh", "ETH-USD").unwrap();
        assert!((snap.price - 150.25).abs() < 0.01);
    }

    #[test]
    fn test_parse_robinhood_quote_missing_price() {
        let json = serde_json::json!({
            "bid_price": "100.00",
            "ask_price": "101.00"
        });

        assert!(parse_robinhood_quote(&json, "rh", "SOL-USD").is_none());
    }

    #[test]
    fn test_parse_robinhood_quote_nan_price() {
        let json = serde_json::json!({
            "mark_price": f64::NAN
        });

        assert!(parse_robinhood_quote(&json, "rh", "BTC-USD").is_none());
    }

    #[test]
    fn test_parse_robinhood_quote_inf_price() {
        let json = serde_json::json!({
            "mark_price": f64::INFINITY
        });

        assert!(parse_robinhood_quote(&json, "rh", "BTC-USD").is_none());
    }

    #[test]
    fn test_parse_robinhood_quote_zero_price() {
        let json = serde_json::json!({
            "mark_price": "0.0"
        });

        assert!(parse_robinhood_quote(&json, "rh", "BTC-USD").is_none());
    }

    #[test]
    fn test_parse_robinhood_quote_negative_price() {
        let json = serde_json::json!({
            "mark_price": "-1.0"
        });

        assert!(parse_robinhood_quote(&json, "rh", "BTC-USD").is_none());
    }

    #[test]
    fn test_parse_robinhood_quote_numeric_values() {
        // Some responses may return numeric rather than string values
        let json = serde_json::json!({
            "mark_price": 500.0,
            "bid_price": 499.5,
            "ask_price": 500.5,
            "volume": 9999.0
        });

        let snap = parse_robinhood_quote(&json, "rh", "BTC-USD").unwrap();
        assert!((snap.price - 500.0).abs() < 0.01);
        assert!((snap.bid - 499.5).abs() < 0.01);
        assert!((snap.ask - 500.5).abs() < 0.01);
        assert!((snap.volume_24h - 9999.0).abs() < 0.01);
    }

    #[test]
    fn test_parse_robinhood_quote_nan_bid_ask_sanitized() {
        let json = serde_json::json!({
            "mark_price": "100.0",
            "bid_price": f64::NAN,
            "ask_price": f64::INFINITY
        });

        let snap = parse_robinhood_quote(&json, "rh", "BTC-USD").unwrap();
        assert!((snap.price - 100.0).abs() < 0.01);
        assert!((snap.bid - 0.0).abs() < f64::EPSILON);
        assert!((snap.ask - 0.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_parse_robinhood_quote_no_optional_fields() {
        let json = serde_json::json!({
            "mark_price": "200.00"
        });

        let snap = parse_robinhood_quote(&json, "rh", "BTC-USD").unwrap();
        assert!((snap.price - 200.0).abs() < 0.01);
        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);
    }

    #[test]
    fn test_clamp_interval() {
        assert!((clamp_interval(10.0) - 10.0).abs() < 1e-6);
        assert!((clamp_interval(0.5) - 1.0).abs() < 1e-6);
        assert!((clamp_interval(f64::NAN) - 5.0).abs() < 1e-6);
        assert!((clamp_interval(f64::INFINITY) - 5.0).abs() < 1e-6);
        assert!((clamp_interval(-1.0) - 5.0).abs() < 1e-6);
        assert!((clamp_interval(0.0) - 5.0).abs() < 1e-6);
        assert!((clamp_interval(1.0) - 1.0).abs() < 1e-6);
        assert!((clamp_interval(60.0) - 60.0).abs() < 1e-6);
    }

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

        mark_disconnected("test", &metrics);
        let m = metrics.get("test").unwrap();
        assert!(!m.is_connected);
    }
}
