// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

#[cfg(feature = "unstable")]
use crate::ZonedTime;
#[cfg(feature = "unstable")]
use crate::zone::ZoneNameTimestamp;
use crate::{
    DateTime, Time, TimeZoneInfo, ZonedDateTime,
    zone::{InvalidOffsetError, UtcOffset, iana::IanaParserBorrowed, models},
};
use core::str::FromStr;
use icu_calendar::{AnyCalendarKind, AsCalendar, Date, DateError, Iso, RangeError};
use ixdtf::{
    ParseError as Rfc9557ParseError,
    encoding::Utf8,
    parsers::IxdtfParser,
    records::{
        IxdtfParseRecord, TimeZoneAnnotation, TimeZoneRecord, UtcOffsetRecord, UtcOffsetRecordOrZ,
    },
};

/// The error type for parsing RFC 9557 strings.
#[derive(Debug, PartialEq, displaydoc::Display)]
#[non_exhaustive]
pub enum ParseError {
    /// Syntax error.
    #[displaydoc("Syntax error in the RFC 9557 string: {0}")]
    Syntax(Rfc9557ParseError),
    /// Parsed record is out of valid date range.
    #[displaydoc("Value out of range: {0}")]
    Range(RangeError),
    /// Parsed date and time records were not a valid ISO date.
    #[displaydoc("Parsed date and time records were not a valid ISO date: {0}")]
    Date(DateError),
    /// There were missing fields required to parse component.
    MissingFields,
    /// There were two offsets provided that were not consistent with each other.
    InconsistentTimeUtcOffsets,
    /// There was an invalid Offset.
    InvalidOffsetError,
    /// Parsed fractional digits had excessive precision beyond nanosecond.
    ExcessivePrecision,
    /// The set of time zone fields was not expected for the given type.
    /// For example, if a named time zone was present with offset-only parsing,
    /// or an offset was present with named-time-zone-only parsing.
    #[displaydoc("The set of time zone fields was not expected for the given type")]
    MismatchedTimeZoneFields,
    /// An unknown calendar was provided.
    UnknownCalendar,
    /// Expected a different calendar.
    #[displaydoc("Expected calendar {0} but found calendar {1}")]
    MismatchedCalendar(AnyCalendarKind, AnyCalendarKind),
    /// A timezone calculation is required to interpret this string, which is not supported.
    ///
    /// # Example
    /// ```
    /// use icu::calendar::Iso;
    /// use icu::time::{ZonedDateTime, ParseError, zone::IanaParser};
    ///
    /// // This timestamp is in UTC, and requires a time zone calculation in order to display a Zurich time.
    /// assert_eq!(
    ///     ZonedDateTime::try_lenient_from_str("2024-08-12T12:32:00Z[Europe/Zurich]", Iso, IanaParser::new()).unwrap_err(),
    ///     ParseError::RequiresCalculation,
    /// );
    ///
    /// // These timestamps are in local time
    /// ZonedDateTime::try_lenient_from_str("2024-08-12T14:32:00+02:00[Europe/Zurich]", Iso, IanaParser::new()).unwrap();
    /// ZonedDateTime::try_lenient_from_str("2024-08-12T14:32:00[Europe/Zurich]", Iso, IanaParser::new()).unwrap();
    /// ```
    #[displaydoc(
        "A timezone calculation is required to interpret this string, which is not supported"
    )]
    RequiresCalculation,
}

impl core::error::Error for ParseError {}

impl From<Rfc9557ParseError> for ParseError {
    fn from(value: Rfc9557ParseError) -> Self {
        Self::Syntax(value)
    }
}

impl From<RangeError> for ParseError {
    fn from(value: RangeError) -> Self {
        Self::Range(value)
    }
}

impl From<DateError> for ParseError {
    fn from(value: DateError) -> Self {
        Self::Date(value)
    }
}

impl From<InvalidOffsetError> for ParseError {
    fn from(_: InvalidOffsetError) -> Self {
        Self::InvalidOffsetError
    }
}

impl From<icu_calendar::ParseError> for ParseError {
    fn from(value: icu_calendar::ParseError) -> Self {
        match value {
            icu_calendar::ParseError::MissingFields => Self::MissingFields,
            icu_calendar::ParseError::Range(r) => Self::Range(r),
            icu_calendar::ParseError::Syntax(s) => Self::Syntax(s),
            icu_calendar::ParseError::UnknownCalendar => Self::UnknownCalendar,
            icu_calendar::ParseError::MismatchedCalendar(a, b) => Self::MismatchedCalendar(
                a.try_into().unwrap_or(AnyCalendarKind::Iso),
                b.try_into().unwrap_or(AnyCalendarKind::Iso),
            ),
            _ => unreachable!(),
        }
    }
}

