use anyhow::{anyhow, Result};
use chrono::NaiveDateTime;
use reqwest::{Client, Url};
use serde::{self, Deserialize, Serialize};

use crate::date_format::{self, OPEN_METEO_DATE_FORMAT};
use crate::emoji::{self, emoji_for_weather};
use crate::units::Units;
use crate::wind;

const API_URL: &str = "https://api.open-meteo.com/v1/forecast";
const DATE_OUTPUT_FORMAT: &str = "%b %-d, %H:%M";

#[derive(Serialize, Debug)]
pub struct Notification {
    pub title: String,
    pub subtitle: String,
    pub body: String,
}

#[derive(Serialize, Debug)]
pub struct Weather<'a> {
    pub name: Option<String>,
    pub location: String,
    pub units: Units,
    pub icon: &'a str,

    #[serde(with = "date_format")]
    pub date: NaiveDateTime,
    pub weather_code: i8,
    pub probability_of_precipitation: i8,
    pub temperature: f64,
    pub feels_like: f64,
    pub humidity: i8,
    pub wind_speed: f64,
    pub wind_direction: i32,
}

impl<'a> Weather<'a> {
    pub async fn new(
        when: NaiveDateTime,
        latitude: f64,
        longitude: f64,
        units: &Units,
        name: Option<String>,
        location: String,
    ) -> Result<Self> {
        let date = when.format("%Y-%m-%d");
        let params = [
            "temperature_2m",
            "apparent_temperature",
            "precipitation_probability",
            "relativehumidity_2m",
            "windspeed_10m",
            "winddirection_10m",
            "weathercode",
        ]
        .join(",");
        let url = Url::parse_with_params(
            API_URL,
            &[
                ("latitude", latitude.to_string()),
                ("longitude", longitude.to_string()),
                ("start_date", date.to_string()),
                ("end_date", date.to_string()),
                ("temperature_unit", units.temperature()),
                ("windspeed_10m", units.speed()),
                ("timezone", "auto".to_string()),
                ("hourly", params),
            ],
        )?;

        let resp = Client::new().get(url.to_string()).send().await?;
        if !resp.status().is_success() {
            return Err(anyhow!(
                "HTTP request to {} returned {}: {}",
                url,
                resp.status(),
                resp.text().await?
            ));
        }

        let data: Response = resp.json().await.map_err(|e| {
            let message = format!("Failed to parse response JSON body from {url}: {e}");
            anyhow!(message)
        })?;
        data.hourly.as_weather(when, name, location, units)
    }

    pub fn as_notification(&self) -> Result<Notification> {
        let title = match &self.name {
            Some(name) => format!(
                "{} {} ({})",
                emoji::CALENDAR,
                name,
                self.date.format(DATE_OUTPUT_FORMAT),
            ),
            None => "".to_string(),
        };
        let subtitle = format!("{} {}", emoji::GLOBE, self.location);
        let temperature = match self.units {
            Units::Metric => "C",
            Units::Imperial => "F",
        };
        let speed = match self.units {
            Units::Metric => "km/h",
            Units::Imperial => "mph",
        };
        let body = format!(
            "{} {}Â°{} (feels like {}Â°{})\n{} {}% chance of rain & {}% humidity\n{} {}{} {}",
            emoji::emoji_for_weather(self.weather_code)?,
            self.temperature.round(),
            temperature,
            self.feels_like.round(),
            temperature,
            emoji::PRECIPITATION,
            self.probability_of_precipitation,
            self.humidity,
            emoji::WIND,
            self.wind_speed.round(),
            speed,
            wind::wind_direction(self.wind_direction)?,
        );

        Ok(Notification {
            title,
            subtitle,
            body,
        })
    }
    pub fn as_string(&self, json: bool) -> Result<String> {
        if json {
            return Ok(serde_json::to_string(&self)?);
        }

        let notification = self.as_notification()?;
        Ok(format!(
            "{} {}\n{}",
            notification.title,
            notification.subtitle,
            notification.body.replace('\n', " "),
        ))
    }
}

#[derive(Deserialize, Debug)]
struct Response {
    hourly: Hourly,
}

#[derive(Debug)]
struct HourlyItem {
    time: NaiveDateTime,
    temperature_2m: f64,
    apparent_temperature: f64,
    relativehumidity_2m: i8,
    precipitation_probability: i8,
    windspeed_10m: f64,
    winddirection_10m: i32,
    weathercode: i8,
}

#[derive(Deserialize, Debug)]
struct Hourly {
    time: Vec<String>,
    temperature_2m: Vec<Option<f64>>,
    apparent_temperature: Vec<Option<f64>>,
    relativehumidity_2m: Vec<Option<i8>>,
    precipitation_probability: Vec<Option<i8>>,
    windspeed_10m: Vec<Option<f64>>,
    winddirection_10m: Vec<Option<i32>>,
    weathercode: Vec<Option<i8>>,
}

