//! Interactive Brokers (IBKR) market data feed.
//!
//! REST polling feed using the Client Portal API snapshot endpoint.
//! Polls `/iserver/marketdata/snapshot` for last price, bid, ask, and sizes.
//!
//! Fields:
//!   31 = last price, 84 = bid, 85 = ask, 86 = bid size, 88 = ask size

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

/// Run the IBKR market data feed until shutdown is signaled.
///
/// Polls the IBKR snapshot endpoint at the configured interval.
/// Each conid gets its own DashMap entry keyed by "{feed_name}:{conid}".
/// The feed-level key "{feed_name}" is updated with the last received data.
pub async fn run_ibkr_feed(
    name: String,
    conids: Vec<String>,
    access_token: String,
    api_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 each conid is purely numeric
    for conid in &conids {
        if conid.is_empty() || !conid.chars().all(|c| c.is_ascii_digit()) {
            warn!(feed = %name, conid = %conid, "IBKR feed: invalid conid, must be numeric digits only");
            return;
        }
    }

    let clamped = clamp_interval(interval_secs);
    let interval = tokio::time::Duration::from_secs_f64(clamped);
    let conids_str = conids.join(",");

    info!(
        feed = %name,
        conids = %conids_str,
        interval_secs = clamped,
        "ibkr feed: starting"
    );

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

    loop {
        let url = format!(
            "{}/iserver/marketdata/snapshot?conids={}&fields=31,84,85,86,88",
            api_url, conids_str
        );

        let result = tokio::select! {
            _ = global_shutdown.notified() => {
                debug!(feed = %name, "ibkr feed: global shutdown");
                break;
            }
            _ = feed_shutdown.notified() => {
                debug!(feed = %name, "ibkr feed: feed shutdown");
                break;
            }
            result = client.get(&url)
                .header("Authorization", format!("Bearer {}", access_token))
                .send() => result,
        };

        match result {
            Ok(resp) if resp.status().is_success() => {
                match resp.json::<serde_json::Value>().await {
                    Ok(json) => {
                        if let Some(arr) = json.as_array() {
                            let now = now_secs();
                            for item in arr {
                                let conid = item
                                    .get("conid")
                                    .and_then(|v| {
                                        v.as_u64()
                                            .map(|n| n.to_string())
                                            .or_else(|| v.as_str().map(|s| s.to_string()))
                                    })
                                    .unwrap_or_default();

                                if conid.is_empty() {
                                    continue;
                                }

                                // Field 31 = last price
                                // IBKR may prefix with 'C' (close) or 'H' (halted)
                                let last_price = extract_price_field(item, "31");
                                let bid = extract_price_field(item, "84");
                                let ask = extract_price_field(item, "85");

                                // Validate last price
                                if !last_price.is_finite() || last_price <= 0.0 {
                                    debug!(
                                        feed = %name,
                                        conid = %conid,
                                        "ibkr feed: invalid last price, skipping"
                                    );
                                    continue;
                                }

                                // Validate bid/ask (allow 0.0 as "not available")
                                let valid_bid = if bid.is_finite() && bid >= 0.0 { bid } else { 0.0 };
                                let valid_ask = if ask.is_finite() && ask >= 0.0 { ask } else { 0.0 };

                                let source = format!("ibkr:{}", conid);
                                let key = format!("{}:{}", name, conid);

                                // Update per-conid snapshot
                                snapshots
                                    .entry(key.clone())
                                    .and_modify(|snap| {
                                        snap.price = last_price;
                                        snap.timestamp = now;
                                        snap.source = source.clone();
                                        if valid_bid > 0.0 {
                                            snap.bid = valid_bid;
                                        }
                                        if valid_ask > 0.0 {
                                            snap.ask = valid_ask;
                                        }
                                    })
                                    .or_insert_with(|| FeedSnapshot {
                                        price: last_price,
                                        timestamp: now,
                                        source,
                                        bid: valid_bid,
                                        ask: valid_ask,
                                        volume_24h: 0.0,
                                        last_trade_size: 0.0,
                                        last_trade_is_buy: false,
                                    });

                                // Also update feed-level snapshot
                                snapshots
                                    .entry(name.clone())
                                    .and_modify(|snap| {
                                        snap.price = last_price;
                                        snap.timestamp = now;
                                        if valid_bid > 0.0 {
                                            snap.bid = valid_bid;
                                        }
                                        if valid_ask > 0.0 {
                                            snap.ask = valid_ask;
                                        }
                                    })
                                    .or_insert_with(|| FeedSnapshot {
                                        price: last_price,
                                        timestamp: now,
                                        source: name.clone(),
                                        bid: valid_bid,
                                        ask: valid_ask,
                                        volume_24h: 0.0,
                                        last_trade_size: 0.0,
                                        last_trade_is_buy: false,
                                    });
                            }

                            if let Some(mut m) = metrics.get_mut(&name) {
                                m.update_count += 1;
                                m.last_update_time = now;
                            }
                        }
                    }
                    Err(e) => {
                        warn!(feed = %name, error = %e, "ibkr feed: JSON parse error");
                        if let Some(mut m) = metrics.get_mut(&name) {
                            m.error_count += 1;
                            m.last_error = e.to_string();
                        }
                    }
                }
            }
            Ok(resp) => {
                let status = resp.status();
                warn!(feed = %name, status = %status, "ibkr feed: non-success response");
                if let Some(mut m) = metrics.get_mut(&name) {
                    m.error_count += 1;
                    m.last_error = format!("HTTP {}", status);
                }
            }
            Err(e) => {
                warn!(feed = %name, error = %e, "ibkr feed: request failed");
                if let Some(mut m) = metrics.get_mut(&name) {
                    m.error_count += 1;
                    m.last_error = e.to_string();
                }
            }
        }

        // Wait for next poll interval
        tokio::select! {
            _ = global_shutdown.notified() => break,
            _ = feed_shutdown.notified() => break,
            _ = tokio::time::sleep(interval) => {},
        }
    }

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

/// Extract a price field from an IBKR snapshot item.
///
/// IBKR may return field values as strings with prefixes:
///   - "C100.50" (close prefix)
///   - "H100.50" (halted prefix)
///   - or plain numeric values
fn extract_price_field(item: &serde_json::Value, field: &str) -> f64 {
    item.get(field)
        .and_then(|v| {
            // Try as number first
            v.as_f64().or_else(|| {
                // Try as string, stripping common IBKR prefixes
                v.as_str().and_then(|s| {
                    let stripped = s
                        .trim_start_matches('C')
                        .trim_start_matches('H')
                        .trim_start_matches('c')
                        .trim_start_matches('h');
                    stripped.parse::<f64>().ok()
                })
            })
        })
        .and_then(|val| if val.is_finite() { Some(val) } else { None })
        .unwrap_or(0.0)
}

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

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

    #[test]
    fn test_extract_price_field_number() {
        let item = json!({"31": 150.25});
        assert!((extract_price_field(&item, "31") - 150.25).abs() < 0.01);
    }

    #[test]
    fn test_extract_price_field_string() {
        let item = json!({"31": "150.25"});
        assert!((extract_price_field(&item, "31") - 150.25).abs() < 0.01);
    }

    #[test]
    fn test_extract_price_field_c_prefix() {
        let item = json!({"31": "C150.25"});
        assert!((extract_price_field(&item, "31") - 150.25).abs() < 0.01);
    }

    #[test]
    fn test_extract_price_field_h_prefix() {
        let item = json!({"31": "H99.50"});
        assert!((extract_price_field(&item, "31") - 99.50).abs() < 0.01);
    }

    #[test]
    fn test_extract_price_field_missing() {
        let item = json!({"84": 100.0});
        assert!((extract_price_field(&item, "31") - 0.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_extract_price_field_nan() {
        let item = json!({"31": "NaN"});
        assert!((extract_price_field(&item, "31") - 0.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_extract_price_field_inf() {
        let item = json!({"31": "Infinity"});
        assert!((extract_price_field(&item, "31") - 0.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_extract_price_field_non_numeric_string() {
        let item = json!({"31": "N/A"});
        assert!((extract_price_field(&item, "31") - 0.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_clamp_interval_valid() {
        assert!((clamp_interval(10.0) - 10.0).abs() < 1e-6);
    }

    #[test]
    fn test_clamp_interval_small() {
        assert!((clamp_interval(0.5) - 1.0).abs() < 1e-6);
    }

    #[test]
    fn test_clamp_interval_nan() {
        assert!((clamp_interval(f64::NAN) - 5.0).abs() < 1e-6);
    }

    #[test]
    fn test_clamp_interval_negative() {
        assert!((clamp_interval(-1.0) - 5.0).abs() < 1e-6);
    }

    #[test]
    fn test_clamp_interval_infinity() {
        assert!((clamp_interval(f64::INFINITY) - 5.0).abs() < 1e-6);
    }

    #[test]
    fn test_snapshot_update_logic() {
        // Simulate what run_ibkr_feed does with a JSON response
        let snapshots: Arc<DashMap<String, FeedSnapshot>> = Arc::new(DashMap::new());
        let metrics: MetricsStore = Arc::new(DashMap::new());
        let name = "ibkr_test";
        metrics.insert(name.to_string(), FeedMetrics::default());

        let response = json!([
            {
                "conid": 265598,
                "31": "150.25",
                "84": "150.10",
                "85": "150.40",
                "86": "100",
                "88": "200"
            }
        ]);

        // Process the snapshot data
        if let Some(arr) = response.as_array() {
            let now = now_secs();
            for item in arr {
                let conid = item
                    .get("conid")
                    .and_then(|v| v.as_u64().map(|n| n.to_string()))
                    .unwrap_or_default();
                let last_price = extract_price_field(item, "31");
                let bid = extract_price_field(item, "84");
                let ask = extract_price_field(item, "85");

                let key = format!("{}:{}", name, conid);
                snapshots.insert(
                    key.clone(),
                    FeedSnapshot {
                        price: last_price,
                        timestamp: now,
                        source: format!("ibkr:{}", conid),
                        bid,
                        ask,
                        volume_24h: 0.0,
                        last_trade_size: 0.0,
                        last_trade_is_buy: false,
                    },
                );

                snapshots.insert(
                    name.to_string(),
                    FeedSnapshot {
                        price: last_price,
                        timestamp: now,
                        source: name.to_string(),
                        bid,
                        ask,
                        volume_24h: 0.0,
                        last_trade_size: 0.0,
                        last_trade_is_buy: false,
                    },
                );
            }
        }

        // Verify per-conid snapshot
        let key = format!("{}:265598", name);
        let snap = snapshots.get(&key).unwrap();
        assert!((snap.price - 150.25).abs() < 0.01);
        assert!((snap.bid - 150.10).abs() < 0.01);
        assert!((snap.ask - 150.40).abs() < 0.01);
        assert!(snap.source.contains("265598"));

        // Verify feed-level snapshot
        let feed_snap = snapshots.get(name).unwrap();
        assert!((feed_snap.price - 150.25).abs() < 0.01);
    }

    #[test]
    fn test_snapshot_update_preserves_existing_bid_ask() {
        let snapshots: Arc<DashMap<String, FeedSnapshot>> = Arc::new(DashMap::new());
        let name = "ibkr_test";
        let key = format!("{}:12345", name);

        // Insert initial snapshot with bid/ask
        snapshots.insert(
            key.clone(),
            FeedSnapshot {
                price: 100.0,
                timestamp: 0.0,
                source: "ibkr:12345".to_string(),
                bid: 99.5,
                ask: 100.5,
                volume_24h: 0.0,
                last_trade_size: 0.0,
                last_trade_is_buy: false,
            },
        );

        // Update with new price but 0 bid/ask (not available)
        let valid_bid = 0.0;
        let valid_ask = 0.0;
        snapshots.entry(key.clone()).and_modify(|snap| {
            snap.price = 101.0;
            if valid_bid > 0.0 {
                snap.bid = valid_bid;
            }
            if valid_ask > 0.0 {
                snap.ask = valid_ask;
            }
        });

        let snap = snapshots.get(&key).unwrap();
        assert!((snap.price - 101.0).abs() < 0.01);
        // Bid/ask should be preserved since new values were 0
        assert!((snap.bid - 99.5).abs() < 0.01);
        assert!((snap.ask - 100.5).abs() < 0.01);
    }

    #[test]
    fn test_multiple_conids() {
        let snapshots: Arc<DashMap<String, FeedSnapshot>> = Arc::new(DashMap::new());
        let name = "ibkr_multi";

        let response = json!([
            {"conid": 265598, "31": "150.25", "84": "150.10", "85": "150.40"},
            {"conid": 8314, "31": "450.00", "84": "449.80", "85": "450.20"}
        ]);

        if let Some(arr) = response.as_array() {
            for item in arr {
                let conid = item
                    .get("conid")
                    .and_then(|v| v.as_u64().map(|n| n.to_string()))
                    .unwrap_or_default();
                let last_price = extract_price_field(item, "31");
                let bid = extract_price_field(item, "84");
                let ask = extract_price_field(item, "85");

                let key = format!("{}:{}", name, conid);
                snapshots.insert(
                    key,
                    FeedSnapshot {
                        price: last_price,
                        timestamp: 0.0,
                        source: format!("ibkr:{}", conid),
                        bid,
                        ask,
                        volume_24h: 0.0,
                        last_trade_size: 0.0,
                        last_trade_is_buy: false,
                    },
                );
            }
        }

        let snap1 = snapshots.get(&format!("{}:265598", name)).unwrap();
        assert!((snap1.price - 150.25).abs() < 0.01);

        let snap2 = snapshots.get(&format!("{}:8314", name)).unwrap();
        assert!((snap2.price - 450.00).abs() < 0.01);
    }
}
