/*
 * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
 * Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
 * Copyright (c) 2023-2024, Shannon Booth <shannon@serenityos.org>
 * Copyright (c) 2024-2025, Tim Flynn <trflynn89@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/NonnullRawPtr.h>
#include <AK/QuickSort.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Temporal/Calendar.h>
#include <LibJS/Runtime/Temporal/DateEquations.h>
#include <LibJS/Runtime/Temporal/Duration.h>
#include <LibJS/Runtime/Temporal/ISO8601.h>
#include <LibJS/Runtime/Temporal/PlainDate.h>
#include <LibJS/Runtime/Temporal/PlainDateTime.h>
#include <LibJS/Runtime/Temporal/PlainMonthDay.h>
#include <LibJS/Runtime/Temporal/PlainYearMonth.h>
#include <LibJS/Runtime/Temporal/TimeZone.h>
#include <LibJS/Runtime/Temporal/ZonedDateTime.h>
#include <LibJS/Runtime/VM.h>
#include <LibUnicode/Locale.h>
#include <LibUnicode/UnicodeKeywords.h>

namespace JS::Temporal {

enum class CalendarFieldConversion {
    ToIntegerWithTruncation,
    ToMonthCode,
    ToOffsetString,
    ToPositiveIntegerWithTruncation,
    ToString,
    ToTemporalTimeZoneIdentifier,
};

// https://tc39.es/proposal-temporal/#table-temporal-calendar-fields-record-fields
#define JS_ENUMERATE_CALENDAR_FIELDS                                                                                                \
    __JS_ENUMERATE(CalendarField::Era, era, vm.names.era, CalendarFieldConversion::ToString)                                        \
    __JS_ENUMERATE(CalendarField::EraYear, era_year, vm.names.eraYear, CalendarFieldConversion::ToIntegerWithTruncation)            \
    __JS_ENUMERATE(CalendarField::Year, year, vm.names.year, CalendarFieldConversion::ToIntegerWithTruncation)                      \
    __JS_ENUMERATE(CalendarField::Month, month, vm.names.month, CalendarFieldConversion::ToPositiveIntegerWithTruncation)           \
    __JS_ENUMERATE(CalendarField::MonthCode, month_code, vm.names.monthCode, CalendarFieldConversion::ToMonthCode)                  \
    __JS_ENUMERATE(CalendarField::Day, day, vm.names.day, CalendarFieldConversion::ToPositiveIntegerWithTruncation)                 \
    __JS_ENUMERATE(CalendarField::Hour, hour, vm.names.hour, CalendarFieldConversion::ToIntegerWithTruncation)                      \
    __JS_ENUMERATE(CalendarField::Minute, minute, vm.names.minute, CalendarFieldConversion::ToIntegerWithTruncation)                \
    __JS_ENUMERATE(CalendarField::Second, second, vm.names.second, CalendarFieldConversion::ToIntegerWithTruncation)                \
    __JS_ENUMERATE(CalendarField::Millisecond, millisecond, vm.names.millisecond, CalendarFieldConversion::ToIntegerWithTruncation) \
    __JS_ENUMERATE(CalendarField::Microsecond, microsecond, vm.names.microsecond, CalendarFieldConversion::ToIntegerWithTruncation) \
    __JS_ENUMERATE(CalendarField::Nanosecond, nanosecond, vm.names.nanosecond, CalendarFieldConversion::ToIntegerWithTruncation)    \
    __JS_ENUMERATE(CalendarField::Offset, offset_string, vm.names.offset, CalendarFieldConversion::ToOffsetString)                  \
    __JS_ENUMERATE(CalendarField::TimeZone, time_zone, vm.names.timeZone, CalendarFieldConversion::ToTemporalTimeZoneIdentifier)

struct CalendarFieldData {
    CalendarField key;
    NonnullRawPtr<PropertyKey> property;
    CalendarFieldConversion conversion;
};
static Vector<CalendarFieldData> sorted_calendar_fields(VM& vm, CalendarFieldList fields)
{
    auto data_for_field = [&](auto field) -> CalendarFieldData {
        switch (field) {
#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \
    case enumeration:                                                     \
        return { enumeration, property_key, conversion };
            JS_ENUMERATE_CALENDAR_FIELDS
#undef __JS_ENUMERATE
        }

        VERIFY_NOT_REACHED();
    };

    Vector<CalendarFieldData> result;
    result.ensure_capacity(fields.size());

    for (auto field : fields)
        result.unchecked_append(data_for_field(field));

    quick_sort(result, [](auto const& lhs, auto const& rhs) {
        return lhs.property->as_string() < rhs.property->as_string();
    });

    return result;
}

template<typename T>
static void set_field_value(CalendarField field, CalendarFields& fields, T&& value)
{
    switch (field) {
#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion)              \
    case enumeration:                                                                  \
        if constexpr (IsAssignable<decltype(fields.field_name), RemoveCVReference<T>>) \
            fields.field_name = value;                                                 \
        return;
        JS_ENUMERATE_CALENDAR_FIELDS
#undef __JS_ENUMERATE
    }

    VERIFY_NOT_REACHED();
}

static void set_default_field_value(CalendarField field, CalendarFields& fields)
{
    CalendarFields default_ {};

    switch (field) {
#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \
    case enumeration:                                                     \
        fields.field_name = default_.field_name;                          \
        return;
        JS_ENUMERATE_CALENDAR_FIELDS
#undef __JS_ENUMERATE
    }

    VERIFY_NOT_REACHED();
}

// 12.1.1 CanonicalizeCalendar ( id ), https://tc39.es/proposal-temporal/#sec-temporal-canonicalizecalendar
ThrowCompletionOr<String> canonicalize_calendar(VM& vm, StringView id)
{
    // 1. Let calendars be AvailableCalendars().
    auto const& calendars = available_calendars();

    // 2. If calendars does not contain the ASCII-lowercase of id, throw a RangeError exception.
    for (auto const& calendar : calendars) {
        if (calendar.equals_ignoring_ascii_case(id)) {
            // 3. Return CanonicalizeUValue("ca", id).
            return Unicode::canonicalize_unicode_extension_values("ca"sv, id);
        }
    }

    return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidCalendarIdentifier, id);
}

// 12.1.2 AvailableCalendars ( ), https://tc39.es/proposal-temporal/#sec-availablecalendars
Vector<String> const& available_calendars()
{
    // The implementation-defined abstract operation AvailableCalendars takes no arguments and returns a List of calendar
    // types. The returned List is sorted according to lexicographic code unit order, and contains unique calendar types
    // in canonical form (12.1) identifying the calendars for which the implementation provides the functionality of
    // Intl.DateTimeFormat objects, including their aliases (e.g., either both or neither of "islamicc" and
    // "islamic-civil"). The List must include "iso8601".
    return Unicode::available_calendars();
}

// https://tc39.es/proposal-temporal/#prod-MonthCode
static bool is_valid_month_code_string(StringView month_code)
{
    // MonthCode :::
    //     M00L
    //     M0 NonZeroDigit L[opt]
    //     M NonZeroDigit DecimalDigit L[opt]
    if (month_code.length() != 3 && month_code.length() != 4)
        return false;

    if (month_code[0] != 'M')
        return false;

    if (!is_ascii_digit(month_code[1]) || !is_ascii_digit(month_code[2]))
        return false;

    if (month_code.length() == 4 && month_code[3] != 'L')
        return false;

    return true;
}

// 12.2.1 ParseMonthCode ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-parsemonthcode
ThrowCompletionOr<MonthCode> parse_month_code(VM& vm, Value argument)
{
    // 1. Let monthCode be ? ToPrimitive(argument, STRING).
    auto month_code = TRY(argument.to_primitive(vm, Value::PreferredType::String));

    // 2. If monthCode is not a String, throw a TypeError exception.
    if (!month_code.is_string())
        return vm.throw_completion<TypeError>(ErrorType::NotAString, month_code);

    return parse_month_code(vm, month_code.as_string().utf8_string_view());
}

// 12.2.1 ParseMonthCode ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-parsemonthcode
ThrowCompletionOr<MonthCode> parse_month_code(VM& vm, StringView month_code)
{
    // 3. If ParseText(StringToCodePoints(monthCode), MonthCode) is a List of errors, throw a RangeError exception.
    if (!is_valid_month_code_string(month_code))
        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidMonthCode);

    // 4. Let isLeapMonth be false.
    auto is_leap_month = false;

    // 5. If the length of monthCode is 4, then
    if (month_code.length() == 4) {
        // a. Assert: The fourth code unit of monthCode is 0x004C (LATIN CAPITAL LETTER L).
        VERIFY(month_code[3] == 'L');

        // b. Set isLeapMonth to true.
        is_leap_month = true;
    }

    // 6. Let monthCodeDigits be the substring of monthCode from 1 to 3.
    auto month_code_digits = month_code.substring_view(1, 2);

    // 7. Let monthNumber be ℝ(StringToNumber(monthCodeDigits)).
    auto month_number = month_code_digits.to_number<u8>().value();

    // 8. If monthNumber is 0 and isLeapMonth is false, throw a RangeError exception.
    if (month_number == 0 && !is_leap_month)
        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidMonthCode);

    // 9. Return the Record { [[MonthNumber]]: monthNumber, [[IsLeapMonth]]: isLeapMonth }.
    return MonthCode { month_number, is_leap_month };
}

// 12.2.2 CreateMonthCode ( monthNumber, isLeapMonth ), https://tc39.es/proposal-temporal/#sec-temporal-createmonthcode
String create_month_code(u8 month_number, bool is_leap_month)
{
    // 1. Assert: If isLeapMonth is false, monthNumber > 0.
    if (!is_leap_month)
        VERIFY(month_number > 0);

    // 2. Let numberPart be ToZeroPaddedDecimalString(monthNumber, 2).

    // 3. If isLeapMonth is true, then
    if (is_leap_month) {
        // a. Return the string-concatenation of the code unit 0x004D (LATIN CAPITAL LETTER M), numberPart, and the
        //    code unit 0x004C (LATIN CAPITAL LETTER L).
        return MUST(String::formatted("M{:02}L", month_number));
    }

    // 4. Else,
    //     a. Return the string-concatenation of the code unit 0x004D (LATIN CAPITAL LETTER M) and numberPart.
    return MUST(String::formatted("M{:02}", month_number));
}

// 12.3.3 PrepareCalendarFields ( calendar, fields, calendarFieldNames, nonCalendarFieldNames, requiredFieldNames ), https://tc39.es/proposal-temporal/#sec-temporal-preparecalendarfields
ThrowCompletionOr<CalendarFields> prepare_calendar_fields(VM& vm, StringView calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names)
{
    // 1. Assert: If requiredFieldNames is a List, requiredFieldNames contains zero or one of each of the elements of
    //    calendarFieldNames and nonCalendarFieldNames.

    // 2. Let fieldNames be the list-concatenation of calendarFieldNames and nonCalendarFieldNames.
    Vector<CalendarField> field_names;
    field_names.append(calendar_field_names.data(), calendar_field_names.size());
    field_names.append(non_calendar_field_names.data(), non_calendar_field_names.size());

    // 3. Let extraFieldNames be CalendarExtraFields(calendar, calendarFieldNames).
    auto extra_field_names = calendar_extra_fields(calendar, calendar_field_names);

    // 4. Set fieldNames to the list-concatenation of fieldNames and extraFieldNames.
    field_names.extend(move(extra_field_names));

    // 5. Assert: fieldNames contains no duplicate elements.

    // 6. Let result be a Calendar Fields Record with all fields equal to UNSET.
    auto result = CalendarFields::unset();

    // 7. Let any be false.
    auto any = false;

    // 8. Let sortedPropertyNames be a List whose elements are the values in the Property Key column of Table 19
    //    corresponding to the elements of fieldNames, sorted according to lexicographic code unit order.
    auto sorted_property_names = sorted_calendar_fields(vm, field_names);

    // 9. For each property name property of sortedPropertyNames, do
    for (auto const& [key, property, conversion] : sorted_property_names) {
        // a. Let key be the value in the Enumeration Key column of Table 19 corresponding to the row whose Property Key value is property.

        // b. Let value be ? Get(fields, property).
        auto value = TRY(fields.get(property));

        // c. If value is not undefined, then
        if (!value.is_undefined()) {
            // i. Set any to true.
            any = true;

            // ii. Let Conversion be the Conversion value of the same row.
            switch (conversion) {
            // iii. If Conversion is TO-INTEGER-WITH-TRUNCATION, then
            case CalendarFieldConversion::ToIntegerWithTruncation:
                // 1. Set value to ? ToIntegerWithTruncation(value).
                // 2. Set value to 𝔽(value).
                set_field_value(key, result, TRY(to_integer_with_truncation(vm, value, ErrorType::TemporalInvalidCalendarFieldName, *property)));
                break;
            // iv. Else if Conversion is TO-POSITIVE-INTEGER-WITH-TRUNCATION, then
            case CalendarFieldConversion::ToPositiveIntegerWithTruncation:
                // 1. Set value to ? ToPositiveIntegerWithTruncation(value).
                // 2. Set value to 𝔽(value).
                set_field_value(key, result, TRY(to_positive_integer_with_truncation(vm, value, ErrorType::TemporalInvalidCalendarFieldName, *property)));
                break;
            // v. Else if Conversion is TO-STRING, then
            case CalendarFieldConversion::ToString:
                // 1. Set value to ? ToString(value).
                set_field_value(key, result, TRY(value.to_string(vm)));
                break;
            // vi. Else if Conversion is TO-TEMPORAL-TIME-ZONE-IDENTIFIER, then
            case CalendarFieldConversion::ToTemporalTimeZoneIdentifier:
                // 1. Set value to ? ToTemporalTimeZoneIdentifier(value).
                set_field_value(key, result, TRY(to_temporal_time_zone_identifier(vm, value)));
                break;
            // vii. Else if Conversion is TO-MONTH-CODE, then
            case CalendarFieldConversion::ToMonthCode: {
                // 1. Let parsed be ? ParseMonthCode(value).
                auto parsed = TRY(parse_month_code(vm, value));

                // 2. Set value to CreateMonthCode(parsed.[[MonthNumber]], parsed.[[IsLeapMonth]]).
                set_field_value(key, result, create_month_code(parsed.month_number, parsed.is_leap_month));

                break;
            }
            // viii. Else,
            case CalendarFieldConversion::ToOffsetString:
                // 1. Assert: Conversion is TO-OFFSET-STRING.
                // 2. Set value to ? ToOffsetString(value).
                set_field_value(key, result, TRY(to_offset_string(vm, value)));
                break;
            }

            // ix. Set result's field whose name is given in the Field Name column of the same row to value.
        }
        // d. Else if requiredFieldNames is a List, then
        else if (auto const* required = required_field_names.get_pointer<CalendarFieldList>()) {
            // i. If requiredFieldNames contains key, then
            if (required->contains_slow(key)) {
                // 1. Throw a TypeError exception.
                return vm.throw_completion<TypeError>(ErrorType::MissingRequiredProperty, *property);
            }

            // ii. Set result's field whose name is given in the Field Name column of the same row to the corresponding
            //     Default value of the same row.
            set_default_field_value(key, result);
        }
    }

    // 10. If requiredFieldNames is PARTIAL and any is false, then
    if (required_field_names.has<Partial>() && !any) {
        // a. Throw a TypeError exception.
        return vm.throw_completion<TypeError>(ErrorType::TemporalObjectMustBePartialTemporalObject);
    }

    // 11. Return result.
    return result;
}

// 12.3.4 CalendarFieldKeysPresent ( fields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeyspresent
Vector<CalendarField> calendar_field_keys_present(CalendarFields const& fields)
{
    // 1. Let list be « ».
    Vector<CalendarField> list;

    auto handle_field = [&](auto enumeration_key, auto const& value) {
        // a. Let value be fields' field whose name is given in the Field Name column of the row.
        // b. Let enumerationKey be the value in the Enumeration Key column of the row.
        // c. If value is not unset, append enumerationKey to list.
        if (value.has_value())
            list.append(enumeration_key);
    };

    // 2. For each row of Table 19, except the header row, do
#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \
    handle_field(enumeration, fields.field_name);
    JS_ENUMERATE_CALENDAR_FIELDS
#undef __JS_ENUMERATE

    // 3. Return list.
    return list;
}

// 12.3.5 CalendarMergeFields ( calendar, fields, additionalFields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmergefields
CalendarFields calendar_merge_fields(StringView calendar, CalendarFields const& fields, CalendarFields const& additional_fields)
{
    // 1. Let additionalKeys be CalendarFieldKeysPresent(additionalFields).
    auto additional_keys = calendar_field_keys_present(additional_fields);

    // 2. Let overriddenKeys be CalendarFieldKeysToIgnore(calendar, additionalKeys).
    auto overridden_keys = calendar_field_keys_to_ignore(calendar, additional_keys);

    // 3. Let merged be a Calendar Fields Record with all fields set to unset.
    auto merged = CalendarFields::unset();

    // 4. Let fieldsKeys be CalendarFieldKeysPresent(fields).
    auto fields_keys = calendar_field_keys_present(fields);

    auto merge_field = [&](auto key, auto& merged_field, auto const& fields_field, auto const& additional_fields_field) {
        // a. Let key be the value in the Enumeration Key column of the row.

        // b. If fieldsKeys contains key and overriddenKeys does not contain key, then
        if (fields_keys.contains_slow(key) && !overridden_keys.contains_slow(key)) {
            // i. Let propValue be fields' field whose name is given in the Field Name column of the row.
            // ii. Set merged's field whose name is given in the Field Name column of the row to propValue.
            merged_field = fields_field;
        }

        // c. If additionalKeys contains key, then
        if (additional_keys.contains_slow(key)) {
            // i. Let propValue be additionalFields' field whose name is given in the Field Name column of the row.
            // ii. Set merged's field whose name is given in the Field Name column of the row to propValue.
            merged_field = additional_fields_field;
        }
    };

    // 5. For each row of Table 19, except the header row, do
#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \
    merge_field(enumeration, merged.field_name, fields.field_name, additional_fields.field_name);
    JS_ENUMERATE_CALENDAR_FIELDS
#undef __JS_ENUMERATE

    // 6. Return merged.
    return merged;
}

// 12.3.6 NonISODateAdd ( calendar, isoDate, duration, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-nonisodateadd
ThrowCompletionOr<ISODate> non_iso_date_add(VM& vm, StringView calendar, ISODate iso_date, DateDuration const& duration, Overflow overflow)
{
    // FIXME: Return an ISODate for an ISO8601 calendar for now.
    (void)calendar;
    return calendar_date_add(vm, "iso8601"sv, iso_date, duration, overflow);
}

// 12.3.7 CalendarDateAdd ( calendar, isoDate, duration, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendardateadd
ThrowCompletionOr<ISODate> calendar_date_add(VM& vm, StringView calendar, ISODate iso_date, DateDuration const& duration, Overflow overflow)
{
    ISODate result;

    // 1. If calendar is "iso8601", then
    if (calendar == "iso8601"sv) {
        // a. Let intermediate be BalanceISOYearMonth(isoDate.[[Year]] + duration.[[Years]], isoDate.[[Month]] + duration.[[Months]]).
        auto intermediate = balance_iso_year_month(static_cast<double>(iso_date.year) + duration.years, static_cast<double>(iso_date.month) + duration.months);

        // b. Set intermediate to ? RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], isoDate.[[Day]], overflow).
        auto intermediate_date = TRY(regulate_iso_date(vm, intermediate.year, intermediate.month, iso_date.day, overflow));

        // c. Let d be intermediate.[[Day]] + duration.[[Days]] + 7 × duration.[[Weeks]].
        auto day = intermediate_date.day + duration.days + (7 * duration.weeks);

        // d. Let result be BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], d).
        result = balance_iso_date(intermediate_date.year, intermediate_date.month, day);
    }
    // 2. Else,
    else {
        // a. Let result be ? NonISODateAdd(calendar, isoDate, duration, overflow).
        return non_iso_date_add(vm, calendar, iso_date, duration, overflow);
    }

    // 3. If ISODateWithinLimits(result) is false, throw a RangeError exception.
    if (!iso_date_within_limits(result))
        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidISODate);

    // 4. Return result.
    return result;
}

// 12.3.8 NonISODateUntil ( calendar, one, two, largestUnit ), https://tc39.es/proposal-temporal/#sec-temporal-nonisodateuntil
DateDuration non_iso_date_until(VM& vm, StringView calendar, ISODate one, ISODate two, Unit largest_unit)
{
    // FIXME: Return a DateDuration for an ISO8601 calendar for now.
    (void)calendar;
    return calendar_date_until(vm, "iso8601"sv, one, two, largest_unit);
}

// 12.3.9 CalendarDateUntil ( calendar, one, two, largestUnit ), https://tc39.es/proposal-temporal/#sec-temporal-calendardateuntil
DateDuration calendar_date_until(VM& vm, StringView calendar, ISODate one, ISODate two, Unit largest_unit)
{
    // 1. If calendar is "iso8601", then
    if (calendar == "iso8601"sv) {
        // a. Let sign be -CompareISODate(one, two).
        auto sign = compare_iso_date(one, two);
        sign *= -1;

        // b. If sign = 0, return ZeroDateDuration().
        if (sign == 0)
            return zero_date_duration(vm);

        // c. Let years be 0.
        double years = 0;

        // e. Let months be 0.
        double months = 0;

        // OPTIMIZATION: If the largestUnit is MONTH, we want to skip ahead to the correct year. If implemented in exact
        //               accordance with the spec, we could enter the second ISODateSurpasses loop below with a very large
        //               number of months to traverse.

        // d. If largestUnit is YEAR, then
        // f. If largestUnit is YEAR or largestUnit is MONTH, then
        if (largest_unit == Unit::Year || largest_unit == Unit::Month) {
            // d.i. Let candidateYears be sign.
            auto candidate_years = two.year - one.year;
            if (candidate_years != 0)
                candidate_years -= sign;

            // d.ii. Repeat, while ISODateSurpasses(sign, one, two, candidateYears, 0, 0, 0) is false,
            while (!iso_date_surpasses(vm, sign, one, two, candidate_years, 0, 0, 0)) {
                // 1. Set years to candidateYears.
                years = candidate_years;

                // 2. Set candidateYears to candidateYears + sign.
                candidate_years += sign;
            }

            // f.i. Let candidateMonths be sign.
            double candidate_months = sign;

            // f.ii. Repeat, while ISODateSurpasses(sign, one, two, years, candidateMonths, 0, 0) is false,
            while (!iso_date_surpasses(vm, sign, one, two, years, candidate_months, 0, 0)) {
                // 1. Set months to candidateMonths.
                months = candidate_months;

                // 2. Set candidateMonths to candidateMonths + sign.
                candidate_months += sign;
            }

            if (largest_unit == Unit::Month) {
                months += years * 12.0;
                years = 0.0;
            }
        }

        // g. Let weeks be 0.
        double weeks = 0;

        // OPTIMIZATION: If the largestUnit is DAY, we do not want to enter an ISODateSurpasses loop. The loop would have
        //               us increment the intermediate ISOYearMonth one day at time, which will take an extremely long
        //               time if the difference is a large number of years. Instead, we can compute the day difference,
        //               and convert to weeks if needed.
        auto year_month = balance_iso_year_month(static_cast<double>(one.year) + years, static_cast<double>(one.month) + months);
        auto regulated_date = MUST(regulate_iso_date(vm, year_month.year, year_month.month, one.day, Overflow::Constrain));

        auto days = iso_date_to_epoch_days(two.year, two.month - 1, two.day) - iso_date_to_epoch_days(regulated_date.year, regulated_date.month - 1, regulated_date.day);

        if (largest_unit == Unit::Week) {
            weeks = trunc(days / 7.0);
            days = fmod(days, 7.0);
        }

        // l. Return ! CreateDateDurationRecord(years, months, weeks, days).
        return MUST(create_date_duration_record(vm, years, months, weeks, days));
    }

    // 2. Return NonISODateUntil(calendar, one, two, largestUnit).
    return non_iso_date_until(vm, calendar, one, two, largest_unit);
}

// 12.3.10 ToTemporalCalendarIdentifier ( temporalCalendarLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalcalendaridentifier
ThrowCompletionOr<String> to_temporal_calendar_identifier(VM& vm, Value temporal_calendar_like)
{
    // 1. If temporalCalendarLike is an Object, then
    if (temporal_calendar_like.is_object()) {
        auto const& temporal_calendar_object = temporal_calendar_like.as_object();

        // a. If temporalCalendarLike has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]],
        //    [[InitializedTemporalMonthDay]], [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]]
        //    internal slot, then
        //     i. Return temporalCalendarLike.[[Calendar]].
        if (is<PlainDate>(temporal_calendar_object))
            return static_cast<PlainDate const&>(temporal_calendar_object).calendar();
        if (is<PlainDateTime>(temporal_calendar_object))
            return static_cast<PlainDateTime const&>(temporal_calendar_object).calendar();
        if (is<PlainMonthDay>(temporal_calendar_object))
            return static_cast<PlainMonthDay const&>(temporal_calendar_object).calendar();
        if (is<PlainYearMonth>(temporal_calendar_object))
            return static_cast<PlainYearMonth const&>(temporal_calendar_object).calendar();
        if (is<ZonedDateTime>(temporal_calendar_object))
            return static_cast<ZonedDateTime const&>(temporal_calendar_object).calendar();
    }

    // 2. If temporalCalendarLike is not a String, throw a TypeError exception.
    if (!temporal_calendar_like.is_string())
        return vm.throw_completion<TypeError>(ErrorType::TemporalInvalidCalendar);

    // 3. Let identifier be ? ParseTemporalCalendarString(temporalCalendarLike).
    auto identifier = TRY(parse_temporal_calendar_string(vm, temporal_calendar_like.as_string().utf8_string()));

    // 4. Return ? CanonicalizeCalendar(identifier).
    return TRY(canonicalize_calendar(vm, identifier));
}

// 12.3.11 GetTemporalCalendarIdentifierWithISODefault ( item ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalcalendarslotvaluewithisodefault
ThrowCompletionOr<String> get_temporal_calendar_identifier_with_iso_default(VM& vm, Object const& item)
{
    // 1. If item has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], [[InitializedTemporalMonthDay]],
    //    [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal slot, then
    //     a. Return item.[[Calendar]].
    if (is<PlainDate>(item))
        return static_cast<PlainDate const&>(item).calendar();
    if (is<PlainDateTime>(item))
        return static_cast<PlainDateTime const&>(item).calendar();
    if (is<PlainMonthDay>(item))
        return static_cast<PlainMonthDay const&>(item).calendar();
    if (is<PlainYearMonth>(item))
        return static_cast<PlainYearMonth const&>(item).calendar();
    if (is<ZonedDateTime>(item))
        return static_cast<PlainYearMonth const&>(item).calendar();

    // 2. Let calendarLike be ? Get(item, "calendar").
    auto calendar_like = TRY(item.get(vm.names.calendar));

    // 3. If calendarLike is undefined, then
    if (calendar_like.is_undefined()) {
        // a. Return "iso8601".
        return "iso8601"_string;
    }

    // 4. Return ? ToTemporalCalendarIdentifier(calendarLike).
    return TRY(to_temporal_calendar_identifier(vm, calendar_like));
}

// 12.3.12 CalendarDateFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendardatefromfields
ThrowCompletionOr<ISODate> calendar_date_from_fields(VM& vm, StringView calendar, CalendarFields& fields, Overflow overflow)
{
    // 1. Perform ? CalendarResolveFields(calendar, fields, DATE).
    TRY(calendar_resolve_fields(vm, calendar, fields, DateType::Date));

    // 2. Let result be ? CalendarDateToISO(calendar, fields, overflow).
    auto result = TRY(calendar_date_to_iso(vm, calendar, fields, overflow));

    // 3. If ISODateWithinLimits(result) is false, throw a RangeError exception.
    if (!iso_date_within_limits(result))
        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidISODate);

    // 4. Return result.
    return result;
}

// 12.3.13 CalendarYearMonthFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendaryearmonthfromfields
ThrowCompletionOr<ISODate> calendar_year_month_from_fields(VM& vm, StringView calendar, CalendarFields& fields, Overflow overflow)
{
    // 1. Perform ? CalendarResolveFields(calendar, fields, YEAR-MONTH).
    TRY(calendar_resolve_fields(vm, calendar, fields, DateType::YearMonth));

    // FIXME: 2. Let firstDayIndex be the 1-based index of the first day of the month described by fields (i.e., 1 unless the
    //           month's first day is skipped by this calendar.)
    static auto constexpr first_day_index = 1;

    // 3. Set fields.[[Day]] to firstDayIndex.
    fields.day = first_day_index;

    // 4. Let result be ? CalendarDateToISO(calendar, fields, overflow).
    auto result = TRY(calendar_date_to_iso(vm, calendar, fields, overflow));

    // 5. If ISOYearMonthWithinLimits(result) is false, throw a RangeError exception.
    if (!iso_year_month_within_limits(result))
        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidISODate);

    // 6. Return result.
    return result;
}

// 12.3.14 CalendarMonthDayFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdayfromfields
ThrowCompletionOr<ISODate> calendar_month_day_from_fields(VM& vm, StringView calendar, CalendarFields& fields, Overflow overflow)
{
    // 1. Perform ? CalendarResolveFields(calendar, fields, MONTH-DAY).
    TRY(calendar_resolve_fields(vm, calendar, fields, DateType::MonthDay));

    // 2. Let result be ? CalendarMonthDayToISOReferenceDate(calendar, fields, overflow).
    auto result = TRY(calendar_month_day_to_iso_reference_date(vm, calendar, fields, overflow));

    // 3. If ISODateWithinLimits(result) is false, throw a RangeError exception.
    if (!iso_date_within_limits(result))
        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidISODate);

    // 4. Return result.
    return result;
}

// 12.3.15 FormatCalendarAnnotation ( id, showCalendar ), https://tc39.es/proposal-temporal/#sec-temporal-formatcalendarannotation
String format_calendar_annotation(StringView id, ShowCalendar show_calendar)
{
    // 1. If showCalendar is NEVER, return the empty String.
    if (show_calendar == ShowCalendar::Never)
        return String {};

    // 2. If showCalendar is AUTO and id is "iso8601", return the empty String.
    if (show_calendar == ShowCalendar::Auto && id == "iso8601"sv)
        return String {};

    // 3. If showCalendar is CRITICAL, let flag be "!"; else, let flag be the empty String.
    auto flag = show_calendar == ShowCalendar::Critical ? "!"sv : ""sv;

    // 4. Return the string-concatenation of "[", flag, "u-ca=", id, and "]".
    return MUST(String::formatted("[{}u-ca={}]", flag, id));
}

// 12.3.16 CalendarEquals ( one, two ), https://tc39.es/proposal-temporal/#sec-temporal-calendarequals
bool calendar_equals(StringView one, StringView two)
{
    // 1. If CanonicalizeUValue("ca", one) is CanonicalizeUValue("ca", two), return true.
    // 2. Return false.
    return Unicode::canonicalize_unicode_extension_values("ca"sv, one)
        == Unicode::canonicalize_unicode_extension_values("ca"sv, two);
}

// 12.3.17 ISODaysInMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-isodaysinmonth
u8 iso_days_in_month(double year, double month)
{
    // 1. If month is 1, 3, 5, 7, 8, 10, or 12, return 31.
    if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12)
        return 31;

    // 2. If month is 4, 6, 9, or 11, return 30.
    if (month == 4 || month == 6 || month == 9 || month == 11)
        return 30;

    // 3. Assert: month is 2.
    VERIFY(month == 2);

    // 4. Return 28 + MathematicalInLeapYear(EpochTimeForYear(year)).
    return 28 + mathematical_in_leap_year(epoch_time_for_year(year));
}

// 12.3.18 ISOWeekOfYear ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isoweekofyear
YearWeek iso_week_of_year(ISODate iso_date)
{
    // 1. Let year be isoDate.[[Year]].
    auto year = iso_date.year;

    // 2. Let wednesday be 3.
    static constexpr auto wednesday = 3;

    // 3. Let thursday be 4.
    static constexpr auto thursday = 4;

    // 4. Let friday be 5.
    static constexpr auto friday = 5;

    // 5. Let saturday be 6.
    static constexpr auto saturday = 6;

    // 6. Let daysInWeek be 7.
    static constexpr auto days_in_week = 7;

    // 7. Let maxWeekNumber be 53.
    static constexpr auto max_week_number = 53;

    // 8. Let dayOfYear be ISODayOfYear(isoDate).
    auto day_of_year = iso_day_of_year(iso_date);

    // 9. Let dayOfWeek be ISODayOfWeek(isoDate).
    auto day_of_week = iso_day_of_week(iso_date);

    // 10. Let week be floor((dayOfYear + daysInWeek - dayOfWeek + wednesday) / daysInWeek).
    auto week = floor(static_cast<double>(day_of_year + days_in_week - day_of_week + wednesday) / static_cast<double>(days_in_week));

    // 11. If week < 1, then
    if (week < 1) {
        // a. NOTE: This is the last week of the previous year.

        // b. Let jan1st be CreateISODateRecord(year, 1, 1).
        auto jan1st = create_iso_date_record(year, 1, 1);

        // c. Let dayOfJan1st be ISODayOfWeek(jan1st).
        auto day_of_jan1st = iso_day_of_week(jan1st);

        // d. If dayOfJan1st = friday, then
        if (day_of_jan1st == friday) {
            // i. Return Year-Week Record { [[Week]]: maxWeekNumber, [[Year]]: year - 1 }.
            return { .week = max_week_number, .year = year - 1 };
        }

        // e. If dayOfJan1st = saturday, and MathematicalInLeapYear(EpochTimeForYear(year - 1)) = 1, then
        if (day_of_jan1st == saturday && mathematical_in_leap_year(epoch_time_for_year(year - 1)) == 1) {
            // i. Return Year-Week Record { [[Week]]: maxWeekNumber. [[Year]]: year - 1 }.
            return { .week = max_week_number, .year = year - 1 };
        }

        // f. Return Year-Week Record { [[Week]]: maxWeekNumber - 1, [[Year]]: year - 1 }.
        return { .week = max_week_number - 1, .year = year - 1 };
    }

    // 12. If week = maxWeekNumber, then
    if (week == max_week_number) {
        // a. Let daysInYear be MathematicalDaysInYear(year).
        auto days_in_year = mathematical_days_in_year(year);

        // b. Let daysLaterInYear be daysInYear - dayOfYear.
        auto days_later_in_year = days_in_year - day_of_year;

        // c. Let daysAfterThursday be thursday - dayOfWeek.
        auto days_after_thursday = thursday - day_of_week;

        // d. If daysLaterInYear < daysAfterThursday, then
        if (days_later_in_year < days_after_thursday) {
            // i. Return Year-Week Record { [[Week]]: 1, [[Year]]: year + 1 }.
            return { .week = 1, .year = year + 1 };
        }
    }

    // 13. Return Year-Week Record { [[Week]]: week, [[Year]]: year }.
    return { .week = week, .year = year };
}

// 12.3.19 ISODayOfYear ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodayofyear
u16 iso_day_of_year(ISODate iso_date)
{
    // 1. Let epochDays be ISODateToEpochDays(isoDate.[[Year]], isoDate.[[Month]] - 1, isoDate.[[Day]]).
    auto epoch_days = iso_date_to_epoch_days(iso_date.year, iso_date.month - 1, iso_date.day);

    // 2. Return EpochTimeToDayInYear(EpochDaysToEpochMs(epochDays, 0)) + 1.
    return epoch_time_to_day_in_year(epoch_days_to_epoch_ms(epoch_days, 0)) + 1;
}

// 12.3.20 ISODayOfWeek ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodayofweek
u8 iso_day_of_week(ISODate iso_date)
{
    // 1. Let epochDays be ISODateToEpochDays(isoDate.[[Year]], isoDate.[[Month]] - 1, isoDate.[[Day]]).
    auto epoch_days = iso_date_to_epoch_days(iso_date.year, iso_date.month - 1, iso_date.day);

    // 2. Let dayOfWeek be EpochTimeToWeekDay(EpochDaysToEpochMs(epochDays, 0)).
    auto day_of_week = epoch_time_to_week_day(epoch_days_to_epoch_ms(epoch_days, 0));

    // 3. If dayOfWeek = 0, return 7.
    if (day_of_week == 0)
        return 7;

    // 4. Return dayOfWeek.
    return day_of_week;
}

// 12.3.21 NonISOCalendarDateToISO ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-nonisocalendardatetoiso
ThrowCompletionOr<ISODate> non_iso_calendar_date_to_iso(VM& vm, StringView calendar, CalendarFields const& fields, Overflow overflow)
{
    // FIXME: Create an ISODateRecord based on an ISO8601 calendar for now. See also: CalendarResolveFields.
    (void)calendar;
    return calendar_date_to_iso(vm, "iso8601"sv, fields, overflow);
}

// 12.3.22 CalendarDateToISO ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendardatetoiso
ThrowCompletionOr<ISODate> calendar_date_to_iso(VM& vm, StringView calendar, CalendarFields const& fields, Overflow overflow)
{
    // 1. If calendar is "iso8601", then
    if (calendar == "iso8601"sv) {
        // a. Assert: fields.[[Year]], fields.[[Month]], and fields.[[Day]] are not UNSET.
        VERIFY(fields.year.has_value());
        VERIFY(fields.month.has_value());
        VERIFY(fields.day.has_value());

        // b. Return ? RegulateISODate(fields.[[Year]], fields.[[Month]], fields.[[Day]], overflow).
        return TRY(regulate_iso_date(vm, *fields.year, *fields.month, *fields.day, overflow));
    }

    // 2. Return ? NonISOCalendarDateToISO(calendar, fields, overflow).
    return non_iso_calendar_date_to_iso(vm, calendar, fields, overflow);
}

// 12.3.23 NonISOMonthDayToISOReferenceDate ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-nonisomonthdaytoisoreferencedate
ThrowCompletionOr<ISODate> non_iso_month_day_to_iso_reference_date(VM& vm, StringView calendar, CalendarFields const& fields, Overflow overflow)
{
    // FIXME: Create an ISODateRecord based on an ISO8601 calendar for now. See also: CalendarResolveFields.
    (void)calendar;
    return calendar_month_day_to_iso_reference_date(vm, "iso8601"sv, fields, overflow);
}

// 12.3.24 CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdaytoisoreferencedate
ThrowCompletionOr<ISODate> calendar_month_day_to_iso_reference_date(VM& vm, StringView calendar, CalendarFields const& fields, Overflow overflow)
{
    // 1. If calendar is "iso8601", then
    if (calendar == "iso8601"sv) {
        // a. Assert: fields.[[Month]] and fields.[[Day]] are not UNSET.
        VERIFY(fields.month.has_value());
        VERIFY(fields.day.has_value());

        // b. Let referenceISOYear be 1972 (the first ISO 8601 leap year after the epoch).
        static constexpr i32 reference_iso_year = 1972;

        // c. If fields.[[Year]] is UNSET, let year be referenceISOYear; else let year be fields.[[Year]].
        auto year = !fields.year.has_value() ? reference_iso_year : *fields.year;

        // d. Let result be ? RegulateISODate(year, fields.[[Month]], fields.[[Day]], overflow).
        auto result = TRY(regulate_iso_date(vm, year, *fields.month, *fields.day, overflow));

        // e. Return CreateISODateRecord(referenceISOYear, result.[[Month]], result.[[Day]]).
        return create_iso_date_record(reference_iso_year, result.month, result.day);
    }

    // 2. Return ? NonISOMonthDayToISOReferenceDate(calendar, fields, overflow).
    return non_iso_month_day_to_iso_reference_date(vm, calendar, fields, overflow);
}

// 12.3.25 NonISOCalendarISOToDate ( calendar, isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-nonisocalendarisotodate
CalendarDate non_iso_calendar_iso_to_date(StringView calendar, ISODate iso_date)
{
    // FIXME: Return an ISO8601 calendar date for now.
    (void)calendar;
    return calendar_iso_to_date("iso8601"sv, iso_date);
}

// 12.3.26 CalendarISOToDate ( calendar, isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-calendarisotodate
CalendarDate calendar_iso_to_date(StringView calendar, ISODate iso_date)
{
    // 1. If calendar is "iso8601", then
    if (calendar == "iso8601"sv) {
        // a. If MathematicalInLeapYear(EpochTimeForYear(isoDate.[[Year]])) = 1, let inLeapYear be true; else let inLeapYear be false.
        auto in_leap_year = mathematical_in_leap_year(epoch_time_for_year(iso_date.year)) == 1;

        // b. Return Calendar Date Record { [[Era]]: undefined, [[EraYear]]: undefined, [[Year]]: isoDate.[[Year]],
        //    [[Month]]: isoDate.[[Month]], [[MonthCode]]: CreateMonthCode(isoDate.[[Month]], false), [[Day]]: isoDate.[[Day]],
        //    [[DayOfWeek]]: ISODayOfWeek(isoDate), [[DayOfYear]]: ISODayOfYear(isoDate), [[WeekOfYear]]: ISOWeekOfYear(isoDate),
        //    [[DaysInWeek]]: 7, [[DaysInMonth]]: ISODaysInMonth(isoDate.[[Year]], isoDate.[[Month]]),
        //    [[DaysInYear]]: MathematicalDaysInYear(isoDate.[[Year]]), [[MonthsInYear]]: 12, [[InLeapYear]]: inLeapYear }.
        return CalendarDate {
            .era = {},
            .era_year = {},
            .year = iso_date.year,
            .month = iso_date.month,
            .month_code = create_month_code(iso_date.month, false),
            .day = iso_date.day,
            .day_of_week = iso_day_of_week(iso_date),
            .day_of_year = iso_day_of_year(iso_date),
            .week_of_year = iso_week_of_year(iso_date),
            .days_in_week = 7,
            .days_in_month = iso_days_in_month(iso_date.year, iso_date.month),
            .days_in_year = mathematical_days_in_year(iso_date.year),
            .months_in_year = 12,
            .in_leap_year = in_leap_year,
        };
    }

    // 2. Return NonISOCalendarISOToDate(calendar, isoDate).
    return non_iso_calendar_iso_to_date(calendar, iso_date);
}

// 12.3.27 CalendarExtraFields ( calendar, fields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarextrafields
Vector<CalendarField> calendar_extra_fields(StringView calendar, CalendarFieldList)
{
    // 1. If calendar is "iso8601", return an empty List.
    if (calendar == "iso8601"sv)
        return {};

    // FIXME: 2. Return an implementation-defined List as described above.
    return {};
}

// 12.3.28 NonISOFieldKeysToIgnore ( calendar, keys ), https://tc39.es/proposal-temporal/#sec-temporal-nonisofieldkeystoignore
Vector<CalendarField> non_iso_field_keys_to_ignore(StringView calendar, ReadonlySpan<CalendarField> keys)
{
    // FIXME: Return keys for an ISO8601 calendar for now.
    (void)calendar;
    return calendar_field_keys_to_ignore("iso8601"sv, keys);
}

// 12.3.29 CalendarFieldKeysToIgnore ( calendar, keys ), https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeystoignore
Vector<CalendarField> calendar_field_keys_to_ignore(StringView calendar, ReadonlySpan<CalendarField> keys)
{
    // 1. If calendar is "iso8601", then
    if (calendar == "iso8601"sv) {
        // a. Let ignoredKeys be an empty List.
        Vector<CalendarField> ignored_keys;

        // b. For each element key of keys, do
        for (auto key : keys) {
            // i. Append key to ignoredKeys.
            ignored_keys.append(key);

            // ii. If key is MONTH, append MONTH-CODE to ignoredKeys.
            if (key == CalendarField::Month)
                ignored_keys.append(CalendarField::MonthCode);
            // iii. Else if key is MONTH-CODE, append MONTH to ignoredKeys.
            else if (key == CalendarField::MonthCode)
                ignored_keys.append(CalendarField::Month);
        }

        // c. NOTE: While ignoredKeys can have duplicate elements, this is not intended to be meaningful. This specification
        //    only checks whether particular keys are or are not members of the list.

        // d. Return ignoredKeys.
        return ignored_keys;
    }

    // 2. Return NonISOFieldKeysToIgnore(calendar, keys).
    return non_iso_field_keys_to_ignore(calendar, keys);
}

// 12.3.30 NonISOResolveFields ( calendar, fields, type ), https://tc39.es/proposal-temporal/#sec-temporal-nonisoresolvefields
ThrowCompletionOr<void> non_iso_resolve_fields(VM& vm, StringView calendar, CalendarFields& fields, DateType type)
{
    // FIXME: Resolve fields as an ISO8601 calendar for now. See also: CalendarMonthDayToISOReferenceDate.
    (void)calendar;
    return calendar_resolve_fields(vm, "iso8601"sv, fields, type);
}

// 12.3.31 CalendarResolveFields ( calendar, fields, type ), https://tc39.es/proposal-temporal/#sec-temporal-calendarresolvefields
ThrowCompletionOr<void> calendar_resolve_fields(VM& vm, StringView calendar, CalendarFields& fields, DateType type)
{
    // 1. If calendar is "iso8601", then
    if (calendar == "iso8601"sv) {
        // a. If type is DATE or YEAR-MONTH and fields.[[Year]] is UNSET, throw a TypeError exception.
        if ((type == DateType::Date || type == DateType::YearMonth) && !fields.year.has_value())
            return vm.throw_completion<TypeError>(ErrorType::MissingRequiredProperty, "year"sv);

        // b. If type is DATE or MONTH-DAY and fields.[[Day]] is UNSET, throw a TypeError exception.
        if ((type == DateType::Date || type == DateType::MonthDay) && !fields.day.has_value())
            return vm.throw_completion<TypeError>(ErrorType::MissingRequiredProperty, "day"sv);

        // c. Let month be fields.[[Month]].
        auto const& month = fields.month;

        // d. Let monthCode be fields.[[MonthCode]].
        auto const& month_code = fields.month_code;

        // e. If monthCode is UNSET, then
        if (!month_code.has_value()) {
            // i. If month is UNSET, throw a TypeError exception.
            if (!month.has_value())
                return vm.throw_completion<TypeError>(ErrorType::MissingRequiredProperty, "month"sv);

            // ii. Return UNUSED.
            return {};
        }

        // f. Assert: monthCode is a String.
        VERIFY(month_code.has_value());

        // g. Let parsedMonthCode be ? ParseMonthCode(monthCode).
        auto parsed_month_code = TRY(parse_month_code(vm, *month_code));

        // h. If parsedMonthCode.[[IsLeapMonth]] is true, throw a RangeError exception.
        if (parsed_month_code.is_leap_month)
            return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv);

        // i. If parsedMonthCode.[[MonthNumber]] > 12, throw a RangeError exception.
        if (parsed_month_code.month_number > 12)
            return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv);

        // j. If month is not UNSET and month ≠ parsedMonthCode.[[MonthNumber]], throw a RangeError exception.
        if (month.has_value() && month != parsed_month_code.month_number)
            return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidCalendarFieldName, "month"sv);

        // k. Set fields.[[Month]] to parsedMonthCode.[[MonthNumber]].
        fields.month = parsed_month_code.month_number;
    }
    // 2. Else,
    else {
        // a. Perform ? NonISOResolveFields(calendar, fields, type).
        return TRY(non_iso_resolve_fields(vm, calendar, fields, type));
    }

    // 3. Return UNUSED.
    return {};
}

}
