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

use super::{FeedSnapshot, MetricsStore};

/// National Weather Service REST polling feed.
///
/// Two modes:
///   - `"forecast"`: Polls `https://api.weather.gov/gridpoints/{office}/{x},{y}/forecast`
///     Maps: temperature -> price, wind_speed -> bid, precip_chance -> ask
///   - `"alerts"`: Polls `https://api.weather.gov/alerts/active?area={state}`
///     Maps: 1.0 if active alerts else 0.0 -> price; severity encoded in source
///
/// NWS API requires a User-Agent header per their terms of service.
pub async fn run_nws_feed(
    name: String,
    mode: String,
    office: String,
    grid_x: u32,
    grid_y: u32,
    state: String,
    user_agent: String,
    interval_secs: f64,
    snapshots: Arc<DashMap<String, FeedSnapshot>>,
    metrics: MetricsStore,
    global_shutdown: Arc<tokio::sync::Notify>,
    feed_shutdown: Arc<tokio::sync::Notify>,
) {
    let url = match mode.as_str() {
        "alerts" => {
            // Validate state: must be exactly 2 uppercase ASCII letters
            if state.len() != 2 || !state.chars().all(|c| c.is_ascii_uppercase()) {
                warn!(feed = %name, state = %state, "NWS feed: invalid state parameter, must be 2 uppercase letters");
                return;
            }
            format!("https://api.weather.gov/alerts/active?area={}", state)
        }
        _ => {
            // Validate office: must be exactly 3 uppercase ASCII letters
            if office.len() != 3 || !office.chars().all(|c| c.is_ascii_uppercase()) {
                warn!(feed = %name, office = %office, "NWS feed: invalid office parameter, must be 3 uppercase letters");
                return;
            }
            format!(
                "https://api.weather.gov/gridpoints/{}/{},{}/forecast",
                office, grid_x, grid_y
            )
        }
    };

    let ua = if user_agent.is_empty() {
        "Horizon-SDK/0.5.0".to_string()
    } else {
        user_agent
    };

    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(15))
        .connect_timeout(std::time::Duration::from_secs(5))
        .user_agent(&ua)
        .build()
        .unwrap_or_else(|_| reqwest::Client::new());

    // NWS asks for no more than 1 request per second; enforce 60s minimum
    let clamped = clamp_interval(interval_secs, 60.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(), "NWS 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) => {
                                let snap = match mode.as_str() {
                                    "alerts" => parse_nws_alerts(&body, &state),
                                    _ => parse_nws_forecast(&body, &office, grid_x, grid_y),
                                };

                                if let Some(snap) = snap {
                                    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, "NWS 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, "NWS 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_nws_forecast(body: &str, office: &str, x: u32, y: u32) -> Option<FeedSnapshot> {
    let json: serde_json::Value = serde_json::from_str(body).ok()?;

    let periods = json
        .get("properties")?
        .get("periods")?
        .as_array()?;

    let period = periods.first()?;

    let temperature = period
        .get("temperature")
        .and_then(|v| v.as_f64())
        .unwrap_or(0.0);

    // Wind speed: "10 mph" or "5 to 10 mph" -> extract first number
    let wind_speed = period
        .get("windSpeed")
        .and_then(|v| v.as_str())
        .map(|s| extract_first_number(s))
        .unwrap_or(0.0);

    let precip_chance = period
        .get("probabilityOfPrecipitation")
        .and_then(|v| v.get("value"))
        .and_then(|v| v.as_f64())
        .unwrap_or(0.0);

    let source = format!("nws:forecast:{}/{},{}", office, x, y);

    Some(FeedSnapshot {
        price: temperature,
        timestamp: now_secs(),
        source,
        bid: wind_speed,
        ask: precip_chance,
        volume_24h: 0.0,
        last_trade_size: 0.0,
        last_trade_is_buy: false,
    })
}

fn parse_nws_alerts(body: &str, state: &str) -> Option<FeedSnapshot> {
    let json: serde_json::Value = serde_json::from_str(body).ok()?;

    let features = json.get("features")?.as_array()?;

    let has_alerts = !features.is_empty();
    let price = if has_alerts { 1.0 } else { 0.0 };

    // Find highest severity alert
    let severity = if has_alerts {
        features
            .iter()
            .filter_map(|f| {
                f.get("properties")
                    .and_then(|p| p.get("severity"))
                    .and_then(|v| v.as_str())
            })
            .map(severity_rank)
            .max()
            .map(severity_name)
            .unwrap_or("Unknown")
    } else {
        "None"
    };

    let alert_count = features.len() as f64;
    let source = format!("nws:alerts:{}:{}", state, severity);

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

fn extract_first_number(s: &str) -> f64 {
    let mut num_str = String::new();
    let mut found_digit = false;
    for ch in s.chars() {
        if ch.is_ascii_digit() || (ch == '.' && found_digit) {
            num_str.push(ch);
            found_digit = true;
        } else if found_digit {
            break;
        }
    }
    num_str.parse::<f64>().unwrap_or(0.0)
}

fn severity_rank(severity: &str) -> u8 {
    match severity.to_lowercase().as_str() {
        "extreme" => 4,
        "severe" => 3,
        "moderate" => 2,
        "minor" => 1,
        _ => 0,
    }
}

fn severity_name(rank: u8) -> &'static str {
    match rank {
        4 => "Extreme",
        3 => "Severe",
        2 => "Moderate",
        1 => "Minor",
        _ => "Unknown",
    }
}

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

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

    #[test]
    fn test_parse_nws_forecast() {
        let json = r#"{
            "properties": {
                "periods": [
                    {
                        "temperature": 72,
                        "windSpeed": "10 to 15 mph",
                        "probabilityOfPrecipitation": {"value": 30}
                    }
                ]
            }
        }"#;

        let snap = parse_nws_forecast(json, "TOP", 31, 80).unwrap();
        assert!((snap.price - 72.0).abs() < 1e-6);
        assert!((snap.bid - 10.0).abs() < 1e-6);
        assert!((snap.ask - 30.0).abs() < 1e-6);
        assert_eq!(snap.source, "nws:forecast:TOP/31,80");
    }

    #[test]
    fn test_parse_nws_forecast_simple_wind() {
        let json = r#"{
            "properties": {
                "periods": [
                    {
                        "temperature": 85,
                        "windSpeed": "5 mph",
                        "probabilityOfPrecipitation": {"value": null}
                    }
                ]
            }
        }"#;

        let snap = parse_nws_forecast(json, "MIA", 75, 50).unwrap();
        assert!((snap.price - 85.0).abs() < 1e-6);
        assert!((snap.bid - 5.0).abs() < 1e-6);
        assert!((snap.ask - 0.0).abs() < 1e-6); // null precipChance -> 0.0
    }

    #[test]
    fn test_parse_nws_forecast_empty_periods() {
        let json = r#"{"properties": {"periods": []}}"#;
        let result = parse_nws_forecast(json, "TOP", 31, 80);
        assert!(result.is_none());
    }

    #[test]
    fn test_parse_nws_alerts_with_alerts() {
        let json = r#"{
            "features": [
                {
                    "properties": {
                        "severity": "Severe",
                        "event": "Tornado Warning"
                    }
                },
                {
                    "properties": {
                        "severity": "Moderate",
                        "event": "Flood Watch"
                    }
                }
            ]
        }"#;

        let snap = parse_nws_alerts(json, "FL").unwrap();
        assert!((snap.price - 1.0).abs() < 1e-6);
        assert!((snap.bid - 2.0).abs() < 1e-6); // 2 alerts
        assert!(snap.source.contains("Severe")); // highest severity
    }

    #[test]
    fn test_parse_nws_alerts_no_alerts() {
        let json = r#"{"features": []}"#;

        let snap = parse_nws_alerts(json, "FL").unwrap();
        assert!((snap.price - 0.0).abs() < 1e-6);
        assert!((snap.bid - 0.0).abs() < 1e-6);
        assert!(snap.source.contains("None"));
    }

    #[test]
    fn test_extract_first_number() {
        assert!((extract_first_number("10 mph") - 10.0).abs() < 1e-6);
        assert!((extract_first_number("5 to 10 mph") - 5.0).abs() < 1e-6);
        assert!((extract_first_number("15.5 knots") - 15.5).abs() < 1e-6);
        assert!((extract_first_number("no wind") - 0.0).abs() < 1e-6);
        assert!((extract_first_number("") - 0.0).abs() < 1e-6);
    }

    #[test]
    fn test_severity_ranking() {
        assert_eq!(severity_rank("Extreme"), 4);
        assert_eq!(severity_rank("Severe"), 3);
        assert_eq!(severity_rank("Moderate"), 2);
        assert_eq!(severity_rank("Minor"), 1);
        assert_eq!(severity_rank("Unknown"), 0);
    }

    #[test]
    fn test_clamp_interval() {
        assert!((clamp_interval(120.0, 60.0) - 120.0).abs() < 1e-6);
        assert!((clamp_interval(30.0, 60.0) - 60.0).abs() < 1e-6);
        assert!((clamp_interval(f64::NAN, 60.0) - 60.0).abs() < 1e-6);
        assert!((clamp_interval(-1.0, 60.0) - 60.0).abs() < 1e-6);
    }
}