impl UtcOffset {
    fn try_from_utc_offset_record(record: UtcOffsetRecord) -> Result<Self, ParseError> {
        let hour_seconds = i32::from(record.hour()) * 3600;
        let minute_seconds = i32::from(record.minute()) * 60;
        Self::try_from_seconds(
            i32::from(record.sign() as i8)
                * (hour_seconds + minute_seconds + i32::from(record.second().unwrap_or(0))),
        )
        .map_err(Into::into)
    }
}

struct Intermediate<'a> {
    offset: Option<UtcOffsetRecord>,
    is_z: bool,
    iana_identifier: Option<&'a [u8]>,
}

impl<'a> Intermediate<'a> {
    fn try_from_ixdtf_record(
        ixdtf_record: &'a IxdtfParseRecord<'_, Utf8>,
    ) -> Result<Self, ParseError> {
        let (offset, is_z, iana_identifier) = match ixdtf_record {
            // empty
            IxdtfParseRecord {
                offset: None,
                tz: None,
                ..
            } => (None, false, None),
            // -0800
            IxdtfParseRecord {
                offset: Some(UtcOffsetRecordOrZ::Offset(offset)),
                tz: None,
                ..
            } => (Some(*offset), false, None),
            // Z
            IxdtfParseRecord {
                offset: Some(UtcOffsetRecordOrZ::Z),
                tz: None,
                ..
            } => (None, true, None),
            // [-0800]
            IxdtfParseRecord {
                offset: None,
                tz:
                    Some(TimeZoneAnnotation {
                        tz: TimeZoneRecord::Offset(offset),
                        ..
                    }),
                ..
            } => (Some(UtcOffsetRecord::MinutePrecision(*offset)), false, None),
            // -0800[-0800]
            IxdtfParseRecord {
                offset: Some(UtcOffsetRecordOrZ::Offset(offset)),
                tz:
                    Some(TimeZoneAnnotation {
                        tz: TimeZoneRecord::Offset(offset1),
                        ..
                    }),
                ..
            } => {
                let annotation_offset = UtcOffsetRecord::MinutePrecision(*offset1);
                if offset != &annotation_offset {
                    return Err(ParseError::InconsistentTimeUtcOffsets);
                }
                (Some(*offset), false, None)
            }
            // -0800[America/Los_Angeles]
            IxdtfParseRecord {
                offset: Some(UtcOffsetRecordOrZ::Offset(offset)),
                tz:
                    Some(TimeZoneAnnotation {
                        tz: TimeZoneRecord::Name(iana_identifier),
                        ..
                    }),
                ..
            } => (Some(*offset), false, Some(*iana_identifier)),
            // Z[-0800]
            IxdtfParseRecord {
                offset: Some(UtcOffsetRecordOrZ::Z),
                tz:
                    Some(TimeZoneAnnotation {
                        tz: TimeZoneRecord::Offset(offset),
                        ..
                    }),
                ..
            } => (Some(UtcOffsetRecord::MinutePrecision(*offset)), true, None),
            // Z[America/Los_Angeles]
            IxdtfParseRecord {
                offset: Some(UtcOffsetRecordOrZ::Z),
                tz:
                    Some(TimeZoneAnnotation {
                        tz: TimeZoneRecord::Name(iana_identifier),
                        ..
                    }),
                ..
            } => (None, true, Some(*iana_identifier)),
            // [America/Los_Angeles]
            IxdtfParseRecord {
                offset: None,
                tz:
                    Some(TimeZoneAnnotation {
                        tz: TimeZoneRecord::Name(iana_identifier),
                        ..
                    }),
                ..
            } => (None, false, Some(*iana_identifier)),
            // non_exhaustive match: maybe something like [u-tz=uslax] in the future
            IxdtfParseRecord {
                tz: Some(TimeZoneAnnotation { tz, .. }),
                ..
            } => {
                debug_assert!(false, "unexpected TimeZoneRecord: {tz:?}");
                (None, false, None)
            }
        };
        Ok(Self {
            offset,
            is_z,
            iana_identifier,
        })
    }

    fn offset_only(&self) -> Result<UtcOffset, ParseError> {
        let None = self.iana_identifier else {
            return Err(ParseError::MismatchedTimeZoneFields);
        };
        if self.is_z {
            if let Some(offset) = self.offset
                && offset != UtcOffsetRecord::zero()
            {
                return Err(ParseError::RequiresCalculation);
            }
            return Ok(UtcOffset::zero());
        }
        let Some(offset) = self.offset else {
            return Err(ParseError::MismatchedTimeZoneFields);
        };
        UtcOffset::try_from_utc_offset_record(offset)
    }

    fn location_only(
        self,
        iana_parser: IanaParserBorrowed<'_>,
    ) -> Result<TimeZoneInfo<models::Base>, ParseError> {
        let None = self.offset else {
            return Err(ParseError::MismatchedTimeZoneFields);
        };
        let Some(iana_identifier) = self.iana_identifier else {
            if self.is_z {
                return Err(ParseError::RequiresCalculation);
            }
            return Err(ParseError::MismatchedTimeZoneFields);
        };
        Ok(iana_parser
            .parse_from_utf8(iana_identifier)
            .with_offset(None))
    }

