#include <AK/TypeCasts.h>
#include <AK/Variant.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/Promise.h>
#include <LibJS/Runtime/VM.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibWeb/Bindings/Credential.h>
#include <LibWeb/Bindings/CredentialsContainer.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/FederatedCredential.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/PasswordCredential.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/CredentialManagement/Credential.h>
#include <LibWeb/CredentialManagement/CredentialsContainer.h>
#include <LibWeb/DOM/AbortSignal.h>
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/WebIDL/Promise.h>
#include <LibWeb/WebIDL/Tracing.h>

namespace Web::Bindings {

void CredentialsContainerConstructor::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, "CredentialsContainer"_utf16), JS::Attribute::Configurable);
    object.define_direct_property(vm.names.prototype, &ensure_web_prototype<CredentialsContainerPrototype>(realm, "CredentialsContainer"_fly_string), 0);
}

JS::ThrowCompletionOr<GC::Ref<JS::Object>> CredentialsContainerConstructor::construct([[maybe_unused]] InterfaceConstructor& constructor, [[maybe_unused]] JS::FunctionObject& new_target)
{
    WebIDL::log_trace(constructor.vm(), "CredentialsContainerConstructor::construct");
    return constructor.vm().throw_completion<JS::TypeError>(JS::ErrorType::NotAConstructor, "CredentialsContainer");
}

void CredentialsContainerPrototype::initialize(JS::Realm& realm, JS::Object& object)
{
    [[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, "get"_utf16_fly_string, get, 0, default_attributes);

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

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

    object.define_native_function(realm, "preventSilentAccess"_utf16_fly_string, prevent_silent_access, 0, default_attributes);

    object.define_direct_property(vm.well_known_symbol_to_string_tag(), JS::PrimitiveString::create(vm, "CredentialsContainer"_utf16), JS::Attribute::Configurable);
}

void CredentialsContainerPrototype::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<CredentialManagement::CredentialsContainer*> impl_from(JS::VM& vm, JS::Value js_value)
{

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

[[maybe_unused]] static JS::ThrowCompletionOr<CredentialManagement::CredentialsContainer*> 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(CredentialsContainerPrototype::get)
{
    WebIDL::log_trace(vm, "CredentialsContainerPrototype::get");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    auto steps = [&realm, &vm]() -> JS::ThrowCompletionOr<GC::Ref<WebIDL::Promise>> {
        (void)realm;
    [[maybe_unused]] CredentialManagement::CredentialsContainer* idl_object = TRY(impl_from(vm));

    auto arg0 = vm.argument(0);
    CredentialRequestOptions options = CredentialRequestOptions {};
    if (!arg0.is_undefined())
        options = TRY(throw_dom_exception_if_needed(vm, [&] { return convert_to_idl_value_for_credential_request_options(vm, arg0); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->get(options); }));
        return R;
    };

    auto maybe_R = steps();

    // And then, if an exception E was thrown:
    // 1. If op has a return type that is a promise type, then return ! Call(%Promise.reject%, %Promise%, «E»).
    // 2. Otherwise, end these steps and allow the exception to propagate.
    if (maybe_R.is_throw_completion())
        return WebIDL::create_rejected_promise(realm, maybe_R.error_value())->promise();

    return GC::Ref { as<JS::Promise>(*maybe_R.release_value()->promise()) };
}

JS_DEFINE_NATIVE_FUNCTION(CredentialsContainerPrototype::store)
{
    WebIDL::log_trace(vm, "CredentialsContainerPrototype::store");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    auto steps = [&realm, &vm]() -> JS::ThrowCompletionOr<GC::Ref<WebIDL::Promise>> {
        (void)realm;
    [[maybe_unused]] CredentialManagement::CredentialsContainer* idl_object = TRY(impl_from(vm));

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

    auto arg0 = vm.argument(0);
    auto credential = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<GC::Ref<CredentialManagement::Credential>> {
        if (auto impl = arg0.as_if<CredentialManagement::Credential>())
            return *impl;
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "Credential");
    }(); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->store(credential); }));
        return R;
    };

    auto maybe_R = steps();

    // And then, if an exception E was thrown:
    // 1. If op has a return type that is a promise type, then return ! Call(%Promise.reject%, %Promise%, «E»).
    // 2. Otherwise, end these steps and allow the exception to propagate.
    if (maybe_R.is_throw_completion())
        return WebIDL::create_rejected_promise(realm, maybe_R.error_value())->promise();

    return GC::Ref { as<JS::Promise>(*maybe_R.release_value()->promise()) };
}

