use std::{
    fmt,
    num::{ParseFloatError, ParseIntError},
    ops::{Deref, DerefMut},
    str::FromStr,
};

use chrono::NaiveDateTime as ChronoNaiveDateTime;
use pest::Parser;
use serde::{
    de::{self, Visitor},
    Deserialize, Deserializer,
};

use crate::MAX_DATETIME_UNIX_TIMESTAMP;

#[derive(Parser)]
#[grammar = "grammars/date_and_time.pest"]
struct DateAndTimeParser;

#[derive(PartialEq, Debug, Clone)]
pub struct NaiveDateTime(pub ChronoNaiveDateTime);
impl From<ChronoNaiveDateTime> for NaiveDateTime {
    fn from(inner: ChronoNaiveDateTime) -> Self {
        Self(inner)
    }
}
impl Deref for NaiveDateTime {
    type Target = ChronoNaiveDateTime;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
impl DerefMut for NaiveDateTime {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

#[derive(thiserror::Error, Debug)]
pub enum ParseError {
    #[error("FormatMismatch {0}")]
    FormatMismatch(String),
    #[error("ValueInvalid {0}")]
    ValueInvalid(String),
    #[error("Unknown")]
    Unknown,
}
impl FromStr for NaiveDateTime {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let pair = DateAndTimeParser::parse(Rule::datetime64, s)
            .map_err(|err| ParseError::FormatMismatch(err.to_string()))?
            .next()
            .ok_or(ParseError::Unknown)?
            .into_inner()
            .next()
            .ok_or(ParseError::Unknown)?;

        match pair.as_rule() {
            Rule::datetime64_simple_milli => parse_simple_str(pair.as_str(), Precision::Milli),
            Rule::datetime64_simple_micro => parse_simple_str(pair.as_str(), Precision::Micro),
            Rule::datetime64_simple_nano => parse_simple_str(pair.as_str(), Precision::Nano),
            Rule::datetime64_iso_milli => parse_iso_str(pair.as_str(), Precision::Milli),
            Rule::datetime64_iso_micro => parse_iso_str(pair.as_str(), Precision::Micro),
            Rule::datetime64_iso_nano => parse_iso_str(pair.as_str(), Precision::Nano),
            Rule::datetime64_unix_timestamp_milli => {
                parse_unix_timestamp_str(pair.as_str(), Precision::Milli)
            }
            Rule::datetime64_unix_timestamp_micro => {
                parse_unix_timestamp_str(pair.as_str(), Precision::Micro)
            }
            Rule::datetime64_unix_timestamp_nano => {
                parse_unix_timestamp_str(pair.as_str(), Precision::Nano)
            }
            _ => Err(ParseError::Unknown),
        }
    }
}

#[derive(Debug)]
enum Precision {
    Milli,
    Micro,
    Nano,
}
fn parse_simple_str(s: &str, precision: Precision) -> Result<NaiveDateTime, ParseError> {
    let fmt = match precision {
        Precision::Milli => "%Y-%m-%d %H:%M:%S%.3f",
        Precision::Micro => "%Y-%m-%d %H:%M:%S%.6f",
        Precision::Nano => "%Y-%m-%d %H:%M:%S%.9f",
    };
    ChronoNaiveDateTime::parse_from_str(s, fmt)
        .map(Into::into)
        .map_err(|err| ParseError::ValueInvalid(err.to_string()))
}
fn parse_iso_str(s: &str, precision: Precision) -> Result<NaiveDateTime, ParseError> {
    let fmt = match precision {
        Precision::Milli => "%Y-%m-%dT%H:%M:%S%.3fZ",
        Precision::Micro => "%Y-%m-%dT%H:%M:%S%.6fZ",
        Precision::Nano => "%Y-%m-%dT%H:%M:%S%.9fZ",
    };
    ChronoNaiveDateTime::parse_from_str(s, fmt)
        .map(Into::into)
        .map_err(|err| ParseError::ValueInvalid(err.to_string()))
}
fn parse_unix_timestamp_str(s: &str, precision: Precision) -> Result<NaiveDateTime, ParseError> {
    let nsecs_str = match precision {
        Precision::Milli => &s[s.len() - 3..],
        Precision::Micro => &s[s.len() - 6..],
        Precision::Nano => &s[s.len() - 9..],
    };
    let nsecs: u32 = nsecs_str
        .parse()
        .map_err(|err: ParseIntError| ParseError::ValueInvalid(err.to_string()))?;
    let nsecs = match precision {
        Precision::Milli => nsecs * 1_000_000,
        Precision::Micro => nsecs * 1_000,
        Precision::Nano => nsecs,
    };

    let v: f64 = s
        .parse()
        .map_err(|err: ParseFloatError| ParseError::ValueInvalid(err.to_string()))?;

    if v as u64 > MAX_DATETIME_UNIX_TIMESTAMP {
        return Err(ParseError::ValueInvalid(
            "Override the max Unix Timestamp".to_string(),
        ));
    }

    Ok(ChronoNaiveDateTime::from_timestamp(v as i64, nsecs).into())
}

struct NaiveDateTimeVisitor;
impl<'de> Visitor<'de> for NaiveDateTimeVisitor {
    type Value = NaiveDateTime;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_str("format simple or iso or unix_timestamp")
    }