    fn lenient(
        self,
        iana_parser: IanaParserBorrowed<'_>,
    ) -> Result<TimeZoneInfo<models::Base>, ParseError> {
        let mut zone = match self.iana_identifier {
            Some(iana_identifier) => {
                if self.is_z {
                    return Err(ParseError::RequiresCalculation);
                }
                iana_parser
                    .parse_from_utf8(iana_identifier)
                    .with_offset(None)
            }
            None if self.is_z => TimeZoneInfo::utc(),
            None => TimeZoneInfo::unknown(),
        };

        if let Some(offset) = self.offset {
            let offset = UtcOffset::try_from_utc_offset_record(offset)?;
            if zone.offset().is_some_and(|i| i != offset) {
                return Err(ParseError::RequiresCalculation);
            }
            zone = zone.id().with_offset(Some(offset));
        }
        Ok(zone)
    }

    fn all(
        self,
        iana_parser: IanaParserBorrowed<'_>,
    ) -> Result<TimeZoneInfo<models::Base>, ParseError> {
        let Some(offset) = self.offset else {
            return Err(ParseError::MismatchedTimeZoneFields);
        };
        let Some(iana_identifier) = self.iana_identifier else {
            return Err(ParseError::MismatchedTimeZoneFields);
        };
        Ok(iana_parser
            .parse_from_utf8(iana_identifier)
            .with_offset(Some(UtcOffset::try_from_utc_offset_record(offset)?)))
    }
}

impl<A: AsCalendar> ZonedDateTime<A, UtcOffset> {
    /// Create a [`ZonedDateTime`] in any calendar from an RFC 9557 string.
    ///
    /// Returns an error if the string has a calendar annotation that does not
    /// match the calendar argument.
    ///
    /// This function is "strict": the string should have only an offset and no named time zone.
    pub fn try_offset_only_from_str(rfc_9557_str: &str, calendar: A) -> Result<Self, ParseError> {
        Self::try_offset_only_from_utf8(rfc_9557_str.as_bytes(), calendar)
    }

    /// Create a [`ZonedDateTime`] in any calendar from RFC 9557 syntax UTF-8 bytes.
    ///
    /// See [`Self:try_offset_only_from_str`](Self::try_offset_only_from_str).
    pub fn try_offset_only_from_utf8(rfc_9557_str: &[u8], calendar: A) -> Result<Self, ParseError> {
        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse()?;
        let date = Date::try_from_ixdtf_record(&ixdtf_record, calendar)?;
        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
        let zone = Intermediate::try_from_ixdtf_record(&ixdtf_record)?.offset_only()?;
        Ok(ZonedDateTime { date, time, zone })
    }
}

impl<A: AsCalendar> ZonedDateTime<A, TimeZoneInfo<models::AtTime>> {
    /// Create a [`ZonedDateTime`] in any calendar from an RFC 9557 string.
    ///
    /// Returns an error if the string has a calendar annotation that does not
    /// match the calendar argument.
    ///
    /// This function is "strict": the string should have only a named time zone and no offset.
    pub fn try_location_only_from_str(
        rfc_9557_str: &str,
        calendar: A,
        iana_parser: IanaParserBorrowed,
    ) -> Result<Self, ParseError> {
        Self::try_location_only_from_utf8(rfc_9557_str.as_bytes(), calendar, iana_parser)
    }

    /// Create a [`ZonedDateTime`] in any calendar from RFC 9557 UTF-8 bytes.
    ///
    /// See [`Self::try_location_only_from_str`].
    pub fn try_location_only_from_utf8(
        rfc_9557_str: &[u8],
        calendar: A,
        iana_parser: IanaParserBorrowed,
    ) -> Result<Self, ParseError> {
        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse()?;
        let (date, rd) = Date::try_from_ixdtf_record_with_rd(&ixdtf_record, calendar)?;
        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
        let zone = Intermediate::try_from_ixdtf_record(&ixdtf_record)?
            .location_only(iana_parser)?
            .at_rd_time(rd, time);
        Ok(ZonedDateTime { date, time, zone })
    }

    /// Create a [`ZonedDateTime`] in any calendar from an RFC 9557 string.
    ///
    /// Returns an error if the string has a calendar annotation that does not
    /// match the calendar argument.
    ///
    /// This function is "lenient": the string can have an offset, and named time zone, both, or
    /// neither. If the named time zone is missing, it is returned as Etc/Unknown.
    pub fn try_lenient_from_str(
        rfc_9557_str: &str,
        calendar: A,
        iana_parser: IanaParserBorrowed,
    ) -> Result<Self, ParseError> {
        Self::try_lenient_from_utf8(rfc_9557_str.as_bytes(), calendar, iana_parser)
    }

