//! timestamp — Wire-format timestamp primitives.
//!
//! The Ticksupply API serializes two kinds of timestamps:
//!
//! - **Market-data timestamps** — nanoseconds since the Unix epoch,
//!   serialized as JSON strings to preserve precision. Modeled by [`Nanos`].
//! - **Control-plane timestamps** (creation times, activity spans, grace
//!   windows, availability-refresh times) — RFC 3339 / ISO 8601 strings.
//!   Modeled by [`Timestamp`].
//!
//! Both types hold the raw wire representation and expose typed accessors
//! that convert on demand under the `chrono` / `time` Cargo features:
//!
//! - [`Nanos::to_chrono`] / [`Nanos::to_time`].
//! - [`Timestamp::to_chrono`] / [`Timestamp::to_time`].
//!
//! Request builders additionally accept user-supplied timestamp values
//! through the [`IntoTimestamp`] trait (`i64` nanoseconds, `&str`, a
//! [`chrono::DateTime<Utc>`] / [`time::OffsetDateTime`] under their
//! respective features).

use serde::{Deserialize, Deserializer, Serialize, Serializer};

/// Nanoseconds since the Unix epoch. The wire format for every timestamp
/// field in Ticksupply API response bodies.
///
/// Serializes as a JSON string (not a number) to preserve precision across
/// implementations.
///
/// # Examples
///
/// ```
/// use ticksupply::Nanos;
/// let n = Nanos::from_i64(1_704_067_200_000_000_000);
/// assert_eq!(n.as_i64(), 1_704_067_200_000_000_000);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct Nanos(i64);

impl Nanos {
    /// Constructs a [`Nanos`] from a raw `i64` nanosecond count.
    ///
    /// # Examples
    ///
    /// ```
    /// use ticksupply::Nanos;
    /// let n = Nanos::from_i64(1_704_067_200_000_000_000);
    /// assert_eq!(n.as_i64(), 1_704_067_200_000_000_000);
    /// ```
    pub const fn from_i64(n: i64) -> Self {
        Self(n)
    }

    /// Returns the raw `i64` nanoseconds since the Unix epoch.
    ///
    /// # Examples
    ///
    /// ```
    /// use ticksupply::Nanos;
    /// assert_eq!(Nanos::from_i64(42).as_i64(), 42);
    /// ```
    pub const fn as_i64(self) -> i64 {
        self.0
    }

    /// Converts the value into a [`chrono::DateTime<Utc>`].
    ///
    /// Available when the `chrono` feature is enabled (default).
    ///
    /// # Panics
    ///
    /// Panics only if the nanosecond count falls outside the range
    /// representable by `chrono::DateTime<Utc>` (roughly years ±262_143).
    /// Any value produced by the API or by [`Nanos::from_i64`] with a
    /// realistic Unix-epoch nanosecond count is comfortably within range.
    ///
    /// # Examples
    ///
    /// ```
    /// # #[cfg(feature = "chrono")]
    /// # {
    /// use ticksupply::Nanos;
    /// let dt = Nanos::from_i64(1_704_067_200_000_000_000).to_chrono();
    /// assert_eq!(dt.to_rfc3339(), "2024-01-01T00:00:00+00:00");
    /// # }
    /// ```
    #[cfg(feature = "chrono")]
    #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
    pub fn to_chrono(self) -> chrono::DateTime<chrono::Utc> {
        let secs = self.0.div_euclid(1_000_000_000);
        let nsec = self.0.rem_euclid(1_000_000_000) as u32;
        chrono::DateTime::from_timestamp(secs, nsec).expect("nanoseconds in range")
    }

    /// Converts the value into a [`time::OffsetDateTime`].
    ///
    /// Available when the `time` feature is enabled.
    ///
    /// # Panics
    ///
    /// Panics only if the value falls outside the range accepted by
    /// [`time::OffsetDateTime::from_unix_timestamp_nanos`]. Any `i64`
    /// nanosecond value is within that range.
    ///
    /// # Examples
    ///
    /// ```
    /// # #[cfg(feature = "time")]
    /// # {
    /// use ticksupply::Nanos;
    /// let dt = Nanos::from_i64(1_704_067_200_000_000_000).to_time();
    /// assert_eq!(dt.unix_timestamp(), 1_704_067_200);
    /// # }
    /// ```
    #[cfg(feature = "time")]
    #[cfg_attr(docsrs, doc(cfg(feature = "time")))]
    pub fn to_time(self) -> time::OffsetDateTime {
        time::OffsetDateTime::from_unix_timestamp_nanos(self.0 as i128)
            .expect("nanoseconds in range")
    }
}

impl Serialize for Nanos {
    fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
        s.serialize_str(&self.0.to_string())
    }
}

impl<'de> Deserialize<'de> for Nanos {
    fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
        use serde::de::Error;
        // Accept string (preferred wire form) or number (defensive).
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum Wire<'a> {
            Str(&'a str),
            Num(i64),
        }
        match Wire::deserialize(d)? {
            Wire::Str(s) => s.parse::<i64>().map(Nanos).map_err(D::Error::custom),
            Wire::Num(n) => Ok(Nanos(n)),
        }
    }
}

