use crate::{Dt, DtErr, DtErrKind, Lang, LiteStr, STRTIME_SIZE, YmdHms, an_err};

#[cfg(feature = "alloc")]
use {
    crate::ATTOS_PER_SEC_U128,
    alloc::string::{String, ToString},
};

#[cfg(not(any(feature = "jiff-tz-bundle", feature = "jiff-tz")))]
use crate::tz::UTC_ALIASES;

#[allow(clippy::unwrap_used)]
#[cfg(feature = "alloc")]
impl Dt {
    /// Converts this `Dt` to an ISO 8601 duration string.
    ///
    /// - Example: **"PT1H23M45.6789S"**.
    /// - Requires the `alloc` feature.
    /// - Does **not** do any time scale conversions prior to output.
    pub fn to_iso_duration(&self) -> String {
        if self.is_zero() {
            return String::from("PT0S");
        }

        let total = self.to_attos();
        let negative = total < 0;
        let mut attos = total.unsigned_abs();

        let mut s = String::with_capacity(48);
        if negative {
            s.push('-');
        }
        s.push_str("PT");

        const A_PER_M: u128 = ATTOS_PER_SEC_U128 * 60;
        const A_PER_H: u128 = A_PER_M * 60;

        let hours = attos / A_PER_H;
        attos %= A_PER_H;
        let minutes = attos / A_PER_M;
        attos %= A_PER_M;
        let seconds = attos / ATTOS_PER_SEC_U128;
        let frac_attos = attos % ATTOS_PER_SEC_U128;

        if hours > 0 {
            s.push_str(&alloc::format!("{}", hours));
            s.push('H');
        }
        if minutes > 0 {
            s.push_str(&alloc::format!("{}", minutes));
            s.push('M');
        }

        if seconds > 0 || frac_attos > 0 {
            s.push_str(&alloc::format!("{}", seconds));

            if frac_attos != 0 {
                let frac_str = alloc::format!("{frac_attos:018}");
                let trimmed = frac_str.trim_end_matches('0');
                s.push('.');
                s.push_str(trimmed);
            }

            s.push('S');
        }

        s
    }

    /// Formats this [`Dt`] into a String. Requires the `alloc` feature.
    ///
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    ///
    /// ## Examples
    ///
    /// ```rust
    /// use deep_time::{Dt, Lang, Scale};
    ///
    /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
    /// let s = x.to_str("%F", Lang::En).unwrap();
    ///
    /// println!("{}", s);
    /// ```
    ///
    /// ## Errors
    ///
    /// Returns [`DtErr`] if the format string contains invalid specifiers
    /// or if the internal formatting buffer overflows (extremely unlikely
    /// with [`STRTIME_SIZE`]).
    ///
    /// ## See also
    ///
    /// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset)
    /// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz)
    #[inline(always)]
    pub fn to_str(&self, fmt: &str, lang: Lang) -> Result<String, DtErr> {
        self.to_str_in_offset(fmt, 0, lang)
    }

    /// Formats this [`Dt`] into a String, applying a fixed offset. Requires the
    /// `"alloc"` feature.
    ///
    /// - A copy of the [`Dt`] is adjusted by the given `secs` offset **before**
    ///   formatting, and the offset is stored so that `%z` / `%:z` format directives
    ///   will reflect it.
    /// - No IANA timezone name or abbreviation is set.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    ///
    /// ## Examples
    ///
    /// ```rust
    /// use deep_time::{Dt, Lang, Scale};
    ///
    /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
    ///
    /// // offset of minus one hour
    /// let s = x.to_str_in_offset("%F", -3600, Lang::En).unwrap();
    ///
    /// println!("{}", s);
    /// ```
    ///
    /// ## Errors
    ///
    /// Returns [`DtErr`] if the format string contains invalid specifiers
    /// or if the internal formatting buffer overflows (extremely unlikely
    /// with [`STRTIME_SIZE`]).
    ///
    /// ## See also
    ///
    /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
    /// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz)
    #[inline(always)]
    pub fn to_str_in_offset(&self, fmt: &str, secs: i32, lang: Lang) -> Result<String, DtErr> {
        self.ymd_with_offset(secs)
            .to_str(fmt, Some(secs), None, None, lang)
    }

