#include <AK/String.h>
#include <AK/TypeCasts.h>
#include <AK/Variant.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/ArrayPrototype.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/Iterator.h>
#include <LibJS/Runtime/IteratorPrototype.h>
#include <LibJS/Runtime/PrimitiveString.h>
#include <LibJS/Runtime/PropertyKey.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/Headers.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Fetch/Headers.h>
#include <LibWeb/Fetch/HeadersIterator.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/Tracing.h>

namespace Web::Bindings {

void HeadersConstructor::initialize(JS::Realm& realm, JS::NativeFunction& object)
{
    auto& vm = realm.vm();
    [[maybe_unused]] u8 default_attributes = JS::Attribute::Enumerable;

    
    object.define_direct_property(vm.names.length, JS::Value(0), JS::Attribute::Configurable);
    object.define_direct_property(vm.names.name, JS::PrimitiveString::create(vm, "Headers"_utf16), JS::Attribute::Configurable);
    object.define_direct_property(vm.names.prototype, &ensure_web_prototype<HeadersPrototype>(realm, "Headers"_fly_string), 0);
}

JS::ThrowCompletionOr<GC::Ref<JS::Object>> HeadersConstructor::construct([[maybe_unused]] InterfaceConstructor& constructor, [[maybe_unused]] JS::FunctionObject& new_target)
{
    WebIDL::log_trace(constructor.vm(), "HeadersConstructor::construct");
    auto& vm = constructor.vm();
    [[maybe_unused]] auto& realm = *vm.current_realm();

    // To internally create a new object implementing the interface Headers:

    // 3.2. Let prototype be ? Get(newTarget, "prototype").
    auto prototype = TRY(new_target.get(vm.names.prototype));

    // 3.3. If Type(prototype) is not Object, then:
    if (!prototype.is_object()) {
        // 1. Let targetRealm be ? GetFunctionRealm(newTarget).
        auto* target_realm = TRY(JS::get_function_realm(vm, new_target));

        // 2. Set prototype to the interface prototype object for interface in targetRealm.
        VERIFY(target_realm);
        prototype = &Bindings::ensure_web_prototype<HeadersPrototype>(*target_realm, "Headers"_fly_string);
    }

    auto arg0 = vm.argument(0);
    Optional<Variant<Vector<Vector<String>>, OrderedHashMap<String, String>>> init {};
    if (!arg0.is_undefined())
        init = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<Variant<Vector<Vector<String>>, OrderedHashMap<String, String>>> {

        if (arg0.is_object()) {
            [[maybe_unused]] auto& object = arg0.as_object();

            // 1. Let method be ? GetMethod(V, @@iterator).
            auto method = TRY(arg0.get_method(vm, vm.well_known_symbol_iterator()));

            // 2. If method is not undefined, return the result of creating a sequence of that type from V and method.
            if (method) {
                auto sequence_union_type = TRY([&]() -> JS::ThrowCompletionOr<Vector<Vector<String>>> {
        // To create an IDL value of type sequence<T> given an iterable iterable and an iterator getter method, perform the following steps:
        // 1. Let iteratorRecord be ? GetIteratorFromMethod(iterable, method).
        auto iterator = TRY(JS::get_iterator_from_method(vm, arg0, *method));

        Vector<Vector<String>> sequence;

        // 2. Initialize i to be 0.
        // 3. Repeat
        for (;;) {
            // 1. Let next be ? IteratorStepValue(iteratorRecord).
            auto next = TRY(JS::iterator_step_value(vm, iterator));

            // 2. If next is done, then return an IDL sequence value of type sequence<T> of length i, where the value of the element at index j is Sj.
            if (!next.has_value())
                break;

            // 3. Initialize Si to the result of converting next to an IDL value of type T.
            auto next_value = next.release_value();
            auto sequence_item = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<Vector<String>> {
        if (!next_value.is_object())
            return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObject, next_value);

        auto method = TRY(next_value.get_method(vm, vm.well_known_symbol_iterator()));
        if (!method)
            return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotIterable, next_value);

        return TRY([&]() -> JS::ThrowCompletionOr<Vector<String>> {
        // To create an IDL value of type sequence<T> given an iterable iterable and an iterator getter method, perform the following steps:
        // 1. Let iteratorRecord be ? GetIteratorFromMethod(iterable, method).
        auto iterator = TRY(JS::get_iterator_from_method(vm, next_value, *method));

        Vector<String> sequence;

        // 2. Initialize i to be 0.
        // 3. Repeat
        for (;;) {
            // 1. Let next be ? IteratorStepValue(iteratorRecord).
            auto next = TRY(JS::iterator_step_value(vm, iterator));

            // 2. If next is done, then return an IDL sequence value of type sequence<T> of length i, where the value of the element at index j is Sj.
            if (!next.has_value())
                break;

            // 3. Initialize Si to the result of converting next to an IDL value of type T.
            auto next_value = next.release_value();
            auto sequence_item = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<String> {
        return TRY(WebIDL::to_byte_string(vm, next_value));
    }(); }));

            // 4. Set i to i + 1.
            sequence.append(sequence_item);
        }

        return sequence;
    }());
    }(); }));

            // 4. Set i to i + 1.
            sequence.append(sequence_item);
        }

        return sequence;
    }());
                return Variant<Vector<Vector<String>>, OrderedHashMap<String, String>> { sequence_union_type };
            }


            auto record_union_type = TRY([&]() -> JS::ThrowCompletionOr<OrderedHashMap<String, String>> {
        // An ECMAScript value O is converted to an IDL record<K, V> value as follows:
        // 1. If Type(O) is not Object, throw a TypeError.
        if (!arg0.is_object())
            return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObject, arg0);

        auto& record_object0 = arg0.as_object();

        // 2. Let result be a new empty instance of record<K, V>.
        OrderedHashMap<String, String> record;

        // 3. Let keys be ? O.[[OwnPropertyKeys]]().
        auto keys = TRY(record_object0.internal_own_property_keys());

        // 4. For each key of keys:
        for (auto& key : keys) {
            auto property_key = MUST(JS::PropertyKey::from_value(vm, key));

            // 1. Let desc be ? O.[[GetOwnProperty]](key).
            auto desc = TRY(record_object0.internal_get_own_property(property_key));

            // 2. If desc is not undefined and desc.[[Enumerable]] is true:
            if (!desc.has_value() || !desc->enumerable.has_value() || !desc->enumerable.value())
                continue;

            // 1. Let typedKey be key converted to an IDL value of type K.
            auto typed_key = TRY([&]() -> JS::ThrowCompletionOr<String> {
        return TRY(WebIDL::to_byte_string(vm, key));
    }());

            // 2. Let value be ? Get(O, key).
            auto value = TRY(record_object0.get(property_key));

            // 3. Let typedValue be value converted to an IDL value of type V.
            auto typed_value = TRY([&]() -> JS::ThrowCompletionOr<String> {
        return TRY(WebIDL::to_byte_string(vm, value));
    }());

            // 4. Set result[typedKey] to typedValue.
            record.set(typed_key, typed_value);
        }

        // 5. Return result.
        return record;
    }());
            return Variant<Vector<Vector<String>>, OrderedHashMap<String, String>> { record_union_type };
        }

        return vm.throw_completion<JS::TypeError>("No union types matched"_utf16);
    }(); }));

    auto impl = TRY(throw_dom_exception_if_needed(vm, [&] { return Fetch::Headers::construct_impl(realm, init); }));

    // 7. Set instance.[[Prototype]] to prototype.
    VERIFY(prototype.is_object());
    impl->set_prototype(&prototype.as_object());

    // FIXME: Steps 8...11. of the "internally create a new object implementing the interface Headers" algorithm
    // (https://webidl.spec.whatwg.org/#js-platform-objects) are currently not handled, or are handled within Fetch::Headers::construct_impl().

    return *impl;
}