JS_DEFINE_NATIVE_FUNCTION(CredentialsContainerPrototype::create)
{
    WebIDL::log_trace(vm, "CredentialsContainerPrototype::create");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    auto steps = [&realm, &vm]() -> JS::ThrowCompletionOr<GC::Ref<WebIDL::Promise>> {
        (void)realm;
    [[maybe_unused]] CredentialManagement::CredentialsContainer* idl_object = TRY(impl_from(vm));

    auto arg0 = vm.argument(0);
    CredentialCreationOptions options = CredentialCreationOptions {};
    if (!arg0.is_undefined())
        options = TRY(throw_dom_exception_if_needed(vm, [&] { return convert_to_idl_value_for_credential_creation_options(vm, arg0); }));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->create(options); }));
        return R;
    };

    auto maybe_R = steps();

    // And then, if an exception E was thrown:
    // 1. If op has a return type that is a promise type, then return ! Call(%Promise.reject%, %Promise%, «E»).
    // 2. Otherwise, end these steps and allow the exception to propagate.
    if (maybe_R.is_throw_completion())
        return WebIDL::create_rejected_promise(realm, maybe_R.error_value())->promise();

    return GC::Ref { as<JS::Promise>(*maybe_R.release_value()->promise()) };
}

JS_DEFINE_NATIVE_FUNCTION(CredentialsContainerPrototype::prevent_silent_access)
{
    WebIDL::log_trace(vm, "CredentialsContainerPrototype::prevent_silent_access");
    [[maybe_unused]] auto& realm = *vm.current_realm();
    auto steps = [&realm, &vm]() -> JS::ThrowCompletionOr<GC::Ref<WebIDL::Promise>> {
        (void)realm;
    [[maybe_unused]] CredentialManagement::CredentialsContainer* idl_object = TRY(impl_from(vm));

    [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] { return idl_object->prevent_silent_access(); }));
        return R;
    };

    auto maybe_R = steps();

    // And then, if an exception E was thrown:
    // 1. If op has a return type that is a promise type, then return ! Call(%Promise.reject%, %Promise%, «E»).
    // 2. Otherwise, end these steps and allow the exception to propagate.
    if (maybe_R.is_throw_completion())
        return WebIDL::create_rejected_promise(realm, maybe_R.error_value())->promise();

    return GC::Ref { as<JS::Promise>(*maybe_R.release_value()->promise()) };
}