    /// Create a [`ZonedDateTime`] in any calendar from RFC 9557 UTF-8 bytes.
    ///
    /// See [`Self::try_lenient_from_str`].
    pub fn try_lenient_from_utf8(
        rfc_9557_str: &[u8],
        calendar: A,
        iana_parser: IanaParserBorrowed,
    ) -> Result<Self, ParseError> {
        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse()?;
        let (date, rd) = Date::try_from_ixdtf_record_with_rd(&ixdtf_record, calendar)?;
        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
        let zone = Intermediate::try_from_ixdtf_record(&ixdtf_record)?
            .lenient(iana_parser)?
            .at_rd_time(rd, time);
        Ok(ZonedDateTime { date, time, zone })
    }

    /// Create a [`ZonedDateTime`] in any calendar from an RFC 9557 string.
    ///
    /// Returns an error if the string has a calendar annotation that does not
    /// match the calendar argument.
    ///
    /// The string should have both an offset and a named time zone.
    ///
    /// For more information on RFC 9557, see the [`ixdtf`] crate.
    ///
    /// # Examples
    ///
    /// Basic usage:
    ///
    /// ```
    /// use icu::calendar::cal::Hebrew;
    /// use icu::locale::subtags::subtag;
    /// use icu::time::{
    ///     zone::{IanaParser, TimeZoneVariant, UtcOffset},
    ///     TimeZone, TimeZoneInfo, ZonedDateTime,
    /// };
    ///
    /// let zoneddatetime = ZonedDateTime::try_strict_from_str(
    ///     "2024-08-08T12:08:19-05:00[America/Chicago][u-ca=hebrew]",
    ///     Hebrew,
    ///     IanaParser::new(),
    /// )
    /// .unwrap();
    ///
    /// assert_eq!(zoneddatetime.date.year().extended_year(), 5784);
    /// assert_eq!(
    ///     zoneddatetime.date.month().to_input().code(),
    ///     icu::calendar::types::MonthCode(tinystr::tinystr!(4, "M11"))
    /// );
    /// assert_eq!(zoneddatetime.date.day_of_month().0, 4);
    ///
    /// assert_eq!(zoneddatetime.time.hour.number(), 12);
    /// assert_eq!(zoneddatetime.time.minute.number(), 8);
    /// assert_eq!(zoneddatetime.time.second.number(), 19);
    /// assert_eq!(zoneddatetime.time.subsecond.number(), 0);
    /// assert_eq!(
    ///     zoneddatetime.zone.id(),
    ///     TimeZone::from_iana_id("America/Chicago")
    /// );
    /// assert_eq!(
    ///     zoneddatetime.zone.offset(),
    ///     Some(UtcOffset::try_from_seconds(-5 * 3600).unwrap())
    /// );
    /// let _ = zoneddatetime.zone.zone_name_timestamp();
    /// ```
    ///
    /// An RFC 9557 string can provide a time zone in two parts: the `DateTime` UTC Offset or the Time Zone
    /// Annotation. A `DateTime` UTC Offset is the time offset as laid out by RFC 3339; meanwhile, the Time
    /// Zone Annotation is the annotation laid out by RFC 9557 and is defined as a UTC offset or IANA Time
    /// Zone identifier.
    ///
    /// ## `DateTime` UTC Offsets
    ///
    /// Below is an example of a time zone from a `DateTime` UTC Offset. The syntax here is familiar to a RFC 3339
    /// `DateTime` string.
    ///
    /// ```
    /// use icu::calendar::Iso;
    /// use icu::time::{zone::UtcOffset, TimeZoneInfo, ZonedDateTime};
    ///
    /// let tz_from_offset = ZonedDateTime::try_offset_only_from_str(
    ///     "2024-08-08T12:08:19-05:00",
    ///     Iso,
    /// )
    /// .unwrap();
    ///
    /// assert_eq!(
    ///     tz_from_offset.zone,
    ///     UtcOffset::try_from_seconds(-5 * 3600).unwrap()
    /// );
    /// ```
    ///
    /// ## Time Zone Annotations
    ///
    /// Below is an example of a time zone being provided by a time zone annotation.
    ///
    /// ```
    /// use icu::calendar::Iso;
    /// use icu::locale::subtags::subtag;
    /// use icu::time::{
    ///     zone::{IanaParser, TimeZoneVariant, UtcOffset},
    ///     TimeZone, TimeZoneInfo, ZonedDateTime,
    /// };
    ///
    /// let tz_from_offset_annotation = ZonedDateTime::try_offset_only_from_str(
    ///     "2024-08-08T12:08:19[-05:00]",
    ///     Iso,
    /// )
    /// .unwrap();
    /// let tz_from_iana_annotation = ZonedDateTime::try_location_only_from_str(
    ///     "2024-08-08T12:08:19[America/Chicago]",
    ///     Iso,
    ///     IanaParser::new(),
    /// )
    /// .unwrap();
    ///
    /// assert_eq!(
    ///     tz_from_offset_annotation.zone,
    ///     UtcOffset::try_from_seconds(-5 * 3600).unwrap()
    /// );
    ///
    /// assert_eq!(
    ///     tz_from_iana_annotation.zone.id(),
    ///     TimeZone(subtag!("uschi"))
    /// );
    /// assert_eq!(tz_from_iana_annotation.zone.offset(), None);
    /// ```
    ///
    /// ## UTC Offset and time zone annotations.
    ///
    /// An RFC 9557 string may contain both a UTC Offset and time zone annotation. This is fine as long as
    /// the time zone parts can be deemed as inconsistent or unknown consistency.
    ///
    /// ### `DateTime` UTC offset with UTC Offset annotation.
    ///
    /// These annotations must always be consistent as they should be either the same value or are inconsistent.
    ///
    /// ```
    /// use icu::calendar::Iso;
    /// use icu::time::{
    ///     zone::UtcOffset, ParseError, TimeZone, TimeZoneInfo, ZonedDateTime,
    /// };
    /// use tinystr::tinystr;
    ///
    /// let consistent_tz_from_both = ZonedDateTime::try_offset_only_from_str(
    ///     "2024-08-08T12:08:19-05:00[-05:00]",
    ///     Iso,
    /// )
    /// .unwrap();
    ///
    /// assert_eq!(
    ///     consistent_tz_from_both.zone,
    ///     UtcOffset::try_from_seconds(-5 * 3600).unwrap()
    /// );
    ///
    /// let inconsistent_tz_from_both = ZonedDateTime::try_offset_only_from_str(
    ///     "2024-08-08T12:08:19-05:00[+05:00]",
    ///     Iso,
    /// );
    ///
    /// assert!(matches!(
    ///     inconsistent_tz_from_both,
    ///     Err(ParseError::InconsistentTimeUtcOffsets)
    /// ));
    /// ```
    pub fn try_strict_from_str(
        rfc_9557_str: &str,
        calendar: A,
        iana_parser: IanaParserBorrowed,
    ) -> Result<Self, ParseError> {
        Self::try_strict_from_utf8(rfc_9557_str.as_bytes(), calendar, iana_parser)
    }

