// 41.78, -71.41
// https://api.solunar.org/solunar/41.78,-71.41,20211029,-4

use cachememory::Cache;
use chrono::{Date, DateTime, Duration, NaiveTime, Offset, Timelike, Utc};
use chrono_tz::Tz;
use geo_types::{Latitude, Longitude};
use reqwest;

const ENDPOINT: &str = "https://api.solunar.org/solunar";

#[derive(Clone, Debug, PartialEq)]
pub struct SunMoon {
    pub sunrise: DateTime<Tz>,
    pub sunset: DateTime<Tz>,
    pub moonrise: Option<DateTime<Tz>>,
    pub moonset: Option<DateTime<Tz>>,
    pub moonphase: MoonPhase,
}

impl SunMoon {
    fn from_js(day: Date<Tz>, val: SunMoonJs) -> Self {
        fn new_time(day: Date<Tz>, val: String) -> Option<DateTime<Tz>> {
            NaiveTime::parse_from_str(&val, "%H:%M")
                .map(|tempo| day.and_hms(tempo.hour(), tempo.minute(), 0))
                .ok()
        }

        let sunrise = new_time(day.clone(), val.sunrise).unwrap();
        let sunset = new_time(day.clone(), val.sunset).unwrap();
        let moonrise = val.moonrise.and_then(|v| new_time(day.clone(), v));
        let moonset = val.moonset.and_then(|v| new_time(day.clone(), v));

        Self {
            sunrise,
            sunset,
            moonrise,
            moonset,
            moonphase: val.moonphase,
        }
    }
}

#[derive(Clone, Debug, Deserialize)]
pub(crate) struct SunMoonJs {
    #[serde(alias = "sunRise")]
    sunrise: String,
    #[serde(alias = "sunSet")]
    sunset: String,
    #[serde(alias = "moonRise")]
    moonrise: Option<String>,
    #[serde(alias = "moonSet")]
    moonset: Option<String>,
    #[serde(alias = "moonPhase")]
    moonphase: MoonPhase,
}

#[derive(Clone, Debug, Deserialize, PartialEq)]
pub enum MoonPhase {
    #[serde(alias = "New Moon")]
    NewMoon,
    #[serde(alias = "Waxing Crescent")]
    WaxingCrescent,
    #[serde(alias = "First Quarter")]
    FirstQuarter,
    #[serde(alias = "Waxing Gibbous")]
    WaxingGibbous,
    #[serde(alias = "Full Moon")]
    FullMoon,
    #[serde(alias = "Waning Gibbous")]
    WaningGibbous,
    #[serde(alias = "Last Quarter")]
    LastQuarter,
    #[serde(alias = "Waning Crescent")]
    WaningCrescent,
}

pub struct SunMoonClient {
    client: reqwest::Client,
    cache: Cache<SunMoonJs>,
}

impl SunMoonClient {
    pub fn new() -> Self {
        Self {
            client: reqwest::Client::new(),
            cache: Cache::new(),
        }
    }

    pub async fn demandu(
        &self,
        latitude: Latitude,
        longitude: Longitude,
        day: Date<Tz>,
    ) -> SunMoon {
        let date = day.format("%Y%m%d");
        let url = format!(
            "{}/{},{},{},{}",
            ENDPOINT,
            latitude,
            longitude,
            date,
            day.offset().fix().local_minus_utc() / 3600
        );
        let js = self
            .cache
            .find(&url, async {
                let response = self.client.get(&url).send().await.unwrap();
                let tempolimo = response
                    .headers()
                    .get(reqwest::header::EXPIRES)
                    .and_then(|header| header.to_str().ok())
                    .and_then(|tempolimo_str| DateTime::parse_from_rfc2822(tempolimo_str).ok())
                    .map(|dt_local| DateTime::<Utc>::from(dt_local))
                    .unwrap_or(Utc::now() + Duration::seconds(3600));
                let soluna: SunMoonJs = response.json().await.unwrap();
                (tempolimo, soluna)
            })
            .await;

        SunMoon::from_js(day, js)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use chrono::TimeZone;
    use chrono_tz::America::New_York;
    use serde_json;

    const EXAMPLE: &str = "{\"sunRise\":\"7:15\",\"sunTransit\":\"12:30\",\"sunSet\":\"17:45\",\"moonRise\":null,\"moonTransit\":\"7:30\",\"moonUnder\":\"19:54\",\"moonSet\":\"15:02\",\"moonPhase\":\"Waning Crescent\",\"moonIllumination\":0.35889454647387764,\"sunRiseDec\":7.25,\"sunTransitDec\":12.5,\"sunSetDec\":17.75,\"moonRiseDec\":null,\"moonSetDec\":15.033333333333333,\"moonTransitDec\":7.5,\"moonUnderDec\":19.9,\"minor1Start\":null,\"minor1Stop\":null,\"minor2StartDec\":14.533333333333333,\"minor2Start\":\"14:32\",\"minor2StopDec\":15.533333333333333,\"minor2Stop\":\"15:32\",\"major1StartDec\":6.5,\"major1Start\":\"06:30\",\"major1StopDec\":8.5,\"major1Stop\":\"08:30\",\"major2StartDec\":18.9,\"major2Start\":\"18:54\",\"major2StopDec\":20.9,\"major2Stop\":\"20:54\",\"dayRating\":1,\"hourlyRating\":{\"0\":20,\"1\":20,\"2\":0,\"3\":0,\"4\":0,\"5\":0,\"6\":20,\"7\":40,\"8\":40,\"9\":20,\"10\":0,\"11\":0,\"12\":0,\"13\":0,\"14\":0,\"15\":20,\"16\":20,\"17\":20,\"18\":40,\"19\":20,\"20\":20,\"21\":20,\"22\":0,\"23\":0}}";

    #[test]
    fn it_parses_a_response() {
        let day = New_York.ymd(2021, 10, 29);
        let sun_moon_js: SunMoonJs = serde_json::from_str(EXAMPLE).unwrap();
        let sun_moon = SunMoon::from_js(day.clone(), sun_moon_js);
        assert_eq!(
            sun_moon,
            SunMoon {
                sunrise: day.and_hms(7, 15, 0),
                sunset: day.and_hms(17, 45, 0),
                moonrise: None,
                moonset: Some(day.and_hms(15, 02, 0)),
                moonphase: MoonPhase::WaningCrescent,
            }
        );
    }
}