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

use chrono::{DateTime, Duration, Local, NaiveTime, Offset, TimeZone, Timelike, Utc};
use geo_types::{Latitude, Longitude};
use memorycache::MemoryCache;
use serde::Deserialize;

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

#[derive(Clone, Debug, PartialEq)]
pub struct SunMoon {
    pub sunrise: NaiveTime,
    pub sunset: NaiveTime,
    pub moonrise: Option<NaiveTime>,
    pub moonset: Option<NaiveTime>,
    pub moon_phase: LunarPhase,
}

impl SunMoon {
    fn from_js(val: SunMoonJs) -> Self {
        fn parse_time(val: String) -> Option<NaiveTime> {
            NaiveTime::parse_from_str(&val, "%H:%M").ok()
        }

        let sunrise = parse_time(val.sunrise).unwrap();
        let sunset = parse_time(val.sunset).unwrap();
        let moonrise = val.moonrise.and_then(parse_time);
        let moonset = val.moonset.and_then(parse_time);

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

#[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")]
    moon_phase: LunarPhase,
}

#[derive(Clone, Debug, Deserialize, PartialEq)]
pub enum LunarPhase {
    #[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 SolunaClient {
    client: reqwest::Client,
    memory_cache: MemoryCache<SunMoonJs>,
}

impl SolunaClient {
    pub fn new() -> Self {
        Self {
            client: reqwest::Client::new(),
            memory_cache: MemoryCache::default(),
        }
    }

    pub async fn request<Tz: TimeZone>(
        &self,
        latitude: Latitude,
        longitude: Longitude,
        day: DateTime<Tz>,
    ) -> SunMoon {
        let date = day.date_naive().format("%Y%m%d");
        let url = format!(
            "{}/{},{},{},{}",
            ENDPOINT,
            latitude,
            longitude,
            date,
            day.offset().fix().local_minus_utc() / 3600
        );
        let js = self
            .memory_cache
            .find(&url, async {
                let response = self.client.get(&url).send().await.unwrap();
                let expiration = response
                    .headers()
                    .get(reqwest::header::EXPIRES)
                    .and_then(|header| header.to_str().ok())
                    .and_then(|expiration| DateTime::parse_from_rfc2822(expiration).ok())
                    .map(DateTime::<Utc>::from)
                    .unwrap_or(
                        Local::now()
                            .with_hour(0)
                            .and_then(|dt| dt.with_minute(0))
                            .and_then(|dt| dt.with_second(0))
                            .and_then(|dt| dt.with_nanosecond(0))
                            .map(|dt| dt.with_timezone(&Utc))
                            .unwrap()
                            + Duration::days(1),
                    );
                let soluna: SunMoonJs = response.json().await.unwrap();
                (expiration, soluna)
            })
            .await;

        SunMoon::from_js(js)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    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 sun_moon_js: SunMoonJs = serde_json::from_str(EXAMPLE).unwrap();
        let sun_moon = SunMoon::from_js(sun_moon_js);
        assert_eq!(
            sun_moon,
            SunMoon {
                sunrise: NaiveTime::from_hms_opt(7, 15, 0).unwrap(),
                sunset: NaiveTime::from_hms_opt(17, 45, 0).unwrap(),
                moonrise: None,
                moonset: Some(NaiveTime::from_hms_opt(15, 02, 0).unwrap()),
                moon_phase: LunarPhase::WaningCrescent,
            }
        );
    }
}