    /// Formats this [`Dt`] into a string, time adjusted to the given IANA timezone.
    ///
    /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
    ///
    /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]'s time for the given
    ///   IANA timezone. This is so that the formatter will have:
    ///     - Accurate wall time for the timezone.
    ///     - Correct numeric offset (for `%z` / `%:z`).
    ///     - Timezone abbreviation (for `%Z`). These **do not** round-trip (the parser
    ///       does not parse them).
    ///     - Full IANA timezone name (for `%Q` / `%:Q`).
    /// - Converts to the provided timezone, if your [`Dt`] is already in
    ///   the timezone then use the label function instead:
    ///   [`Dt::to_str_with_tz_label`](../struct.Dt.html#method.to_str_with_tz_label).
    ///   This is unlikely to be case because when a date with a timezone is parsed
    ///   the returned [`Dt`] is not in local time. But, label only functions are
    ///   provided just in case anyway.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    /// - Requires the `jiff-tz` feature for timezone names other than UTC aliases.
    ///
    /// ## Examples
    ///
    /// You can offset an output that wasn't originally from a zoned input:
    ///
    /// ```rust
    /// # #[cfg(all(any(feature = "jiff-tz", feature = "jiff-tz-bundle"), feature = "parse"))]
    /// # {
    /// use deep_time::{Dt, Lang, Scale};
    ///
    /// let x: Dt = "2000-01-01 12:00:00".parse().unwrap();
    /// let s = x.to_str_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En).unwrap();
    /// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
    /// # }
    /// ```
    ///
    /// You can also return to a zoned output from a zoned input:
    ///
    /// ```rust
    /// # #[cfg(all(any(feature = "jiff-tz", feature = "jiff-tz-bundle"), feature = "parse"))]
    /// # {
    /// use deep_time::{Dt, Lang, Scale};
    ///
    /// let x: Dt = "Saturday, January 01, 2000 07:00:00 America/New_York".parse().unwrap();
    /// let s = x.to_str_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En).unwrap();
    /// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
    /// # }
    /// ```
    ///
    /// ## Errors
    ///
    /// Returns [`DtErr`] if the format string contains invalid specifiers
    /// or if the internal formatting buffer overflows (extremely unlikely
    /// with [`STRTIME_SIZE`]).
    ///
    /// ## See also
    ///
    /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
    /// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset)
    #[inline(always)]
    pub fn to_str_in_tz(&self, fmt: &str, tz_name: &str, lang: Lang) -> Result<String, DtErr> {
        let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, true)?;
        ymd.to_str(
            fmt,
            Some(offset),
            Some(LiteStr::new(tz_name)),
            Some(abbrev),
            lang,
        )
    }

    /// **RFC 9557** / Temporal format with IANA timezone name in brackets.
    ///
    /// - Example: **`"2020-06-15T14:30:00-04:00[America/New_York]"`**.
    /// - Converts to the provided timezone, if your [`Dt`] is already in
    ///   the timezone then use the label function instead:
    ///   [`Dt::to_str_with_tz_label`](../struct.Dt.html#method.to_str_with_tz_label).
    ///   This is unlikely to be case because when a date with a timezone is parsed
    ///   the returned [`Dt`] is not in local time. But, label only functions are
    ///   provided just in case anyway.
    /// - Automatically trims trailing zeros in the fractional part.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    #[inline(always)]
    pub fn to_str_rfc9557(&self, tz_name: &str) -> Result<String, DtErr> {
        self.to_str_in_tz("%Y-%m-%dT%H:%M:%S%.~f%:z[%Q]", tz_name, Lang::En)
    }

    /// Returns this instant as an **RFC 3339** / ISO 8601 timestamp with a
    /// `Z` suffix.
    ///
    /// - Example: **`"2024-03-14T15:30:45.123Z"`**
    /// - Default = 9 digits (nanoseconds) but **automatically trims trailing zeros**.
    /// - If fractional part is zero → no decimal point at all (e.g. `...45Z`).
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    #[inline(always)]
    pub fn to_str_rfc3339(&self) -> String {
        self.to_str_rfc3339_nf(9)
    }

    /// Same as [`Dt::to_str_rfc3339`](../struct.Dt.html#method.to_str_rfc3339) but
    /// with a configurable maximum number of fractional digits (0–18). Trailing zeros are
    /// always trimmed.
    ///
    /// - Example: **`"2024-03-14T15:30:45.123Z"`**
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    pub fn to_str_rfc3339_nf(&self, max_precision: usize) -> String {
        let prec = max_precision.min(18);
        // Uses the formatter with the `~` "trim trailing zeros" flag.
        // The formatter already handles:
        //   - correct 4-digit years (with sign) for |yr| < 10000
        //   - full-width years otherwise
        //   - suppressing the decimal point entirely when the trimmed fraction is zero
        let fmt = alloc::format!("%Y-%m-%dT%H:%M:%S%.{}~fZ", prec);
        self.to_str_in_offset(&fmt, 0, Lang::En).unwrap()
    }

    /// **ISO 8601 / RFC 3339** with **actual offset** (modern `+00:00` style).
    ///
    /// - Example: **`"2025-04-16T14:30:45.123+00:00"`**.
    /// - Uses colon-separated offset (`%:z`) instead of forcing `Z`.
    /// - Still trims trailing zeros in the fractional part.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    #[inline(always)]
    pub fn to_str_iso8601(&self) -> String {
        self.to_str_in_offset("%Y-%m-%dT%H:%M:%S%.~f%:z", 0, Lang::En)
            .unwrap()
    }

    /// **Compact ISO 8601 basic format** (no separators).
    ///
    /// - Example: **`"20250416T143045.123456789Z"`**.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    #[inline(always)]
    pub fn to_str_iso8601_basic(&self) -> String {
        self.to_str_in_offset("%Y%m%dT%H%M%S%.~fZ", 0, Lang::En)
            .unwrap()
    }

    /// **ISO 8601 week date**.
    ///
    /// - Example: **`"2025-W16-3"`**. (year-week-day)
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    #[inline(always)]
    pub fn to_str_iso_week_date(&self) -> String {
        self.to_str_in_offset("%G-W%V-%u", 0, Lang::En).unwrap()
    }

    /// Just the **ISO date** part (no time).
    ///
    /// - Example: **`"2025-04-16"`**.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    #[inline(always)]
    pub fn to_str_iso_date(&self) -> String {
        self.to_str_in_offset("%Y-%m-%d", 0, Lang::En).unwrap()
    }

    /// Just the **time** part with fractional seconds (trimmed).
    ///
    /// - Example: **`"14:30:45.123456789"`**.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    #[inline(always)]
    pub fn to_str_iso_time(&self) -> String {
        self.to_str_in_offset("%H:%M:%S%.~f", 0, Lang::En).unwrap()
    }

    /// **HTTP-date** format (RFC 7231 / RFC 1123) — **always in GMT**.
    ///
    /// - Example: **`"Wed, 16 Apr 2025 14:30:45 GMT"`**.
    /// - Always outputs in GMT (equivalent to UTC+00:00). Does not apply
    ///   regional DST rules.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    #[inline(always)]
    pub fn to_str_http(&self, lang: Lang) -> String {
        self.to_str_in_offset("%a, %d %b %Y %H:%M:%S GMT", 0, lang)
            .unwrap()
    }

    /// **RFC 2822** date format (used in email `Date` headers).
    ///
    /// - Example: **`"Wed, 16 Apr 2025 14:30:45 +0000"`**.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    #[inline(always)]
    pub fn to_str_rfc2822(&self, lang: Lang) -> String {
        self.to_str_in_offset("%a, %d %b %Y %H:%M:%S %z", 0, lang)
            .unwrap()
    }

    /// Formats this [`Dt`] into a `String`, attaching an offset **as a label only**.
    ///
    /// - The actual datetime components are **not** shifted or adjusted.
    /// - The given `offset` is used **only** for `%z` / `%:z` format directives.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    ///
    /// ## Errors
    ///
    /// Returns [`DtErr`] if the format string contains invalid specifiers
    /// or if the internal formatting buffer overflows (extremely unlikely
    /// with [`STRTIME_SIZE`]).
    ///
    /// ## See also
    ///
    /// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset) —
    ///   shifts the datetime by the offset
    #[inline(always)]
    pub fn to_str_with_offset_label(
        &self,
        fmt: &str,
        offset: i32,
        lang: Lang,
    ) -> Result<String, DtErr> {
        self.to_ymd().to_str(fmt, Some(offset), None, None, lang)
    }

    /// Formats this [`Dt`] into a `String`, attaching a timezone **as a label only**.
    ///
    /// - The actual datetime components are **not** shifted or adjusted.
    /// - The timezone is used to provide correct values for `%z`, `%:z`, `%Z`, `%Q`, and `%:Q`.
    /// - The timezone abbreviation is automatically looked up from tzdata.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    /// - Requires the `jiff-tz` feature for timezone names other than UTC aliases.
    ///
    /// ## Errors
    ///
    /// Returns [`DtErr`] if the format string contains invalid specifiers,
    /// if the timezone name is invalid, or if the internal formatting buffer
    /// overflows (extremely unlikely with [`STRTIME_SIZE`]).
    ///
    /// ## See also
    ///
    /// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz) —
    ///   shifts the datetime into the given timezone
    #[inline(always)]
    pub fn to_str_with_tz_label(
        &self,
        fmt: &str,
        tz_name: &str,
        lang: Lang,
    ) -> Result<String, DtErr> {
        let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, false)?;
        ymd.to_str(
            fmt,
            Some(offset),
            Some(LiteStr::new(tz_name)),
            Some(abbrev),
            lang,
        )
    }
}