GC_DEFINE_ALLOCATOR(HeadersPrototype);

HeadersPrototype::HeadersPrototype([[maybe_unused]] JS::Realm& realm)
    : Object(ConstructWithPrototypeTag::Tag, realm.intrinsics().object_prototype())
{
}

HeadersPrototype::~HeadersPrototype()
{
}


void HeadersPrototype::initialize(JS::Realm& realm)
{
    auto& object = *this;
    [[maybe_unused]] auto& vm = realm.vm();
    [[maybe_unused]] u8 default_attributes = JS::Attribute::Enumerable | JS::Attribute::Configurable | JS::Attribute::Writable;

    object.set_prototype(realm.intrinsics().object_prototype());
    object.define_native_function(realm, "append"_utf16_fly_string, append, 2, default_attributes);

    object.define_native_function(realm, "delete"_utf16_fly_string, delete_, 1, default_attributes);

    object.define_native_function(realm, "get"_utf16_fly_string, get, 1, default_attributes);

    object.define_native_function(realm, "getSetCookie"_utf16_fly_string, get_set_cookie, 0, default_attributes);

    object.define_native_function(realm, "has"_utf16_fly_string, has, 1, default_attributes);

    object.define_native_function(realm, "set"_utf16_fly_string, set, 2, default_attributes);

    object.define_native_function(realm, vm.names.entries, entries, 0, default_attributes);
    object.define_native_function(realm, vm.names.forEach, for_each, 1, default_attributes);
    object.define_native_function(realm, vm.names.keys, keys, 0, default_attributes);
    object.define_native_function(realm, vm.names.values, values, 0, default_attributes);

    object.define_direct_property(vm.well_known_symbol_iterator(), object.get_without_side_effects(vm.names.entries), JS::Attribute::Configurable | JS::Attribute::Writable);

    object.define_direct_property(vm.well_known_symbol_to_string_tag(), JS::PrimitiveString::create(vm, "Headers"_utf16), JS::Attribute::Configurable);
    Base::initialize(realm);
}