    /// Create a [`ZonedDateTime`] in any calendar from RFC 9557 UTF-8 bytes.
    ///
    /// See [`Self::try_strict_from_str`].
    pub fn try_strict_from_utf8(
        rfc_9557_str: &[u8],
        calendar: A,
        iana_parser: IanaParserBorrowed,
    ) -> Result<Self, ParseError> {
        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse()?;
        let (date, rd) = Date::try_from_ixdtf_record_with_rd(&ixdtf_record, calendar)?;
        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
        let zone = Intermediate::try_from_ixdtf_record(&ixdtf_record)?
            .all(iana_parser)?
            .at_rd_time(rd, time);
        Ok(ZonedDateTime { date, time, zone })
    }
}

#[allow(deprecated)]
impl<A: AsCalendar> ZonedDateTime<A, TimeZoneInfo<models::Full>> {
    /// Create a [`ZonedDateTime`] in any calendar from an RFC 9557 string.
    #[deprecated(since = "2.1.0", note = "use `try_strict_from_str`")]
    pub fn try_full_from_str(
        rfc_9557_str: &str,
        calendar: A,
        iana_parser: IanaParserBorrowed,
        offset_calculator: crate::zone::VariantOffsetsCalculatorBorrowed,
    ) -> Result<Self, ParseError> {
        Self::try_full_from_utf8(
            rfc_9557_str.as_bytes(),
            calendar,
            iana_parser,
            offset_calculator,
        )
    }

    /// Create a [`ZonedDateTime`] in any calendar from RFC 9557 UTF-8 bytes.
    ///
    /// See [`Self::try_full_from_str`].
    #[deprecated(since = "2.1.0", note = "use `try_strict_from_utf8`")]
    pub fn try_full_from_utf8(
        rfc_9557_str: &[u8],
        calendar: A,
        iana_parser: IanaParserBorrowed,
        offset_calculator: crate::zone::VariantOffsetsCalculatorBorrowed,
    ) -> Result<Self, ParseError> {
        let ZonedDateTime { date, time, zone } =
            ZonedDateTime::try_strict_from_utf8(rfc_9557_str, calendar, iana_parser)?;
        Ok(ZonedDateTime {
            date,
            time,
            zone: zone.infer_variant(offset_calculator),
        })
    }
}

#[cfg(feature = "unstable")]
impl ZonedTime<UtcOffset> {
    /// Create a [`ZonedTime`] from an RFC 9557 string.
    ///
    /// This function is "strict": the string should have only an offset and no named time zone.
    pub fn try_offset_only_from_str(rfc_9557_str: &str) -> Result<Self, ParseError> {
        Self::try_offset_only_from_utf8(rfc_9557_str.as_bytes())
    }