impl Dt {
    /// Formats this [`Dt`] into a fixed-size binary string.
    ///
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    ///
    /// ## Examples
    ///
    /// ```rust
    /// use deep_time::{Dt, Lang, Scale};
    ///
    /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
    /// let b = x.to_str_lite("%F", Lang::En).unwrap();
    /// let s = b.as_str();
    ///
    /// println!("{}", s);
    /// ```
    ///
    /// ## Errors
    ///
    /// Returns [`DtErr`] if the format string contains invalid specifiers
    /// or if the internal formatting buffer overflows (extremely unlikely
    /// with [`STRTIME_SIZE`]).
    ///
    /// ## See also
    ///
    /// - [`Dt::to_str_lite_in_offset`](../struct.Dt.html#method.to_str_lite_in_offset)
    /// - [`Dt::to_str_lite_in_tz`](../struct.Dt.html#method.to_str_lite_in_tz)
    #[inline(always)]
    pub fn to_str_lite(&self, fmt: &str, lang: Lang) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
        self.to_ymd().to_str_lite(fmt, None, None, None, lang)
    }

    /// Formats this [`Dt`] into a fixed-size binary string, applying a fixed UTC offset.
    ///
    /// - A copy of the [`Dt`] is adjusted by the given `secs` offset **before**
    ///   formatting, and the offset is stored so that `%z` / `%:z` format directives
    ///   will reflect it.
    /// - No IANA timezone name or abbreviation is set.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    ///
    /// ## Examples
    ///
    /// ```rust
    /// use deep_time::{Dt, Lang, Scale};
    ///
    /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
    ///
    /// // offset of minus one hour
    /// let b = x.to_str_lite_in_offset("%F", -3600, Lang::En).unwrap();
    /// let s = b.as_str();
    ///
    /// println!("{}", s);
    /// ```
    ///
    /// ## Errors
    ///
    /// Returns [`DtErr`] if the format string contains invalid specifiers
    /// or if the internal formatting buffer overflows (extremely unlikely
    /// with [`STRTIME_SIZE`]).
    ///
    /// ## See also
    ///
    /// - [`Dt::to_str_lite`](../struct.Dt.html#method.to_str_lite)
    /// - [`Dt::to_str_lite_in_tz`](../struct.Dt.html#method.to_str_lite_in_tz)
    #[inline(always)]
    pub fn to_str_lite_in_offset(
        &self,
        fmt: &str,
        secs: i32,
        lang: Lang,
    ) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
        self.ymd_with_offset(secs)
            .to_str_lite(fmt, Some(secs), None, None, lang)
    }

    /// Formats this [`Dt`] into a fixed-size binary string, time adjusted to the given
    /// IANA timezone.
    ///
    /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
    ///
    /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]'s time for the given
    ///   IANA timezone. This is so that the formatter will have:
    ///     - Accurate wall time for the timezone.
    ///     - Correct numeric offset (for `%z` / `%:z`).
    ///     - Timezone abbreviation (for `%Z`). These **do not** round-trip.
    ///     - Full IANA timezone name (for `%Q` / `%:Q`).
    /// - Converts to the provided timezone, if your [`Dt`] is already in
    ///   the timezone then use the label function instead:
    ///   [`Dt::to_str_lite_with_tz_label`](../struct.Dt.html#method.to_str_lite_with_tz_label).
    ///   This is unlikely to be case because when a date with a timezone is parsed
    ///   the returned [`Dt`] is not in local time. But, label only functions are
    ///   provided just in case anyway.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    /// - Requires the `jiff-tz` feature for timezone names other than UTC aliases.
    ///
    /// ## Examples
    ///
    /// ```rust
    /// # #[cfg(any(feature = "jiff-tz-bundle", feature = "jiff-tz"))]
    /// # {
    /// use deep_time::{Dt, Lang, Scale};
    ///
    /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
    ///
    /// let b = x.to_str_lite_in_tz("%F", "America/New_York", Lang::En).unwrap();
    /// let s = b.as_str();
    ///
    /// println!("{}", s);
    /// # }
    /// ```
    ///
    /// ## Errors
    ///
    /// Returns [`DtErr`] if the format string contains invalid specifiers
    /// or if the internal formatting buffer overflows (extremely unlikely
    /// with [`STRTIME_SIZE`]).
    ///
    /// ## See also
    ///
    /// - [`Dt::to_str_lite`](../struct.Dt.html#method.to_str_lite)
    /// - [`Dt::to_str_lite_in_offset`](../struct.Dt.html#method.to_str_lite_in_offset)
    /// - [`Dt::to_str_lite_with_tz_label`](../struct.Dt.html#method.to_str_lite_with_tz_label)
    #[inline(always)]
    pub fn to_str_lite_in_tz(
        &self,
        fmt: &str,
        tz_name: &str,
        lang: Lang,
    ) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
        let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, true)?;
        ymd.to_str_lite(
            fmt,
            Some(offset),
            Some(LiteStr::new(tz_name)),
            Some(abbrev),
            lang,
        )
    }

    /// Formats this [`Dt`] into a `LiteStr`, attaching an offset **as a label only**.
    ///
    /// - The actual datetime components are **not** shifted or adjusted.
    /// - The given `offset` is used **only** for `%z` / `%:z` format directives.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    ///
    /// ## Errors
    ///
    /// Returns [`DtErr`] if the format string contains invalid specifiers
    /// or if the internal formatting buffer overflows (extremely unlikely
    /// with [`STRTIME_SIZE`]).
    ///
    /// ## See also
    ///
    /// - [`Dt::to_str_lite_in_offset`](../struct.Dt.html#method.to_str_lite_in_offset) —
    ///   shifts the datetime by the offset
    #[inline(always)]
    pub fn to_str_lite_with_offset_label(
        &self,
        fmt: &str,
        offset: i32,
        lang: Lang,
    ) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
        self.to_ymd()
            .to_str_lite(fmt, Some(offset), None, None, lang)
    }

    /// Formats this [`Dt`] into a `LiteStr`, attaching a timezone **as a label only**.
    ///
    /// - The actual datetime components are **not** shifted or adjusted.
    /// - The timezone is used to provide correct values for `%z`, `%:z`, `%Z`, `%Q`, and `%:Q`.
    /// - The timezone abbreviation is automatically looked up from tzdata.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    /// - Requires the `jiff-tz` feature for timezone names other than UTC aliases.
    ///
    /// ## Errors
    ///
    /// Returns [`DtErr`] if the format string contains invalid specifiers,
    /// if the timezone name is invalid, or if the internal formatting buffer
    /// overflows (extremely unlikely with [`STRTIME_SIZE`]).
    ///
    /// ## See also
    ///
    /// - [`Dt::to_str_lite_in_tz`](../struct.Dt.html#method.to_str_lite_in_tz) —
    ///   shifts the datetime into the given timezone
    #[inline(always)]
    pub fn to_str_lite_with_tz_label(
        &self,
        fmt: &str,
        tz_name: &str,
        lang: Lang,
    ) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
        let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, false)?;
        ymd.to_str_lite(
            fmt,
            Some(offset),
            Some(LiteStr::new(tz_name)),
            Some(abbrev),
            lang,
        )
    }

    /// **ISO 8601 / RFC 3339** with **actual offset** (modern `+00:00` style)
    /// as a fixed size no-alloc binary string.
    ///
    /// - Example: **`"2025-04-16T14:30:45.123+00:00"`**.
    /// - Uses colon-separated offset (`%:z`) instead of forcing `Z`.
    /// - Trims trailing zeros in the fractional part.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    #[inline(always)]
    pub fn to_str_lite_iso8601(&self) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
        self.to_str_lite_in_offset("%Y-%m-%dT%H:%M:%S%.~f%:z", 0, Lang::En)
    }

    /// **RFC 9557** / Temporal format with IANA timezone name in brackets
    /// as a fixed size no-alloc binary string.
    ///
    /// - Example: **`"2020-06-15T14:30:00-04:00[America/New_York]"`**.
    /// - Automatically trims trailing zeros in the fractional part.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    #[inline(always)]
    pub fn to_str_lite_rfc9557(&self, tz_name: &str) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
        self.to_str_lite_in_tz("%Y-%m-%dT%H:%M:%S%.~f%:z[%Q]", tz_name, Lang::En)
    }

    /// **HTTP-date** format (RFC 7231 / RFC 1123) — **always in GMT**
    /// as a fixed size no-alloc binary string.
    ///
    /// - Example: **`"Wed, 16 Apr 2025 14:30:45 GMT"`**.
    /// - Always outputs in GMT (equivalent to UTC+00:00). Does not apply
    ///   regional DST rules.
    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
    ///   time scale before producing the result.
    #[inline(always)]
    pub fn to_str_lite_http(&self, lang: Lang) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
        self.to_str_lite_in_offset("%a, %d %b %Y %H:%M:%S GMT", 0, lang)
    }

    /// Returns `(is_negative, hours, minutes)`.
    #[inline]
    pub(crate) const fn sec_as_hhmm(seconds: i32) -> (bool, u8, u8) {
        let total = seconds.saturating_abs();
        let hours = (total / 3600) as u8;
        let minutes = ((total % 3600) / 60) as u8;
        (seconds < 0, hours, minutes)
    }

    #[inline(always)]
    pub(crate) fn ymd_with_offset(&self, secs: i32) -> YmdHms {
        if secs != 0 {
            self.add_sec(secs as i128).to_ymd()
        } else {
            self.to_ymd()
        }
    }

    pub(crate) fn ymd_with_tz(
        &self,
        tz_name: &str,
        apply_offset: bool,
    ) -> Result<(YmdHms, i32, LiteStr<49>), DtErr> {
        #[cfg(any(feature = "jiff-tz-bundle", feature = "jiff-tz"))]
        let (offset_secs, abbrev): (i32, LiteStr<49>) = {
            use jiff::{Timestamp, tz::TimeZone};

            let tz =
                TimeZone::get(tz_name).map_err(|e| an_err!(DtErrKind::InvalidTimeZone, "{}", e))?;

            let unix_sec = self.to_unix().to_sec64();

            let ts = Timestamp::from_second(unix_sec)
                .map_err(|e| an_err!(DtErrKind::InvalidTimestamp, "{}", e))?;

            let info = tz.to_offset_info(ts);
            let offset_secs = info.offset().seconds();
            let abbrev: LiteStr<49> = LiteStr::new(info.abbreviation());

            (offset_secs, abbrev)
        };

        #[cfg(not(any(feature = "jiff-tz-bundle", feature = "jiff-tz")))]
        let (offset_secs, abbrev): (i32, LiteStr<49>) = {
            if !UTC_ALIASES.contains(&tz_name) {
                return Err(an_err!(DtErrKind::MissingFeature));
            }
            // UTC → offset 0, canonical abbrev "UTC"
            let abbrev: LiteStr<49> = LiteStr::new("UTC");
            (0i32, abbrev)
        };

        let ymd = if offset_secs != 0 && apply_offset {
            self.add_sec(offset_secs as i128).to_ymd()
        } else {
            self.to_ymd()
        };

        Ok((ymd, offset_secs, abbrev))
    }
}