impl Hourly {
    fn item(&self, idx: usize) -> Option<HourlyItem> {
        Some(HourlyItem {
            time: NaiveDateTime::parse_from_str(&self.time[idx], OPEN_METEO_DATE_FORMAT).ok()?,
            temperature_2m: self.temperature_2m[idx]?,
            apparent_temperature: self.apparent_temperature[idx]?,
            relativehumidity_2m: self.relativehumidity_2m[idx]?,
            precipitation_probability: self.precipitation_probability[idx].unwrap_or(0),
            windspeed_10m: self.windspeed_10m[idx]?,
            winddirection_10m: self.winddirection_10m[idx]?,
            weathercode: self.weathercode[idx]?,
        })
    }

    fn as_weather<'a>(
        &self,
        target: NaiveDateTime,
        name: Option<String>,
        location: String,
        units: &Units,
    ) -> Result<Weather<'a>> {
        let item: HourlyItem = (0..self.time.len())
            .filter_map(|idx| self.item(idx))
            .map(|item| {
                let diff = (target - item.time).num_minutes().abs();
                (item, diff)
            })
            .min_by_key(|(_, diff)| *diff)
            .ok_or(anyhow!("No weather data found"))?
            .0;

        Ok(Weather {
            name,
            location,
            weather_code: item.weathercode,
            icon: emoji_for_weather(item.weathercode)?,
            units: units.clone(),
            date: item.time,
            probability_of_precipitation: item.precipitation_probability,
            temperature: item.temperature_2m,
            feels_like: item.apparent_temperature,
            humidity: item.relativehumidity_2m,
            wind_speed: item.windspeed_10m,
            wind_direction: item.winddirection_10m,
        })
    }
}

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

    #[test]
    fn test_weather_as_notification() {
        let weather = Weather {
            name: Some("Event".to_string()),
            location: "Location".to_string(),
            units: Units::Metric,
            icon: "â˜€ï¸�",
            date: NaiveDateTime::parse_from_str("2021-05-20 8:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
            weather_code: 1,
            probability_of_precipitation: 20,
            temperature: 25.0,
            feels_like: 28.0,
            humidity: 80,
            wind_speed: 10.0,
            wind_direction: 180,
        };

        let result = weather.as_notification();
        assert!(result.is_ok());

        let notification = result.unwrap();
        assert_eq!(notification.title, "ğŸ—“ï¸� Event (May 20, 08:00)");
        assert_eq!(notification.subtitle, "ğŸŒ� Location");

        let lines = notification.body.split('\n').collect::<Vec<&str>>();
        assert_eq!(lines.len(), 3);
        assert_eq!(lines[0], "â˜€ï¸� 25Â°C (feels like 28Â°C)");
        assert_eq!(lines[1], "â˜” 20% chance of rain & 80% humidity");
        assert_eq!(lines[2], "ğŸ’¨ 10km/h S");
    }

    #[test]
    fn test_weather_as_string() {
        let weather = Weather {
            name: Some("Event".to_string()),
            location: "Location".to_string(),
            units: Units::Metric,
            icon: "â˜€ï¸�",
            date: NaiveDateTime::parse_from_str("2021-05-20 8:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
            weather_code: 1,
            probability_of_precipitation: 20,
            temperature: 25.0,
            feels_like: 28.0,
            humidity: 80,
            wind_speed: 10.0,
            wind_direction: 180,
        };

        let result = weather.as_string(false);
        assert!(result.is_ok());
        assert_eq!(
            result.unwrap(),
            [
                "ğŸ—“ï¸� Event (May 20, 08:00) ğŸŒ� Location",
                "â˜€ï¸� 25Â°C (feels like 28Â°C) â˜” 20% chance of rain & 80% humidity ğŸ’¨ 10km/h S"
            ]
            .join("\n")
        );
    }

    #[test]
    fn test_weather_as_json() {
        let weather = Weather {
            name: Some("Event".to_string()),
            location: "Location".to_string(),
            units: Units::Metric,
            icon: "â˜€ï¸�",
            date: NaiveDateTime::parse_from_str("2021-05-20 8:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
            weather_code: 1,
            probability_of_precipitation: 20,
            temperature: 25.0,
            feels_like: 28.0,
            humidity: 80,
            wind_speed: 10.0,
            wind_direction: 180,
        };

        let result = weather.as_string(true);
        assert!(result.is_ok());
        assert_eq!(
            result.unwrap(),
            r#"{"name":"Event","location":"Location","units":"Metric","icon":"â˜€ï¸�","date":"2021-05-20 08:00:00","weather_code":1,"probability_of_precipitation":20,"temperature":25.0,"feels_like":28.0,"humidity":80,"wind_speed":10.0,"wind_direction":180}"#
        );
    }
}