    /// Create a [`ZonedTime`] in any calendar from RFC 9557 syntax UTF-8 bytes.
    ///
    /// See [`Self:try_offset_only_from_str`](Self::try_offset_only_from_str).
    pub fn try_offset_only_from_utf8(rfc_9557_str: &[u8]) -> Result<Self, ParseError> {
        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse_time()?;
        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
        let zone = Intermediate::try_from_ixdtf_record(&ixdtf_record)?.offset_only()?;
        Ok(Self { time, zone })
    }
}

#[cfg(feature = "unstable")]
impl ZonedTime<TimeZoneInfo<models::AtTime>> {
    /// Create a [`ZonedTime`] from an RFC 9557 string.
    ///
    /// This function is "strict": the string should have only a named time zone and no offset.
    pub fn try_location_only_from_str(
        rfc_9557_str: &str,
        iana_parser: IanaParserBorrowed,
    ) -> Result<Self, ParseError> {
        Self::try_location_only_from_utf8(rfc_9557_str.as_bytes(), iana_parser)
    }

    /// Create a [`ZonedTime`] from RFC 9557 UTF-8 bytes.
    ///
    /// See [`Self::try_location_only_from_str`].
    pub fn try_location_only_from_utf8(
        rfc_9557_str: &[u8],
        iana_parser: IanaParserBorrowed,
    ) -> Result<Self, ParseError> {
        use crate::zone::ZoneNameTimestamp;

        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse_time()?;
        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
        let zone = Intermediate::try_from_ixdtf_record(&ixdtf_record)?
            .location_only(iana_parser)?
            .with_zone_name_timestamp(ZoneNameTimestamp::far_in_future());
        Ok(Self { time, zone })
    }

    /// Create a [`ZonedTime`] from an RFC 9557 string.
    ///
    /// This function is "lenient": the string can have an offset, and named time zone, both, or
    /// neither. If the named time zone is missing, it is returned as Etc/Unknown.
    pub fn try_lenient_from_str(
        rfc_9557_str: &str,
        iana_parser: IanaParserBorrowed,
    ) -> Result<Self, ParseError> {
        Self::try_lenient_from_utf8(rfc_9557_str.as_bytes(), iana_parser)
    }

    /// Create a [`ZonedTime`] from RFC 9557 UTF-8 bytes.
    ///
    /// See [`Self::try_lenient_from_str`].
    pub fn try_lenient_from_utf8(
        rfc_9557_str: &[u8],
        iana_parser: IanaParserBorrowed,
    ) -> Result<Self, ParseError> {
        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse_time()?;
        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
        let zone = Intermediate::try_from_ixdtf_record(&ixdtf_record)?
            .lenient(iana_parser)?
            .with_zone_name_timestamp(ZoneNameTimestamp::far_in_future());
        Ok(Self { time, zone })
    }