/// Converts a user-supplied timestamp value into the wire form accepted by
/// the API (a nanosecond count encoded as a JSON string).
///
/// Implemented for:
/// - [`i64`] — treated as nanoseconds since the Unix epoch.
/// - [`&str`] and [`String`] — passed through verbatim (caller's
///   responsibility to supply a format the API accepts: ISO 8601 or a
///   nanosecond string).
/// - [`Nanos`].
/// - [`chrono::DateTime<Utc>`] when the `chrono` feature is enabled.
/// - [`time::OffsetDateTime`] when the `time` feature is enabled.
///
/// # Panics
///
/// The `chrono::DateTime<Utc>` impl panics if the datetime falls outside
/// the `i64` nanosecond range (roughly 1677-09-21 to 2262-04-11). Any
/// realistic trading-data timestamp is comfortably within range.
///
/// # Examples
///
/// ```
/// use ticksupply::IntoTimestamp;
/// assert_eq!(1_704_067_200_000_000_000_i64.into_timestamp_string(), "1704067200000000000");
/// assert_eq!("2024-01-01T00:00:00Z".into_timestamp_string(), "2024-01-01T00:00:00Z");
/// ```
pub trait IntoTimestamp {
    /// Encodes the timestamp as a string suitable for the API.
    ///
    /// # Examples
    ///
    /// ```
    /// use ticksupply::IntoTimestamp;
    /// assert_eq!(123_i64.into_timestamp_string(), "123");
    /// ```
    fn into_timestamp_string(self) -> String;
}

impl IntoTimestamp for i64 {
    fn into_timestamp_string(self) -> String {
        self.to_string()
    }
}

impl IntoTimestamp for Nanos {
    fn into_timestamp_string(self) -> String {
        self.0.to_string()
    }
}

impl IntoTimestamp for &str {
    fn into_timestamp_string(self) -> String {
        self.to_string()
    }
}

impl IntoTimestamp for String {
    fn into_timestamp_string(self) -> String {
        self
    }
}

#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl IntoTimestamp for chrono::DateTime<chrono::Utc> {
    fn into_timestamp_string(self) -> String {
        self.timestamp_nanos_opt()
            .expect("datetime in representable range")
            .to_string()
    }
}

#[cfg(feature = "time")]
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl IntoTimestamp for time::OffsetDateTime {
    fn into_timestamp_string(self) -> String {
        self.unix_timestamp_nanos().to_string()
    }
}

/// An RFC 3339 / ISO 8601 timestamp as returned by the Ticksupply API.
///
/// Holds the server-supplied string losslessly; typed access is available
/// via [`Self::to_chrono`] and [`Self::to_time`] under their respective
/// Cargo features. Parse errors surface only when a typed accessor is
/// called, so deserialization never fails on an unexpected timestamp
/// format — you still get the raw string via [`Self::as_str`].
///
/// # Examples
///
/// ```
/// use ticksupply::Timestamp;
/// let t = Timestamp::from("2024-01-01T00:00:00Z");
/// assert_eq!(t.as_str(), "2024-01-01T00:00:00Z");
/// ```
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Timestamp(String);

impl Timestamp {
    /// Constructs a [`Timestamp`] from a string-like value.
    ///
    /// # Examples
    ///
    /// ```
    /// use ticksupply::Timestamp;
    /// let t = Timestamp::new("2024-01-01T00:00:00Z");
    /// assert_eq!(t.as_str(), "2024-01-01T00:00:00Z");
    /// ```
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }

    /// Returns the raw RFC 3339 / ISO 8601 string.
    ///
    /// # Examples
    ///
    /// ```
    /// use ticksupply::Timestamp;
    /// assert_eq!(Timestamp::from("2024-01-01T00:00:00Z").as_str(), "2024-01-01T00:00:00Z");
    /// ```
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Consumes the [`Timestamp`] and returns the underlying [`String`].
    ///
    /// # Examples
    ///
    /// ```
    /// use ticksupply::Timestamp;
    /// assert_eq!(Timestamp::from("2024-01-01T00:00:00Z").into_string(), "2024-01-01T00:00:00Z".to_string());
    /// ```
    pub fn into_string(self) -> String {
        self.0
    }

    /// Parses the wire string as a [`chrono::DateTime<Utc>`].
    ///
    /// Available when the `chrono` feature is enabled (default). The result
    /// is normalized to UTC; a timestamp with a non-UTC offset is converted
    /// in-place.
    ///
    /// # Errors
    ///
    /// Returns [`chrono::ParseError`] if the wire string is not a valid
    /// RFC 3339 timestamp.
    ///
    /// # Examples
    ///
    /// ```
    /// # #[cfg(feature = "chrono")]
    /// # {
    /// use ticksupply::Timestamp;
    /// let t = Timestamp::from("2024-01-01T00:00:00Z");
    /// let dt = t.to_chrono().unwrap();
    /// assert_eq!(dt.to_rfc3339(), "2024-01-01T00:00:00+00:00");
    /// # }
    /// ```
    #[cfg(feature = "chrono")]
    #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
    pub fn to_chrono(
        &self,
    ) -> std::result::Result<chrono::DateTime<chrono::Utc>, chrono::ParseError> {
        chrono::DateTime::parse_from_rfc3339(&self.0).map(|dt| dt.with_timezone(&chrono::Utc))
    }

    /// Parses the wire string as a [`time::OffsetDateTime`].
    ///
    /// Available when the `time` feature is enabled.
    ///
    /// # Errors
    ///
    /// Returns [`time::error::Parse`] if the wire string is not a valid
    /// RFC 3339 timestamp.
    ///
    /// # Examples
    ///
    /// ```
    /// # #[cfg(feature = "time")]
    /// # {
    /// use ticksupply::Timestamp;
    /// let t = Timestamp::from("2024-01-01T00:00:00Z");
    /// let dt = t.to_time().unwrap();
    /// assert_eq!(dt.unix_timestamp(), 1_704_067_200);
    /// # }
    /// ```
    #[cfg(feature = "time")]
    #[cfg_attr(docsrs, doc(cfg(feature = "time")))]
    pub fn to_time(&self) -> std::result::Result<time::OffsetDateTime, time::error::Parse> {
        time::OffsetDateTime::parse(&self.0, &time::format_description::well_known::Rfc3339)
    }
}

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

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

