use std::{
    fmt,
    num::TryFromIntError,
    ops::{Add, Deref, Sub},
};

use localtime::LocalTime;
use sqlite as sql;

/// Milliseconds since epoch.
#[derive(
    Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[serde(transparent)]
pub struct Timestamp(u64);

impl Add<u64> for Timestamp {
    type Output = Timestamp;

    fn add(self, millis: u64) -> Self::Output {
        Self(self.0.saturating_add(millis))
    }
}

impl Sub<u64> for Timestamp {
    type Output = Timestamp;

    fn sub(self, millis: u64) -> Self::Output {
        Self(self.0.saturating_sub(millis))
    }
}

impl Timestamp {
    /// UNIX epoch.
    pub const EPOCH: Self = Self(0);
    /// Minimum value.
    pub const MIN: Self = Self(0);
    /// Maximum value.
    // Nb. This is the maximum value that can fit in a signed 64-bit integer (`i64`).
    // This makes it possible to store timestamps in sqlite.
    pub const MAX: Self = Self(9223372036854775807);

    /// Convert to local time.
    pub fn to_local_time(&self) -> LocalTime {
        (*self).into()
    }
}

impl fmt::Display for Timestamp {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

impl Deref for Timestamp {
    type Target = u64;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl From<LocalTime> for Timestamp {
    fn from(t: LocalTime) -> Self {
        Self(t.as_millis())
    }
}

impl From<Timestamp> for LocalTime {
    fn from(t: Timestamp) -> Self {
        LocalTime::from_millis(t.0 as u128)
    }
}

impl TryFrom<u64> for Timestamp {
    type Error = u64;

    fn try_from(u: u64) -> Result<Self, u64> {
        if u <= *Self::MAX { Ok(Self(u)) } else { Err(u) }
    }
}

impl TryFrom<i64> for Timestamp {
    type Error = TryFromIntError;

    fn try_from(i: i64) -> Result<Self, Self::Error> {
        i.try_into().map(Self)
    }
}

impl TryFrom<&sql::Value> for Timestamp {
    type Error = sql::Error;

    fn try_from(value: &sql::Value) -> Result<Self, Self::Error> {
        match value {
            sql::Value::Integer(i) => match (*i).try_into() {
                Ok(u) => Ok(Timestamp(u)),
                Err(e) => Err(sql::Error {
                    code: None,
                    message: Some(format!("sql: invalid integer `{i}` for timestamp: {e}")),
                }),
            },
            _ => Err(sql::Error {
                code: None,
                message: Some("sql: invalid type for timestamp".to_owned()),
            }),
        }
    }
}

impl sql::BindableWithIndex for &Timestamp {
    fn bind<I: sql::ParameterIndex>(self, stmt: &mut sql::Statement<'_>, i: I) -> sql::Result<()> {
        match i64::try_from(*self.deref()) {
            Ok(integer) => integer.bind(stmt, i),
            Err(e) => Err(sql::Error {
                code: None,
                message: Some(format!("sql: invalid timestamp `{self}`: {e}")),
            }),
        }
    }
}

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

    #[test]
    fn test_timestamp_max() {
        assert_eq!(i64::try_from(*Timestamp::MAX), Ok(i64::MAX));
    }
}
