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

use super::{FeedSnapshot, MetricsStore};

/// ESPN scoreboard REST polling feed.
///
/// Polls `https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard`
/// and maps:
///   - home team score -> price
///   - away team score -> bid
///   - period/quarter -> volume_24h
///
/// If `event_id` is provided, filters to that specific event.
/// Otherwise, picks the first event with status "in" (active), or the first event.
pub async fn run_espn_feed(
    name: String,
    sport: String,
    league: String,
    event_id: Option<String>,
    interval_secs: f64,
    snapshots: Arc<DashMap<String, FeedSnapshot>>,
    metrics: MetricsStore,
    global_shutdown: Arc<tokio::sync::Notify>,
    feed_shutdown: Arc<tokio::sync::Notify>,
) {
    // Validate sport and league contain only safe URL path characters
    if !sport.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
        warn!(feed = %name, sport = %sport, "ESPN feed: invalid sport parameter, must be [a-zA-Z0-9-]");
        return;
    }
    if !league.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
        warn!(feed = %name, league = %league, "ESPN feed: invalid league parameter, must be [a-zA-Z0-9-]");
        return;
    }

    let url = format!(
        "https://site.api.espn.com/apis/site/v2/sports/{}/{}/scoreboard",
        sport, league
    );
    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, 5.0);
    let interval = tokio::time::Duration::from_secs_f64(clamped);

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

    loop {
        tokio::select! {
            _ = global_shutdown.notified() => break,
            _ = feed_shutdown.notified() => break,
            _ = tokio::time::sleep(interval) => {
                match client.get(&url).send().await {
                    Ok(resp) => {
                        if !resp.status().is_success() {
                            warn!(feed = %name, status = %resp.status(), "ESPN HTTP error");
                            if let Some(mut m) = metrics.get_mut(&name) {
                                m.error_count += 1;
                                m.last_error = format!("HTTP {}", resp.status());
                            }
                            continue;
                        }

                        match resp.text().await {
                            Ok(body) => {
                                if let Some(snap) = parse_espn_response(
                                    &body, &sport, &league, event_id.as_deref(),
                                ) {
                                    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, "ESPN 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, "ESPN 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_espn_response(
    body: &str,
    sport: &str,
    league: &str,
    event_id: Option<&str>,
) -> Option<FeedSnapshot> {
    let json: serde_json::Value = serde_json::from_str(body).ok()?;
    let events = json.get("events")?.as_array()?;

    if events.is_empty() {
        return None;
    }

    // Find the target event
    let event = if let Some(eid) = event_id {
        events
            .iter()
            .find(|e| e.get("id").and_then(|v| v.as_str()) == Some(eid))
    } else {
        // Prefer an active ("in") game, else first event
        events
            .iter()
            .find(|e| get_event_status(e) == "in")
            .or(events.first())
    }?;

    let eid = event
        .get("id")
        .and_then(|v| v.as_str())
        .unwrap_or("unknown");
    let status = get_event_status(event);

    // Navigate: competitions[0].competitors[]
    let competition = event.get("competitions")?.as_array()?.first()?;
    let competitors = competition.get("competitors")?.as_array()?;

    let mut home_score: f64 = 0.0;
    let mut away_score: f64 = 0.0;

    for competitor in competitors {
        let score_str = competitor
            .get("score")
            .and_then(|v| v.as_str())
            .unwrap_or("0");
        let mut score = score_str.parse::<f64>().unwrap_or(0.0);
        if !score.is_finite() {
            score = 0.0;
        }
        let home_away = competitor
            .get("homeAway")
            .and_then(|v| v.as_str())
            .unwrap_or("");

        if home_away == "home" {
            home_score = score;
        } else {
            away_score = score;
        }
    }

    // Get period/quarter info from status
    let period = event
        .get("status")
        .and_then(|s| s.get("period"))
        .and_then(|v| v.as_f64())
        .unwrap_or(0.0);

    let source = format!("espn:{}:{}:{}:{}", sport, league, eid, status);

    Some(FeedSnapshot {
        price: home_score,
        timestamp: now_secs(),
        source,
        bid: away_score,
        ask: 0.0,
        volume_24h: period,
        last_trade_size: 0.0,
        last_trade_is_buy: false,
    })
}

fn get_event_status(event: &serde_json::Value) -> String {
    event
        .get("status")
        .and_then(|s| s.get("type"))
        .and_then(|t| t.get("name"))
        .and_then(|v| v.as_str())
        .unwrap_or("unknown")
        .to_lowercase()
        .replace("status_", "")
}

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 {
        10.0
    } else {
        interval.max(min)
    }
}

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

    fn sample_scoreboard() -> &'static str {
        r#"{
            "events": [
                {
                    "id": "401585601",
                    "name": "Lakers vs Celtics",
                    "status": {
                        "period": 3,
                        "type": {"name": "STATUS_IN_PROGRESS"}
                    },
                    "competitions": [
                        {
                            "competitors": [
                                {"homeAway": "home", "score": "98"},
                                {"homeAway": "away", "score": "95"}
                            ]
                        }
                    ]
                },
                {
                    "id": "401585602",
                    "name": "Nets vs Knicks",
                    "status": {
                        "period": 0,
                        "type": {"name": "STATUS_SCHEDULED"}
                    },
                    "competitions": [
                        {
                            "competitors": [
                                {"homeAway": "home", "score": "0"},
                                {"homeAway": "away", "score": "0"}
                            ]
                        }
                    ]
                }
            ]
        }"#
    }

    #[test]
    fn test_parse_espn_active_game() {
        let snap =
            parse_espn_response(sample_scoreboard(), "basketball", "nba", None).unwrap();
        assert!((snap.price - 98.0).abs() < 1e-6); // home score
        assert!((snap.bid - 95.0).abs() < 1e-6); // away score
        assert!((snap.volume_24h - 3.0).abs() < 1e-6); // period
        assert!(snap.source.contains("espn:basketball:nba:401585601"));
        assert!(snap.source.contains("in_progress"));
    }

    #[test]
    fn test_parse_espn_specific_event() {
        let snap = parse_espn_response(
            sample_scoreboard(),
            "basketball",
            "nba",
            Some("401585602"),
        )
        .unwrap();
        assert!((snap.price - 0.0).abs() < 1e-6);
        assert!((snap.bid - 0.0).abs() < 1e-6);
        assert!(snap.source.contains("401585602"));
    }

    #[test]
    fn test_parse_espn_missing_event() {
        let result = parse_espn_response(
            sample_scoreboard(),
            "basketball",
            "nba",
            Some("nonexistent"),
        );
        assert!(result.is_none());
    }

    #[test]
    fn test_parse_espn_empty_events() {
        let json = r#"{"events": []}"#;
        let result = parse_espn_response(json, "basketball", "nba", None);
        assert!(result.is_none());
    }

    #[test]
    fn test_parse_espn_invalid_json() {
        let result = parse_espn_response("not json", "basketball", "nba", None);
        assert!(result.is_none());
    }

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