//! Timestamp formatting for `SigV4` (`YYYYMMDDTHHMMSSZ` and `YYYYMMDD`).

use exn::ResultExt as _;
use time::{OffsetDateTime, format_description::FormatItem, macros::format_description};

// ────────────────────────────────────────────────────────────────────────────
// Time helpers
// ────────────────────────────────────────────────────────────────────────────

/// Format for the `x-amz-date` header value: `YYYYMMDDTHHMMSSZ`.
///
/// Must be UTC (`Z` suffix); `SigV4` rejects timezone offsets.  The `T` and
/// `Z` literals are outside the `[…]` tokens so `time` emits them verbatim.
const FMT_AMZDATE: &[FormatItem<'_>] =
    format_description!("[year][month][day]T[hour][minute][second]Z");
/// Format for the credential-scope date component: `YYYYMMDD`.
const FMT_DATESTAMP: &[FormatItem<'_>] = format_description!("[year][month][day]");

/// Errors returned by [`timestamps`]; practically unreachable in normal use.
///
/// The `time` crate only errors when a format description references a field
/// unavailable on the given type; both `FMT_AMZDATE` and `FMT_DATESTAMP` use
/// only `[year][month][day]…` tokens, which are always present on
/// `OffsetDateTime`.
#[derive(Clone, Copy, Debug, derive_more::Display)]
pub enum TimestampError {
    /// Formatting `x-amz-date` failed.
    #[display("Error formatting to date")]
    Date,
    /// Formatting the credential-scope datestamp failed.
    #[display("Error formatting to date stamp")]
    DateStamp,
}
impl std::error::Error for TimestampError {}

#[cfg_attr(
    feature = "tracing",
    tracing::instrument(level = "trace", skip(ts), ret, err(level = "debug"))
)]
/// Formats `ts` into the two timestamp strings required by `SigV4`.
///
/// Returns `(amzdate, datestamp)` co-derived from the same instant so that
/// `x-amz-date` and the credential-scope date component always agree.  The
/// caller must pass a UTC `OffsetDateTime`; a non-zero offset would produce a
/// wrong `Z`-terminated string.
pub fn timestamps(ts: OffsetDateTime) -> exn::Result<(String, String), TimestampError> {
    let amzdate = ts.format(FMT_AMZDATE).or_raise(|| TimestampError::Date)?;
    let datestamp = ts
        .format(FMT_DATESTAMP)
        .or_raise(|| TimestampError::DateStamp)?;
    Ok((amzdate, datestamp))
}

#[cfg(test)]
#[allow(clippy::expect_used, reason = "test code")]
mod tests {
    use time::macros::datetime;

    use super::*;

    #[test]
    fn timestamps_reference_aws_vector() {
        let (amz, date) = timestamps(datetime!(2015-08-30 12:36:00 UTC))
            .expect("timestamps should format successfully");
        assert_eq!(amz, "20150830T123600Z");
        assert_eq!(date, "20150830");
    }
}