    /// Create a [`ZonedTime`] from an RFC 9557 string.
    ///
    /// The string should have both an offset and a named time zone.
    ///
    /// For more information on RFC 9557, see the [`ixdtf`] crate.
    ///
    /// # Examples
    ///
    /// Basic usage:
    ///
    /// ```
    /// use icu::calendar::cal::Hebrew;
    /// use icu::locale::subtags::subtag;
    /// use icu::time::{
    ///     zone::{IanaParser, TimeZoneVariant, UtcOffset},
    ///     TimeZone, TimeZoneInfo, ZonedTime,
    /// };
    ///
    /// let zonedtime = ZonedTime::try_strict_from_str(
    ///     "T12:08:19-05:00[America/Chicago][u-ca=hebrew]",
    ///     IanaParser::new(),
    /// )
    /// .unwrap();
    ///
    /// assert_eq!(zonedtime.time.hour.number(), 12);
    /// assert_eq!(zonedtime.time.minute.number(), 8);
    /// assert_eq!(zonedtime.time.second.number(), 19);
    /// assert_eq!(zonedtime.time.subsecond.number(), 0);
    /// assert_eq!(zonedtime.zone.id(), TimeZone(subtag!("uschi")));
    /// assert_eq!(
    ///     zonedtime.zone.offset(),
    ///     Some(UtcOffset::try_from_seconds(-5 * 3600).unwrap())
    /// );
    /// let _ = zonedtime.zone.zone_name_timestamp();
    /// ```
    ///
    /// An RFC 9557 string can provide a time zone in two parts: the `DateTime` UTC Offset or the Time Zone
    /// Annotation. A `DateTime` UTC Offset is the time offset as laid out by RFC 3339; meanwhile, the Time
    /// Zone Annotation is the annotation laid out by RFC 9557 and is defined as a UTC offset or IANA Time
    /// Zone identifier.
    ///
    /// ## `DateTime` UTC Offsets
    ///
    /// Below is an example of a time zone from a `DateTime` UTC Offset. The syntax here is familiar to a RFC 3339
    /// `DateTime` string.
    ///
    /// ```
    /// use icu::calendar::Iso;
    /// use icu::time::{zone::UtcOffset, TimeZoneInfo, ZonedTime};
    ///
    /// let tz_from_offset =
    ///     ZonedTime::try_offset_only_from_str("T12:08:19-05:00").unwrap();
    ///
    /// assert_eq!(
    ///     tz_from_offset.zone,
    ///     UtcOffset::try_from_seconds(-5 * 3600).unwrap()
    /// );
    /// ```
    ///
    /// ## Time Zone Annotations
    ///
    /// Below is an example of a time zone being provided by a time zone annotation.
    ///
    /// ```
    /// use icu::calendar::Iso;
    /// use icu::locale::subtags::subtag;
    /// use icu::time::{
    ///     zone::{IanaParser, TimeZoneVariant, UtcOffset},
    ///     TimeZone, TimeZoneInfo, ZonedTime,
    /// };
    ///
    /// let tz_from_offset_annotation =
    ///     ZonedTime::try_offset_only_from_str("T12:08:19[-05:00]").unwrap();
    /// let tz_from_iana_annotation = ZonedTime::try_location_only_from_str(
    ///     "T12:08:19[America/Chicago]",
    ///     IanaParser::new(),
    /// )
    /// .unwrap();
    ///
    /// assert_eq!(
    ///     tz_from_offset_annotation.zone,
    ///     UtcOffset::try_from_seconds(-5 * 3600).unwrap()
    /// );
    ///
    /// assert_eq!(
    ///     tz_from_iana_annotation.zone.id(),
    ///     TimeZone(subtag!("uschi"))
    /// );
    /// assert_eq!(tz_from_iana_annotation.zone.offset(), None);
    /// ```
    ///
    /// ## UTC Offset and time zone annotations.
    ///
    /// An RFC 9557 string may contain both a UTC Offset and time zone annotation. This is fine as long as
    /// the time zone parts can be deemed as inconsistent or unknown consistency.
    ///
    /// ### `DateTime` UTC offset with UTC Offset annotation.
    ///
    /// These annotations must always be consistent as they should be either the same value or are inconsistent.
    ///
    /// ```
    /// use icu::calendar::Iso;
    /// use icu::time::{
    ///     zone::UtcOffset, ParseError, TimeZone, TimeZoneInfo, ZonedTime,
    /// };
    /// use tinystr::tinystr;
    ///
    /// let consistent_tz_from_both =
    ///     ZonedTime::try_offset_only_from_str("T12:08:19-05:00[-05:00]").unwrap();
    ///
    /// assert_eq!(
    ///     consistent_tz_from_both.zone,
    ///     UtcOffset::try_from_seconds(-5 * 3600).unwrap()
    /// );
    ///
    /// let inconsistent_tz_from_both =
    ///     ZonedTime::try_offset_only_from_str("T12:08:19-05:00[+05:00]");
    ///
    /// assert!(matches!(
    ///     inconsistent_tz_from_both,
    ///     Err(ParseError::InconsistentTimeUtcOffsets)
    /// ));
    /// ```
    pub fn try_strict_from_str(
        rfc_9557_str: &str,
        iana_parser: IanaParserBorrowed,
    ) -> Result<Self, ParseError> {
        Self::try_strict_from_utf8(rfc_9557_str.as_bytes(), iana_parser)
    }

    /// Create a [`ZonedTime`] from RFC 9557 UTF-8 bytes.
    ///
    /// See [`Self::try_strict_from_str`].
    pub fn try_strict_from_utf8(
        rfc_9557_str: &[u8],
        iana_parser: IanaParserBorrowed,
    ) -> Result<Self, ParseError> {
        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse_time()?;
        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
        let zone = Intermediate::try_from_ixdtf_record(&ixdtf_record)?
            .all(iana_parser)?
            .with_zone_name_timestamp(ZoneNameTimestamp::far_in_future());

        Ok(Self { time, zone })
    }
}

impl FromStr for DateTime<Iso> {
    type Err = ParseError;
    fn from_str(rfc_9557_str: &str) -> Result<Self, Self::Err> {
        Self::try_from_str(rfc_9557_str, Iso)
    }
}

impl<A: AsCalendar> DateTime<A> {
    /// Creates a [`DateTime`] in any calendar from an RFC 9557 string.
    ///
    /// Returns an error if the string has a calendar annotation that does not
    /// match the calendar argument.
    ///
    /// ✨ *Enabled with the `ixdtf` Cargo feature.*
    ///
    /// # Examples
    ///
    /// ```
    /// use icu::calendar::cal::Hebrew;
    /// use icu::time::DateTime;
    ///
    /// let datetime =
    ///     DateTime::try_from_str("2024-07-17T16:01:17.045[u-ca=hebrew]", Hebrew)
    ///         .unwrap();
    ///
    /// assert_eq!(datetime.date.era_year().year, 5784);
    /// assert_eq!(
    ///     datetime.date.month().to_input().code(),
    ///     icu::calendar::types::MonthCode(tinystr::tinystr!(4, "M10"))
    /// );
    /// assert_eq!(datetime.date.day_of_month().0, 11);
    ///
    /// assert_eq!(datetime.time.hour.number(), 16);
    /// assert_eq!(datetime.time.minute.number(), 1);
    /// assert_eq!(datetime.time.second.number(), 17);
    /// assert_eq!(datetime.time.subsecond.number(), 45000000);
    /// ```
    pub fn try_from_str(rfc_9557_str: &str, calendar: A) -> Result<Self, ParseError> {
        Self::try_from_utf8(rfc_9557_str.as_bytes(), calendar)
    }