// https://webidl.spec.whatwg.org/#es-dictionary
JS::ThrowCompletionOr<CredentialRequestOptions> convert_to_idl_value_for_credential_request_options(JS::VM& vm, JS::Value js_dict)
{
    // 1. If jsDict is not an Object and jsDict is neither undefined nor null, then throw a TypeError.
    if (!js_dict.is_object() && !js_dict.is_nullish())
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "CredentialRequestOptions");

    // 2. Let idlDict be an empty ordered map, representing a dictionary of type D.
    // 3. Let dictionaries be a list consisting of D and all of D’s inherited dictionaries, in order from least to most derived.
    // 4. For each dictionary dictionary in dictionaries, in order:
    // NB: We defer construction until the return initializer because some members may not be default-constructible. Inherited dictionaries are represented by the generated C++ struct inheritance.

    // 5. Return idlDict.
    return CredentialRequestOptions {
        .federated = TRY([&]() -> JS::ThrowCompletionOr<Optional<FederatedCredentialRequestOptions>> {
            // 1. Let key be the identifier of member.
            // 2. If jsDict is either undefined or null, then:
            //     1. Let jsMemberValue be undefined.
            // 3. Otherwise,
            //     1. Let jsMemberValue be ? Get(jsDict, key).
            auto js_member_value = JS::js_undefined();
            if (js_dict.is_object())
                js_member_value = TRY(js_dict.as_object().get("federated"_utf16_fly_string));

            // 4. If jsMemberValue is not undefined, then:
            if (!js_member_value.is_undefined()) {
                // 1. Let idlMemberValue be the result of converting jsMemberValue to an IDL value whose type is the type member is declared to be of.
                auto idl_member_value = TRY(throw_dom_exception_if_needed(vm, [&] { return convert_to_idl_value_for_federated_credential_request_options(vm, js_member_value); }));

                // 2. Set idlDict[key] to idlMemberValue.
                return idl_member_value;
            }
            // 7. Otherwise, jsMemberValue is undefined and the member is optional.
            return OptionalNone {};
        }()),
        .mediation = TRY([&]() -> JS::ThrowCompletionOr<CredentialMediationRequirement> {
            // 1. Let key be the identifier of member.
            // 2. If jsDict is either undefined or null, then:
            //     1. Let jsMemberValue be undefined.
            // 3. Otherwise,
            //     1. Let jsMemberValue be ? Get(jsDict, key).
            auto js_member_value = JS::js_undefined();
            if (js_dict.is_object())
                js_member_value = TRY(js_dict.as_object().get("mediation"_utf16_fly_string));

            // 4. If jsMemberValue is not undefined, then:
            if (!js_member_value.is_undefined()) {
                // 1. Let idlMemberValue be the result of converting jsMemberValue to an IDL value whose type is the type member is declared to be of.
                auto idl_member_value = TRY(throw_dom_exception_if_needed(vm, [&] { return convert_to_idl_value_for_credential_mediation_requirement(vm, js_member_value); }));

                // 2. Set idlDict[key] to idlMemberValue.
                return idl_member_value;
            }
            // 5. Otherwise, if jsMemberValue is undefined but member has a default value, then:
            // 1. Let idlMemberValue be the result of converting member's default value to an IDL value whose type is the type member is declared to be of.
            auto idl_member_value = CredentialMediationRequirement::Optional;

            // 2. Set idlDict[key] to idlMemberValue.
            return idl_member_value;
        }()),
        .password = TRY([&]() -> JS::ThrowCompletionOr<bool> {
            // 1. Let key be the identifier of member.
            // 2. If jsDict is either undefined or null, then:
            //     1. Let jsMemberValue be undefined.
            // 3. Otherwise,
            //     1. Let jsMemberValue be ? Get(jsDict, key).
            auto js_member_value = JS::js_undefined();
            if (js_dict.is_object())
                js_member_value = TRY(js_dict.as_object().get("password"_utf16_fly_string));

            // 4. If jsMemberValue is not undefined, then:
            if (!js_member_value.is_undefined()) {
                // 1. Let idlMemberValue be the result of converting jsMemberValue to an IDL value whose type is the type member is declared to be of.
                auto idl_member_value = TRY(throw_dom_exception_if_needed(vm, [&] { return js_member_value.to_boolean(); }));

                // 2. Set idlDict[key] to idlMemberValue.
                return idl_member_value;
            }
            // 5. Otherwise, if jsMemberValue is undefined but member has a default value, then:
            // 1. Let idlMemberValue be the result of converting member's default value to an IDL value whose type is the type member is declared to be of.
            auto idl_member_value = false;

            // 2. Set idlDict[key] to idlMemberValue.
            return idl_member_value;
        }()),
        .signal = TRY([&]() -> JS::ThrowCompletionOr<GC::Ptr<DOM::AbortSignal>> {
            // 1. Let key be the identifier of member.
            // 2. If jsDict is either undefined or null, then:
            //     1. Let jsMemberValue be undefined.
            // 3. Otherwise,
            //     1. Let jsMemberValue be ? Get(jsDict, key).
            auto js_member_value = JS::js_undefined();
            if (js_dict.is_object())
                js_member_value = TRY(js_dict.as_object().get("signal"_utf16_fly_string));

            // 4. If jsMemberValue is not undefined, then:
            if (!js_member_value.is_undefined()) {
                // 1. Let idlMemberValue be the result of converting jsMemberValue to an IDL value whose type is the type member is declared to be of.
                auto idl_member_value = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<GC::Ref<DOM::AbortSignal>> {
        if (auto impl = js_member_value.as_if<DOM::AbortSignal>())
            return *impl;
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "AbortSignal");
    }(); }));

                // 2. Set idlDict[key] to idlMemberValue.
                return idl_member_value;
            }
            // 7. Otherwise, jsMemberValue is undefined and the member is optional.
            return nullptr;
        }()),
    };
}