impl AsRef<str> for Timestamp {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl From<String> for Timestamp {
    fn from(s: String) -> Self {
        Self(s)
    }
}

impl From<&str> for Timestamp {
    fn from(s: &str) -> Self {
        Self(s.to_string())
    }
}

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

    #[test]
    fn nanos_roundtrip_json() {
        let n = Nanos::from_i64(1_704_067_200_123_456_789);
        let s = serde_json::to_string(&n).unwrap();
        assert_eq!(s, "\"1704067200123456789\"");
        let back: Nanos = serde_json::from_str(&s).unwrap();
        assert_eq!(back, n);
    }

    #[test]
    fn nanos_deserializes_number_too() {
        let back: Nanos = serde_json::from_str("1704067200000000000").unwrap();
        assert_eq!(back.as_i64(), 1_704_067_200_000_000_000);
    }

    #[test]
    fn into_timestamp_i64() {
        assert_eq!(123_i64.into_timestamp_string(), "123");
    }

    #[test]
    fn into_timestamp_str_passthrough() {
        assert_eq!(
            "2025-01-01T00:00:00Z".into_timestamp_string(),
            "2025-01-01T00:00:00Z"
        );
    }

    #[cfg(feature = "chrono")]
    #[test]
    fn into_timestamp_chrono() {
        use chrono::{TimeZone, Utc};
        let dt = Utc.timestamp_opt(1_704_067_200, 0).single().unwrap();
        assert_eq!(dt.into_timestamp_string(), "1704067200000000000");
    }

    #[test]
    fn timestamp_roundtrip_json() {
        let t = Timestamp::from("2024-01-01T00:00:00Z");
        let s = serde_json::to_string(&t).unwrap();
        assert_eq!(s, "\"2024-01-01T00:00:00Z\"");
        let back: Timestamp = serde_json::from_str(&s).unwrap();
        assert_eq!(back, t);
    }

    #[test]
    fn timestamp_deserializes_preserves_format() {
        // Server may send any RFC 3339 variant; we hold it unchanged.
        let t: Timestamp = serde_json::from_str("\"2024-01-01T00:00:00.123+02:00\"").unwrap();
        assert_eq!(t.as_str(), "2024-01-01T00:00:00.123+02:00");
    }

    #[test]
    fn timestamp_debug_prints_as_string() {
        let t = Timestamp::from("2024-01-01T00:00:00Z");
        assert_eq!(format!("{t:?}"), "\"2024-01-01T00:00:00Z\"");
        assert_eq!(format!("{t}"), "2024-01-01T00:00:00Z");
    }

    #[cfg(feature = "chrono")]
    #[test]
    fn timestamp_to_chrono_normalizes_offset_to_utc() {
        // +02:00 offset → same instant expressed as UTC.
        let t = Timestamp::from("2024-01-01T02:00:00+02:00");
        let dt = t.to_chrono().unwrap();
        assert_eq!(dt.to_rfc3339(), "2024-01-01T00:00:00+00:00");
    }

    #[cfg(feature = "chrono")]
    #[test]
    fn timestamp_to_chrono_surfaces_parse_error() {
        let t = Timestamp::from("not a timestamp");
        assert!(t.to_chrono().is_err());
    }

    #[cfg(feature = "time")]
    #[test]
    fn timestamp_to_time_parses_rfc3339() {
        let t = Timestamp::from("2024-01-01T00:00:00Z");
        let dt = t.to_time().unwrap();
        assert_eq!(dt.unix_timestamp(), 1_704_067_200);
    }

    #[cfg(feature = "time")]
    #[test]
    fn timestamp_to_time_surfaces_parse_error() {
        let t = Timestamp::from("not a timestamp");
        assert!(t.to_time().is_err());
    }
}
