/*
Copyright 2020-2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>

This file is part of the Luminescent Dreams Tools.

Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/

use chrono::{DateTime, FixedOffset, NaiveDate};
use std::{cmp::Ordering, fmt, io, str};
use thiserror::Error;
use uuid::Uuid;

#[derive(Debug, Error)]
pub enum EmseriesReadError {
    /// Indicates that the UUID specified is invalid and cannot be parsed
    #[error("UUID failed to parse: {0}")]
    UUIDParseError(uuid::Error),

    /// Indicates an error in the JSON deserialization
    #[error("Error parsing JSON: {0}")]
    JSONParseError(serde_json::error::Error),

    #[error("Record was deleted")]
    RecordDeleted(RecordId),

    /// Indicates a general IO error
    #[error("IO Error: {0}")]
    IOError(io::Error),
}

#[derive(Debug, Error)]
pub enum EmseriesWriteError {
    /// Indicates a general IO error
    #[error("IO Error: {0}")]
    IOError(io::Error),

    /// Indicates an error in the JSON serialization
    #[error("Error generating a JSON string: {0}")]
    JSONWriteError(serde_json::error::Error),
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// A Timestamp, stored with reference to human reckoning. This could be either a Naive Date or a
/// date and a time with a timezone. The idea of the "human reckoning" is that, no matter what
/// timezone the record was created in, we want to group things based on the date that the human
/// was perceiving at the time it was recorded.
pub enum Timestamp {
    DateTime(DateTime<FixedOffset>),
    Date(NaiveDate),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum TimestampJS {
    DateTime(String),
    Date(String),
}

impl From<Timestamp> for TimestampJS {
    fn from(s: Timestamp) -> TimestampJS {
        match s {
            Timestamp::DateTime(ts) => TimestampJS::DateTime(ts.to_rfc3339()),
            Timestamp::Date(ts) => TimestampJS::Date(ts.to_string()),
        }
    }
}

impl From<TimestampJS> for Timestamp {
    fn from(s: TimestampJS) -> Timestamp {
        match s {
            TimestampJS::DateTime(ts) => {
                Timestamp::DateTime(DateTime::parse_from_rfc3339(&ts).unwrap())
            }
            TimestampJS::Date(ts) => Timestamp::Date(ts.parse::<NaiveDate>().unwrap()),
        }
    }
}

impl str::FromStr for Timestamp {
    type Err = chrono::ParseError;
    fn from_str(line: &str) -> Result<Self, Self::Err> {
        DateTime::parse_from_rfc3339(line)
            .map(Timestamp::DateTime)
            .or(NaiveDate::from_str(line).map(Timestamp::Date))
    }
}

impl PartialOrd for Timestamp {
    fn partial_cmp(&self, other: &Timestamp) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Timestamp {
    fn cmp(&self, other: &Timestamp) -> Ordering {
        match (self, other) {
            (Timestamp::DateTime(dt1), Timestamp::DateTime(dt2)) => dt1.cmp(dt2),
            (Timestamp::DateTime(dt1), Timestamp::Date(dt2)) => dt1.date_naive().cmp(dt2),
            (Timestamp::Date(dt1), Timestamp::DateTime(dt2)) => dt1.cmp(&dt2.date_naive()),
            (Timestamp::Date(dt1), Timestamp::Date(dt2)) => dt1.cmp(dt2),
        }
    }
}

/// Any element to be put into the database needs to be Recordable. This is the common API that
/// will aid in searching and later in indexing records.
pub trait Recordable {
    /// The timestamp for the record.
    fn timestamp(&self) -> Timestamp;

    /// A list of string tags that can be used for indexing. This list defined per-type.
    fn tags(&self) -> Vec<String>;
}

/// Uniquely identifies a record.
///
/// This is a wrapper around a basic uuid with some extra convenience methods.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
pub struct RecordId(Uuid);

impl Default for RecordId {
    fn default() -> Self {
        Self(Uuid::new_v4())
    }
}

impl str::FromStr for RecordId {
    type Err = EmseriesReadError;

    /// Parse a RecordId from a string. Raise UUIDParseError if the parsing fails.
    fn from_str(val: &str) -> Result<Self, Self::Err> {
        Uuid::parse_str(val)
            .map(RecordId)
            .map_err(EmseriesReadError::UUIDParseError)
    }
}

impl fmt::Display for RecordId {
    /// Convert to a hyphenated string
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        write!(f, "{}", self.0.to_hyphenated())
    }
}

/// A record represents data that actually exists in the database. Users cannot make the record
/// directly, as the database will create them.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct Record<T: Clone + Recordable> {
    pub id: RecordId,
    pub data: T,
}

impl<T: Clone + Recordable> Record<T> {
    pub fn date(&self) -> NaiveDate {
        match self.data.timestamp() {
            Timestamp::DateTime(dt) => dt.date_naive(),
            Timestamp::Date(dt) => dt,
        }
    }

    pub fn timestamp(&self) -> Timestamp {
        self.data.timestamp()
    }

    pub fn map<Map, U>(self, map: Map) -> Record<U>
    where
        Map: Fn(T) -> U,
        U: Clone + Recordable,
    {
        Record {
            id: self.id,
            data: map(self.data),
        }
    }
}

#[cfg(test)]
mod test {
    extern crate dimensioned;

    extern crate serde_json;

    use self::dimensioned::si::{Kilogram, KG};
    use super::*;
    use chrono::TimeZone;
    use chrono_tz::Etc::UTC;

    #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
    pub struct Weight(Kilogram<f64>);

    #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
    pub struct WeightRecord {
        pub date: NaiveDate,
        pub weight: Weight,
    }

    impl Recordable for WeightRecord {
        fn timestamp(&self) -> Timestamp {
            Timestamp::Date(self.date)
        }

        fn tags(&self) -> Vec<String> {
            Vec::new()
        }
    }

    #[test]
    fn timestamp_parses_utc_time() {
        assert_eq!(
            "2003-11-10T06:00:00Z".parse::<Timestamp>().unwrap(),
            Timestamp::DateTime(
                UTC.with_ymd_and_hms(2003, 11, 10, 6, 0, 0)
                    .unwrap()
                    .with_timezone(&FixedOffset::east_opt(0).unwrap())
            ),
        );
    }

    #[test]
    fn timestamp_parses_date() {
        assert_eq!(
            "2023-11-10".parse::<Timestamp>().unwrap(),
            Timestamp::Date(NaiveDate::from_ymd_opt(2023, 11, 10).unwrap())
        );
    }

    /*
    #[ignore]
    fn v_alpha_serialization() {
        const WEIGHT_ENTRY: &str = "{\"data\":{\"weight\":77.79109},\"date\":\"2003-11-10\",\"id\":\"3330c5b0-783f-4919-b2c4-8169c38f65ff\"}";

        let rec: Record<WeightRecord> = WEIGHT_ENTRY
            .parse()
            .expect("should successfully parse the record");
        assert_eq!(
            rec.id,
            "3330c5b0-783f-4919-b2c4-8169c38f65ff".parse().unwrap()
        );
        assert_eq!(
            rec.data,
            WeightRecord {
                date: NaiveDate::from_ymd_opt(2003, 11, 10).unwrap(),
                weight: Weight(77.79109 * KG),
            }
        );
    }
    */

    #[test]
    fn serialization_output() {
        let rec = WeightRecord {
            date: NaiveDate::from_ymd_opt(2003, 11, 10).unwrap(),
            weight: Weight(77.0 * KG),
        };
        assert_eq!(
            serde_json::to_string(&rec).unwrap(),
            "{\"date\":\"2003-11-10\",\"weight\":77.0}"
        );

        let rec2 = WeightRecord {
            date: NaiveDate::from_ymd_opt(2003, 11, 10).unwrap(),
            weight: Weight(77.0 * KG),
        };
        assert_eq!(
            serde_json::to_string(&rec2).unwrap(),
            "{\"date\":\"2003-11-10\",\"weight\":77.0}"
        );
    }

    #[test]
    fn two_datetimes_can_be_compared() {
        let time1 = Timestamp::DateTime(
            UTC.with_ymd_and_hms(2003, 11, 10, 6, 0, 0)
                .unwrap()
                .with_timezone(&FixedOffset::east_opt(0).unwrap()),
        );
        let time2 = Timestamp::DateTime(
            UTC.with_ymd_and_hms(2003, 11, 11, 6, 0, 0)
                .unwrap()
                .with_timezone(&FixedOffset::east_opt(0).unwrap()),
        );
        assert!(time1 < time2);
    }

    #[test]
    fn two_dates_can_be_compared() {
        let time1: Timestamp = Timestamp::Date(NaiveDate::from_ymd_opt(2003, 11, 10).unwrap());
        let time2: Timestamp = Timestamp::Date(NaiveDate::from_ymd_opt(2003, 11, 11).unwrap());
        assert!(time1 < time2);
    }

    #[test]
    fn datetime_and_date_can_be_compared() {
        let time1 = Timestamp::DateTime(
            UTC.with_ymd_and_hms(2003, 11, 10, 6, 0, 0)
                .unwrap()
                .with_timezone(&FixedOffset::east_opt(0).unwrap()),
        );
        let time2 = Timestamp::Date(NaiveDate::from_ymd_opt(2003, 11, 11).unwrap());
        assert!(time1 < time2)
    }
}