// https://webidl.spec.whatwg.org/#es-dictionary
JS::ThrowCompletionOr<CredentialCreationOptions> convert_to_idl_value_for_credential_creation_options(JS::VM& vm, JS::Value js_dict)
{
    // 1. If jsDict is not an Object and jsDict is neither undefined nor null, then throw a TypeError.
    if (!js_dict.is_object() && !js_dict.is_nullish())
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "CredentialCreationOptions");

    // 2. Let idlDict be an empty ordered map, representing a dictionary of type D.
    // 3. Let dictionaries be a list consisting of D and all of D’s inherited dictionaries, in order from least to most derived.
    // 4. For each dictionary dictionary in dictionaries, in order:
    // NB: We defer construction until the return initializer because some members may not be default-constructible. Inherited dictionaries are represented by the generated C++ struct inheritance.

    // 5. Return idlDict.
    return CredentialCreationOptions {
        .federated = TRY([&]() -> JS::ThrowCompletionOr<Optional<FederatedCredentialInit>> {
            // 1. Let key be the identifier of member.
            // 2. If jsDict is either undefined or null, then:
            //     1. Let jsMemberValue be undefined.
            // 3. Otherwise,
            //     1. Let jsMemberValue be ? Get(jsDict, key).
            auto js_member_value = JS::js_undefined();
            if (js_dict.is_object())
                js_member_value = TRY(js_dict.as_object().get("federated"_utf16_fly_string));

            // 4. If jsMemberValue is not undefined, then:
            if (!js_member_value.is_undefined()) {
                // 1. Let idlMemberValue be the result of converting jsMemberValue to an IDL value whose type is the type member is declared to be of.
                auto idl_member_value = TRY(throw_dom_exception_if_needed(vm, [&] { return convert_to_idl_value_for_federated_credential_init(vm, js_member_value); }));

                // 2. Set idlDict[key] to idlMemberValue.
                return idl_member_value;
            }
            // 7. Otherwise, jsMemberValue is undefined and the member is optional.
            return OptionalNone {};
        }()),
        .mediation = TRY([&]() -> JS::ThrowCompletionOr<CredentialMediationRequirement> {
            // 1. Let key be the identifier of member.
            // 2. If jsDict is either undefined or null, then:
            //     1. Let jsMemberValue be undefined.
            // 3. Otherwise,
            //     1. Let jsMemberValue be ? Get(jsDict, key).
            auto js_member_value = JS::js_undefined();
            if (js_dict.is_object())
                js_member_value = TRY(js_dict.as_object().get("mediation"_utf16_fly_string));

            // 4. If jsMemberValue is not undefined, then:
            if (!js_member_value.is_undefined()) {
                // 1. Let idlMemberValue be the result of converting jsMemberValue to an IDL value whose type is the type member is declared to be of.
                auto idl_member_value = TRY(throw_dom_exception_if_needed(vm, [&] { return convert_to_idl_value_for_credential_mediation_requirement(vm, js_member_value); }));

                // 2. Set idlDict[key] to idlMemberValue.
                return idl_member_value;
            }
            // 5. Otherwise, if jsMemberValue is undefined but member has a default value, then:
            // 1. Let idlMemberValue be the result of converting member's default value to an IDL value whose type is the type member is declared to be of.
            auto idl_member_value = CredentialMediationRequirement::Optional;

            // 2. Set idlDict[key] to idlMemberValue.
            return idl_member_value;
        }()),
        .password = TRY([&]() -> JS::ThrowCompletionOr<Optional<Variant<PasswordCredentialData, GC::Ref<HTML::HTMLFormElement>>>> {
            // 1. Let key be the identifier of member.
            // 2. If jsDict is either undefined or null, then:
            //     1. Let jsMemberValue be undefined.
            // 3. Otherwise,
            //     1. Let jsMemberValue be ? Get(jsDict, key).
            auto js_member_value = JS::js_undefined();
            if (js_dict.is_object())
                js_member_value = TRY(js_dict.as_object().get("password"_utf16_fly_string));

            // 4. If jsMemberValue is not undefined, then:
            if (!js_member_value.is_undefined()) {
                // 1. Let idlMemberValue be the result of converting jsMemberValue to an IDL value whose type is the type member is declared to be of.
                auto idl_member_value = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<Variant<PasswordCredentialData, GC::Ref<HTML::HTMLFormElement>>> {

        if (js_member_value.is_nullish()) {
            return Variant<PasswordCredentialData, GC::Ref<HTML::HTMLFormElement>> { TRY(convert_to_idl_value_for_password_credential_data(vm, js_member_value)) };
        }

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

            if (is<PlatformObject>(object)) {

                if (auto* result = as_if<HTML::HTMLFormElement>(object))
                    return Variant<PasswordCredentialData, GC::Ref<HTML::HTMLFormElement>> { GC::Ref { *result } };
            }
            return Variant<PasswordCredentialData, GC::Ref<HTML::HTMLFormElement>> { TRY(convert_to_idl_value_for_password_credential_data(vm, js_member_value)) };
        }

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

                // 2. Set idlDict[key] to idlMemberValue.
                return idl_member_value;
            }
            // 7. Otherwise, jsMemberValue is undefined and the member is optional.
            return OptionalNone {};
        }()),
        .signal = TRY([&]() -> JS::ThrowCompletionOr<GC::Ptr<DOM::AbortSignal>> {
            // 1. Let key be the identifier of member.
            // 2. If jsDict is either undefined or null, then:
            //     1. Let jsMemberValue be undefined.
            // 3. Otherwise,
            //     1. Let jsMemberValue be ? Get(jsDict, key).
            auto js_member_value = JS::js_undefined();
            if (js_dict.is_object())
                js_member_value = TRY(js_dict.as_object().get("signal"_utf16_fly_string));

            // 4. If jsMemberValue is not undefined, then:
            if (!js_member_value.is_undefined()) {
                // 1. Let idlMemberValue be the result of converting jsMemberValue to an IDL value whose type is the type member is declared to be of.
                auto idl_member_value = TRY(throw_dom_exception_if_needed(vm, [&] { return [&]() -> JS::ThrowCompletionOr<GC::Ref<DOM::AbortSignal>> {
        if (auto impl = js_member_value.as_if<DOM::AbortSignal>())
            return *impl;
        return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "AbortSignal");
    }(); }));

                // 2. Set idlDict[key] to idlMemberValue.
                return idl_member_value;
            }
            // 7. Otherwise, jsMemberValue is undefined and the member is optional.
            return nullptr;
        }()),
    };
}

} // namespace Web::Bindings