void HeadersPrototype::define_unforgeable_attributes(JS::Realm& realm, [[maybe_unused]] JS::Object& object)
{
    [[maybe_unused]] auto& vm = realm.vm();
    [[maybe_unused]] u8 default_attributes = JS::Attribute::Enumerable;
}

[[maybe_unused]] static JS::ThrowCompletionOr<Fetch::Headers*> impl_from(JS::VM& vm, JS::Value js_value)
{

    if (auto impl = js_value.as_if<Fetch::Headers>())
        return impl.ptr();
    return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "Headers");
}

[[maybe_unused]] static JS::ThrowCompletionOr<Fetch::Headers*> impl_from(JS::VM& vm)
{
    auto this_value = vm.this_value();
    if (this_value.is_nullish())
        this_value = &vm.current_realm()->global_object();
    return impl_from(vm, this_value);
}

JS_DEFINE_NATIVE_FUNCTION(HeadersPrototype::append)
{
    WebIDL::log_trace(vm, "HeadersPrototype::append");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] Fetch::Headers* idl_object = TRY(impl_from(vm));

    if (vm.argument_count() < 2)
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::BadArgCountMany, "append", "2");

    auto arg0 = vm.argument(0);
    auto name = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<String> {
        return TRY(WebIDL::to_byte_string(vm, arg0));
    }(); }));

    auto arg1 = vm.argument(1);
    auto value = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<String> {
        return TRY(WebIDL::to_byte_string(vm, arg1));
    }(); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->append(name, value); }));
    return JS::js_undefined();
}

JS_DEFINE_NATIVE_FUNCTION(HeadersPrototype::delete_)
{
    WebIDL::log_trace(vm, "HeadersPrototype::delete_");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] Fetch::Headers* idl_object = TRY(impl_from(vm));

    if (vm.argument_count() < 1)
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::BadArgCountOne, "delete");

    auto arg0 = vm.argument(0);
    auto name = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<String> {
        return TRY(WebIDL::to_byte_string(vm, arg0));
    }(); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->delete_(name); }));
    return JS::js_undefined();
}