    fn visit_str<E>(self, string: &str) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        string
            .parse()
            .map_err(|err: ParseError| de::Error::custom(err.to_string()))
    }
}
impl<'de> Deserialize<'de> for NaiveDateTime {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_str(NaiveDateTimeVisitor)
    }
}
pub fn deserialize<'de, D>(d: D) -> Result<ChronoNaiveDateTime, D::Error>
where
    D: de::Deserializer<'de>,
{
    d.deserialize_str(NaiveDateTimeVisitor).map(|x| x.0)
}

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

    use std::{error, fs, path::PathBuf};

    use chrono::NaiveDate;

    #[test]
    fn test_parse() -> Result<(), Box<dyn error::Error>> {
        assert_eq!(
            "2021-03-01 01:02:03.123".parse::<NaiveDateTime>()?,
            NaiveDate::from_ymd(2021, 3, 1)
                .and_hms_milli(1, 2, 3, 123)
                .into()
        );

        assert_eq!(
            "2021-03-01 01:02:03.123456".parse::<NaiveDateTime>()?,
            NaiveDate::from_ymd(2021, 3, 1)
                .and_hms_micro(1, 2, 3, 123456)
                .into()
        );

        assert_eq!(
            "2021-03-01T01:02:03.123Z".parse::<NaiveDateTime>()?,
            NaiveDate::from_ymd(2021, 3, 1)
                .and_hms_milli(1, 2, 3, 123)
                .into()
        );

        assert_eq!(
            "1614560523.123".parse::<NaiveDateTime>()?,
            NaiveDate::from_ymd(2021, 3, 1)
                .and_hms_milli(1, 2, 3, 123)
                .into()
        );

        match format!("").parse::<NaiveDateTime>() {
            Ok(_) => assert!(false),
            Err(ParseError::FormatMismatch(err)) if err.ends_with("= expected datetime64") => {}
            Err(err) => assert!(false, "{:?}", err),
        }

        match format!(
            "{}.123",
            NaiveDate::from_ymd(2106, 1, 1).and_hms(0, 0, 0).timestamp()
        )
        .parse::<NaiveDateTime>()
        {
            Ok(_) => assert!(false),
            Err(ParseError::ValueInvalid(err)) if err == "Override the max Unix Timestamp" => {}
            Err(err) => assert!(false, "{:?}", err),
        }

        Ok(())
    }

    #[derive(Deserialize)]
    struct Row {
        #[serde(deserialize_with = "crate::datetime64::deserialize")]
        datetime64_milli_utc: chrono::NaiveDateTime,
        #[allow(dead_code)]
        datetime64_milli_shanghai: NaiveDateTime,
        #[serde(deserialize_with = "crate::datetime64::deserialize")]
        datetime64_micro_utc: chrono::NaiveDateTime,
        #[allow(dead_code)]
        datetime64_micro_shanghai: NaiveDateTime,
        #[serde(deserialize_with = "crate::datetime64::deserialize")]
        datetime64_nano_utc: chrono::NaiveDateTime,
        #[allow(dead_code)]
        datetime64_nano_shanghai: NaiveDateTime,
    }

    #[test]
    fn test_de() -> Result<(), Box<dyn error::Error>> {
        let deserializer = de::IntoDeserializer::<de::value::Error>::into_deserializer;
        assert_eq!(
            super::deserialize(deserializer("2021-03-01 01:02:03.123")).unwrap(),
            NaiveDate::from_ymd(2021, 3, 1).and_hms_milli(1, 2, 3, 123)
        );

        for format in ["simple", "iso", "unix_timestamp"].iter() {
            let content = fs::read_to_string(
                PathBuf::new().join(format!("tests/files/datetime64_{}.txt", format)),
            )?;
            let line = content.lines().next().unwrap();

            let Row {
                datetime64_milli_utc,
                datetime64_milli_shanghai: _,
                datetime64_micro_utc,
                datetime64_micro_shanghai: _,
                datetime64_nano_utc,
                datetime64_nano_shanghai: _,
            } = serde_json::from_str(line)?;
            assert_eq!(
                datetime64_milli_utc,
                NaiveDate::from_ymd(2021, 3, 1).and_hms_milli(1, 2, 3, 123)
            );
            assert_eq!(
                datetime64_micro_utc,
                NaiveDate::from_ymd(2021, 3, 1).and_hms_micro(1, 2, 3, 123456)
            );
            assert_eq!(
                datetime64_nano_utc,
                NaiveDate::from_ymd(2021, 3, 1).and_hms_nano(1, 2, 3, 123456789)
            );
        }

        Ok(())
    }
}