impl Dt {
    /// Formats the duration using the common media/video player style
    /// (e.g. `"0:45"`, `"9:41"`, `"1:23:45"`, `"1:07:54:30"`).
    #[cfg(feature = "alloc")]
    #[inline(always)]
    pub fn to_str_media_duration(&self) -> String {
        self.to_str_lite_media_duration().to_string()
    }

    /// Formats the duration using the common media/video player style
    /// (e.g. `"0:45"`, `"9:41"`, `"1:23:45"`, `"1:07:54:30"`).
    #[inline(always)]
    pub fn to_str_lite_media_duration(&self) -> LiteStr<STRTIME_SIZE> {
        let (buf, len) = self.format_media_duration();
        LiteStr::from_bytes(&buf[..len])
    }

    /// Returns a stack buffer + the number of valid bytes written.
    fn format_media_duration(&self) -> ([u8; 64], usize) {
        let mut buf = [0u8; 64];
        let mut pos = 0;

        if self.is_zero() {
            buf[0] = b'0';
            buf[1] = b':';
            buf[2] = b'0';
            buf[3] = b'0';
            return (buf, 4);
        }

        let negative = self.attos < 0;
        let total = self.to_sec_rounded().unsigned_abs();

        if negative {
            buf[pos] = b'-';
            pos += 1;
        }

        let days = total / 86400;
        let rem = total % 86400;
        let hours = rem / 3600;
        let rem = rem % 3600;
        let mins = rem / 60;
        let secs = rem % 60;

        if days > 0 {
            pos += write_u128(&mut buf[pos..], days);
            buf[pos] = b':';
            pos += 1;
            pos += write_u128_padded(&mut buf[pos..], hours);
            buf[pos] = b':';
            pos += 1;
            pos += write_u128_padded(&mut buf[pos..], mins);
            buf[pos] = b':';
            pos += 1;
            pos += write_u128_padded(&mut buf[pos..], secs);
        } else if hours > 0 {
            pos += write_u128(&mut buf[pos..], hours);
            buf[pos] = b':';
            pos += 1;
            pos += write_u128_padded(&mut buf[pos..], mins);
            buf[pos] = b':';
            pos += 1;
            pos += write_u128_padded(&mut buf[pos..], secs);
        } else {
            pos += write_u128(&mut buf[pos..], mins);
            buf[pos] = b':';
            pos += 1;
            pos += write_u128_padded(&mut buf[pos..], secs);
        }

        (buf, pos)
    }
}

/// Write number with no leading zeros. Returns bytes written.
fn write_u128(buf: &mut [u8], mut n: u128) -> usize {
    if n == 0 {
        buf[0] = b'0';
        return 1;
    }
    let mut i = buf.len();
    while n > 0 {
        i -= 1;
        buf[i] = b'0' + (n % 10) as u8;
        n /= 10;
    }
    let len = buf.len() - i;
    buf.copy_within(i.., 0);
    len
}

/// Write number padded to exactly 2 digits (assumes n < 100).
fn write_u128_padded(buf: &mut [u8], n: u128) -> usize {
    buf[0] = b'0' + ((n / 10) % 10) as u8;
    buf[1] = b'0' + (n % 10) as u8;
    2
}