JS_DEFINE_NATIVE_FUNCTION(HeadersPrototype::get)
{
    WebIDL::log_trace(vm, "HeadersPrototype::get");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] Fetch::Headers* idl_object = TRY(impl_from(vm));

    if (vm.argument_count() < 1)
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::BadArgCountOne, "get");

    auto arg0 = vm.argument(0);
    auto name = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<String> {
        return TRY(WebIDL::to_byte_string(vm, arg0));
    }(); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->get(name); }));
    return [&]() -> JS::Value {
        // 1. If the IDL nullable type T? value is null, then the JavaScript value is null.
        if (!R.has_value())
            return JS::js_null();

        // 2. Otherwise, the JavaScript value is the result of converting the IDL nullable type value to the inner IDL type T.
        return JS::Value(WebIDL::primitive_string_from_string(vm, R.value()));
    }();
}

JS_DEFINE_NATIVE_FUNCTION(HeadersPrototype::get_set_cookie)
{
    WebIDL::log_trace(vm, "HeadersPrototype::get_set_cookie");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] Fetch::Headers* idl_object = TRY(impl_from(vm));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->get_set_cookie(); }));
    return [&]() -> JS::Value {
        // An IDL sequence<T> value S is converted to a JavaScript value as follows:
        // 1. Let n be the length of S.
        auto sequence_length = R.size();

        // 2. Let A be a new Array object created as if by the expression [].
        auto sequence_array = MUST(JS::Array::create(realm, sequence_length));

        // 3. Initialize i to be 0.
        // 4. While i < n:
        for (size_t sequence_index = 0; sequence_index < sequence_length; ++sequence_index) {
            // 1. Let V be the value in S at index i.
            auto& sequence_element = R.at(sequence_index);

            // 2. Let E be the result of converting V to a JavaScript value.
            JS::Value js_sequence_element = WebIDL::primitive_string_from_string(vm, sequence_element);

            // 3. Let P be the result of calling ! ToString(i).
            // 4. Perform ! CreateDataPropertyOrThrow(A, P, E).
            MUST(sequence_array->create_data_property(JS::PropertyKey { sequence_index }, js_sequence_element));

            // 5. Set i to i + 1.
        }

        // 5. Return A.
        return sequence_array;
    }();
}

JS_DEFINE_NATIVE_FUNCTION(HeadersPrototype::has)
{
    WebIDL::log_trace(vm, "HeadersPrototype::has");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] Fetch::Headers* idl_object = TRY(impl_from(vm));

    if (vm.argument_count() < 1)
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::BadArgCountOne, "has");

    auto arg0 = vm.argument(0);
    auto name = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<String> {
        return TRY(WebIDL::to_byte_string(vm, arg0));
    }(); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->has(name); }));
    return JS::Value(R);
}

JS_DEFINE_NATIVE_FUNCTION(HeadersPrototype::set)
{
    WebIDL::log_trace(vm, "HeadersPrototype::set");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    [[maybe_unused]] Fetch::Headers* idl_object = TRY(impl_from(vm));

    if (vm.argument_count() < 2)
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::BadArgCountMany, "set", "2");

    auto arg0 = vm.argument(0);
    auto name = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<String> {
        return TRY(WebIDL::to_byte_string(vm, arg0));
    }(); }));

    auto arg1 = vm.argument(1);
    auto value = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<String> {
        return TRY(WebIDL::to_byte_string(vm, arg1));
    }(); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->set(name, value); }));
    return JS::js_undefined();
}

JS_DEFINE_NATIVE_FUNCTION(HeadersPrototype::entries)
{
    WebIDL::log_trace(vm, "HeadersPrototype::entries");

    // 1. Let jsValue be ? ToObject(this value).
    // 2. If jsValue is a platform object, then perform a security check, passing jsValue, "%Symbol.iterator%", and "method".
    // 3. If jsValue does not implement definition, then throw a TypeError.
    auto* this_impl = TRY(impl_from(vm));

    // 4. Return a newly created default iterator object for definition, with jsValue as its target, "key+value" as its kind, and index set to 0.
    return TRY(throw_dom_exception_if_needed(vm, [&] { return Fetch::HeadersIterator::create(*this_impl, JS::Object::PropertyKind::KeyAndValue); }));
}