    /// Creates a [`DateTime`] in any calendar from an RFC 9557 string.
    ///
    /// See [`Self::try_from_str()`].
    ///
    /// ✨ *Enabled with the `ixdtf` Cargo feature.*
    pub fn try_from_utf8(rfc_9557_str: &[u8], calendar: A) -> Result<Self, ParseError> {
        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse()?;
        let date = Date::try_from_ixdtf_record(&ixdtf_record, calendar)?;
        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
        Ok(Self { date, time })
    }
}

impl Time {
    /// Creates a [`Time`] from an RFC 9557 string of a time.
    ///
    /// Does not support parsing an RFC 9557 string with a date and time; for that, use [`DateTime`].
    ///
    /// ✨ *Enabled with the `ixdtf` Cargo feature.*
    ///
    /// # Examples
    ///
    /// ```
    /// use icu::time::Time;
    ///
    /// let time = Time::try_from_str("16:01:17.045").unwrap();
    ///
    /// assert_eq!(time.hour.number(), 16);
    /// assert_eq!(time.minute.number(), 1);
    /// assert_eq!(time.second.number(), 17);
    /// assert_eq!(time.subsecond.number(), 45000000);
    /// ```
    pub fn try_from_str(rfc_9557_str: &str) -> Result<Self, ParseError> {
        Self::try_from_utf8(rfc_9557_str.as_bytes())
    }

    /// Creates a [`Time`] in the ISO-8601 calendar from an RFC 9557 string.
    ///
    /// ✨ *Enabled with the `ixdtf` Cargo feature.*
    ///
    /// See [`Self::try_from_str()`].
    pub fn try_from_utf8(rfc_9557_str: &[u8]) -> Result<Self, ParseError> {
        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse_time()?;
        Self::try_from_ixdtf_record(&ixdtf_record)
    }

    fn try_from_ixdtf_record(
        ixdtf_record: &IxdtfParseRecord<'_, Utf8>,
    ) -> Result<Self, ParseError> {
        let time_record = ixdtf_record.time.ok_or(ParseError::MissingFields)?;

        let nanosecond = time_record
            .fraction
            .map(|fraction| {
                fraction
                    .to_nanoseconds()
                    .ok_or(ParseError::ExcessivePrecision)
            })
            .transpose()?
            .unwrap_or_default();

        // values are in range by construction
        Ok(Time {
            hour: crate::types::Hour(time_record.hour),
            minute: crate::types::Minute(time_record.minute),
            second: crate::types::Second(time_record.second),
            subsecond: crate::types::Nanosecond(nanosecond),
        })
    }
}

impl FromStr for Time {
    type Err = ParseError;
    fn from_str(rfc_9557_str: &str) -> Result<Self, Self::Err> {
        Self::try_from_str(rfc_9557_str)
    }
}

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

    #[test]
    fn max_possible_rfc_9557_utc_offset() {
        assert_eq!(
            ZonedDateTime::try_offset_only_from_str("2024-08-08T12:08:19+23:59:59.999999999", Iso)
                .unwrap_err(),
            ParseError::InvalidOffsetError
        );
    }

    #[test]
    fn zone_calculations() {
        ZonedDateTime::try_offset_only_from_str("2024-08-08T12:08:19Z", Iso).unwrap();
        assert_eq!(
            ZonedDateTime::try_offset_only_from_str("2024-08-08T12:08:19Z[+08:00]", Iso)
                .unwrap_err(),
            ParseError::RequiresCalculation
        );
        assert_eq!(
            ZonedDateTime::try_offset_only_from_str("2024-08-08T12:08:19Z[Europe/Zurich]", Iso)
                .unwrap_err(),
            ParseError::MismatchedTimeZoneFields
        );
    }

    #[test]
    fn future_zone() {
        let result = ZonedDateTime::try_location_only_from_str(
            "2024-08-08T12:08:19[Future/Zone]",
            Iso,
            IanaParserBorrowed::new(),
        )
        .unwrap();
        assert_eq!(result.zone.id(), TimeZone::UNKNOWN);
        assert_eq!(result.zone.offset(), None);
    }

    #[test]
    fn lax() {
        ZonedDateTime::try_location_only_from_str(
            "2024-10-18T15:44[America/Los_Angeles]",
            icu_calendar::cal::Gregorian,
            IanaParserBorrowed::new(),
        )
        .unwrap();
    }
}
