use dashmap::DashMap;
use std::sync::Arc;
use tracing::warn;

use super::{FeedSnapshot, MetricsStore};

/// PredictIt REST polling feed.
///
/// Polls `https://www.predictit.org/api/marketdata/markets/{market_id}` and maps:
///   - `lastTradePrice` -> price
///   - `bestBuyYesCost` -> bid
///   - `bestSellYesCost` -> ask
///
/// If `contract_id` is provided, looks for the specific contract in the `contracts` array.
/// Otherwise, uses the first contract.
pub async fn run_predictit_feed(
    name: String,
    market_id: u64,
    contract_id: Option<u64>,
    interval_secs: f64,
    snapshots: Arc<DashMap<String, FeedSnapshot>>,
    metrics: MetricsStore,
    global_shutdown: Arc<tokio::sync::Notify>,
    feed_shutdown: Arc<tokio::sync::Notify>,
) {
    let url = format!(
        "https://www.predictit.org/api/marketdata/markets/{}",
        market_id
    );
    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, 1.0);
    let interval = tokio::time::Duration::from_secs_f64(clamped);

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

    let mut backoff_secs: f64 = 0.0;

    loop {
        tokio::select! {
            _ = global_shutdown.notified() => break,
            _ = feed_shutdown.notified() => break,
            _ = tokio::time::sleep(interval + tokio::time::Duration::from_secs_f64(backoff_secs)) => {
                match client.get(&url).send().await {
                    Ok(resp) => {
                        let status = resp.status();
                        if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
                            backoff_secs = (backoff_secs * 2.0 + 1.0).min(60.0);
                            warn!(feed = %name, backoff = backoff_secs, "PredictIt 429, backing off");
                            if let Some(mut m) = metrics.get_mut(&name) {
                                m.error_count += 1;
                                m.last_error = "429 Too Many Requests".to_string();
                            }
                            continue;
                        }
                        backoff_secs = 0.0;

                        if !status.is_success() {
                            warn!(feed = %name, status = %status, "PredictIt HTTP error");
                            if let Some(mut m) = metrics.get_mut(&name) {
                                m.error_count += 1;
                                m.last_error = format!("HTTP {}", status);
                            }
                            continue;
                        }

                        match resp.text().await {
                            Ok(body) => {
                                if let Some(snap) = parse_predictit_response(&body, &name, market_id, contract_id) {
                                    snapshots.insert(name.clone(), snap);
                                    if let Some(mut m) = metrics.get_mut(&name) {
                                        m.update_count += 1;
                                        m.last_update_time = now_secs();
                                    }
                                } else {
                                    if let Some(mut m) = metrics.get_mut(&name) {
                                        m.error_count += 1;
                                        m.last_error = "parse failed".to_string();
                                    }
                                }
                            }
                            Err(e) => {
                                warn!(feed = %name, error = %e, "PredictIt body read 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, error = %e, "PredictIt request failed");
                        if let Some(mut m) = metrics.get_mut(&name) {
                            m.error_count += 1;
                            m.last_error = e.to_string();
                        }
                    }
                }
            }
        }
    }

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

fn parse_predictit_response(
    body: &str,
    _name: &str,
    market_id: u64,
    contract_id: Option<u64>,
) -> Option<FeedSnapshot> {
    let json: serde_json::Value = serde_json::from_str(body).ok()?;

    let contracts = json.get("contracts")?.as_array()?;
    if contracts.is_empty() {
        return None;
    }

    let contract = if let Some(cid) = contract_id {
        contracts
            .iter()
            .find(|c| c.get("id").and_then(|v| v.as_u64()) == Some(cid))?
    } else {
        &contracts[0]
    };

    let price = contract
        .get("lastTradePrice")
        .and_then(|v| v.as_f64())
        .unwrap_or(0.0);
    let bid = contract
        .get("bestBuyYesCost")
        .and_then(|v| v.as_f64())
        .unwrap_or(0.0);
    let ask = contract
        .get("bestSellYesCost")
        .and_then(|v| v.as_f64())
        .unwrap_or(0.0);

    if !price.is_finite() || price <= 0.0 {
        return None;
    }
    if !bid.is_finite() || bid < 0.0 {
        return None;
    }
    if !ask.is_finite() || ask < 0.0 {
        return None;
    }

    let cid_str = contract_id
        .map(|c| c.to_string())
        .unwrap_or_else(|| "0".to_string());
    let source = format!("predictit:{}/{}", market_id, cid_str);

    Some(FeedSnapshot {
        price,
        timestamp: now_secs(),
        source,
        bid,
        ask,
        volume_24h: 0.0,
        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, min: f64) -> f64 {
    if interval.is_nan() || interval.is_infinite() || interval <= 0.0 {
        5.0
    } else {
        interval.max(min)
    }
}

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

    #[test]
    fn test_parse_predictit_with_contract_id() {
        let json = r#"{
            "id": 7456,
            "name": "Test Market",
            "contracts": [
                {
                    "id": 28562,
                    "name": "Yes",
                    "lastTradePrice": 0.65,
                    "bestBuyYesCost": 0.64,
                    "bestSellYesCost": 0.66
                },
                {
                    "id": 28563,
                    "name": "No",
                    "lastTradePrice": 0.35,
                    "bestBuyYesCost": 0.34,
                    "bestSellYesCost": 0.36
                }
            ]
        }"#;

        let snap = parse_predictit_response(json, "test", 7456, Some(28562)).unwrap();
        assert!((snap.price - 0.65).abs() < 1e-6);
        assert!((snap.bid - 0.64).abs() < 1e-6);
        assert!((snap.ask - 0.66).abs() < 1e-6);
        assert_eq!(snap.source, "predictit:7456/28562");
    }

    #[test]
    fn test_parse_predictit_first_contract() {
        let json = r#"{
            "contracts": [
                {
                    "id": 100,
                    "lastTradePrice": 0.50,
                    "bestBuyYesCost": 0.49,
                    "bestSellYesCost": 0.51
                }
            ]
        }"#;

        let snap = parse_predictit_response(json, "test", 1, None).unwrap();
        assert!((snap.price - 0.50).abs() < 1e-6);
        assert_eq!(snap.source, "predictit:1/0");
    }

    #[test]
    fn test_parse_predictit_missing_contract() {
        let json = r#"{
            "contracts": [
                {"id": 100, "lastTradePrice": 0.50, "bestBuyYesCost": 0.49, "bestSellYesCost": 0.51}
            ]
        }"#;

        let result = parse_predictit_response(json, "test", 1, Some(999));
        assert!(result.is_none());
    }

    #[test]
    fn test_parse_predictit_empty_contracts() {
        let json = r#"{"contracts": []}"#;
        let result = parse_predictit_response(json, "test", 1, None);
        assert!(result.is_none());
    }

    #[test]
    fn test_parse_predictit_invalid_json() {
        let result = parse_predictit_response("not json", "test", 1, None);
        assert!(result.is_none());
    }

    #[test]
    fn test_parse_predictit_missing_fields() {
        let json = r#"{
            "contracts": [
                {"id": 100}
            ]
        }"#;

        // Missing price fields default to 0.0, which fails the price > 0.0 validation
        let result = parse_predictit_response(json, "test", 1, None);
        assert!(result.is_none());
    }

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