JS_DEFINE_NATIVE_FUNCTION(HeadersPrototype::keys)
{
    WebIDL::log_trace(vm, "HeadersPrototype::keys");

    // 1. Let jsValue be ? ToObject(this value).
    // 2. If jsValue is a platform object, then perform a security check, passing jsValue, "keys", and "method".
    // 3. If jsValue does not implement definition, then throw a TypeError.
    auto* this_impl = TRY(impl_from(vm));

    // 4. Return a newly created default iterator object for definition, with jsValue as its target, "key" as its kind, and index set to 0.
    return TRY(throw_dom_exception_if_needed(vm, [&] { return Fetch::HeadersIterator::create(*this_impl, JS::Object::PropertyKind::Key); }));
}

JS_DEFINE_NATIVE_FUNCTION(HeadersPrototype::values)
{
    WebIDL::log_trace(vm, "HeadersPrototype::values");

    // 1. Let jsValue be ? ToObject(this value).
    // 2. If jsValue is a platform object, then perform a security check, passing jsValue, "values", and "method".
    // 3. If jsValue does not implement definition, then throw a TypeError.
    auto* this_impl = TRY(impl_from(vm));

    // 4. Return a newly created default iterator object for definition, with jsValue as its target, "value" as its kind, and index set to 0.
    return TRY(throw_dom_exception_if_needed(vm, [&] { return Fetch::HeadersIterator::create(*this_impl, JS::Object::PropertyKind::Value); }));
}

JS_DEFINE_NATIVE_FUNCTION(HeadersPrototype::for_each)
{
    WebIDL::log_trace(vm, "HeadersPrototype::for_each");
    auto* this_impl = TRY(impl_from(vm));

    auto callback = vm.argument(0);
    if (!callback.is_function())
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAFunction, callback);

    auto this_value = vm.this_value();
    TRY(this_impl->for_each([&](auto key, auto value) -> JS::ThrowCompletionOr<void> {
        JS::Value wrapped_key = WebIDL::primitive_string_from_string(vm, key);
        JS::Value wrapped_value = WebIDL::primitive_string_from_string(vm, value);
        TRY(JS::call(vm, callback.as_function(), vm.argument(1), wrapped_value, wrapped_key, this_value));
        return {};
    }));

    return JS::js_undefined();
}

GC_DEFINE_ALLOCATOR(HeadersIteratorPrototype);

HeadersIteratorPrototype::HeadersIteratorPrototype(JS::Realm& realm)
    : Object(ConstructWithPrototypeTag::Tag, realm.intrinsics().iterator_prototype())
{
}

HeadersIteratorPrototype::~HeadersIteratorPrototype()
{
}

void HeadersIteratorPrototype::initialize(JS::Realm& realm)
{
    auto& vm = this->vm();
    Base::initialize(realm);
    define_native_function(realm, vm.names.next, next, 0, JS::Attribute::Writable | JS::Attribute::Enumerable | JS::Attribute::Configurable);
    define_direct_property(vm.well_known_symbol_to_string_tag(), JS::PrimitiveString::create(vm, "Headers Iterator"_utf16), JS::Attribute::Configurable);
}

static JS::ThrowCompletionOr<Fetch::HeadersIterator*> headers_iterator_impl_from(JS::VM& vm)
{
    auto this_object = TRY(vm.this_value().to_object(vm));
    if (!is<Fetch::HeadersIterator>(*this_object))
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "HeadersIterator");
    return static_cast<Fetch::HeadersIterator*>(this_object.ptr());
}

JS_DEFINE_NATIVE_FUNCTION(HeadersIteratorPrototype::next)
{
    WebIDL::log_trace(vm, "HeadersIteratorPrototype::next");
    auto* impl = TRY(headers_iterator_impl_from(vm));
    return TRY(throw_dom_exception_if_needed(vm, [&] { return impl->next(); }));
}

} // namespace Web::Bindings
