/*
 * Copyright (c) 2025, Miguel Sacristán Izcue <miguel_tete17@hotmail.com>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibWeb/TrustedTypes/TrustedTypePolicy.h>

#include <LibGC/Ptr.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/Value.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/TagNames.h>
#include <LibWeb/HTML/WindowOrWorkerGlobalScope.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/SVG/TagNames.h>
#include <LibWeb/TrustedTypes/RequireTrustedTypesForDirective.h>
#include <LibWeb/TrustedTypes/TrustedHTML.h>
#include <LibWeb/TrustedTypes/TrustedScript.h>
#include <LibWeb/TrustedTypes/TrustedScriptURL.h>
#include <LibWeb/TrustedTypes/TrustedTypePolicyFactory.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/CallbackType.h>
#include <LibWeb/WebIDL/ExceptionOr.h>

namespace Web::TrustedTypes {

GC_DEFINE_ALLOCATOR(TrustedTypePolicy);

TrustedTypePolicy::TrustedTypePolicy(JS::Realm& realm, Utf16String const& name, Bindings::TrustedTypePolicyOptions const& options)
    : PlatformObject(realm)
    , m_name(name)
    , m_create_html(options.create_html)
    , m_create_script(options.create_script)
    , m_create_script_url(options.create_script_url)
{
}

void TrustedTypePolicy::initialize(JS::Realm& realm)
{
    WEB_SET_PROTOTYPE_FOR_INTERFACE(TrustedTypePolicy);
    Base::initialize(realm);
}

void TrustedTypePolicy::visit_edges(Visitor& visitor)
{
    Base::visit_edges(visitor);
    visitor.visit(m_create_html);
    visitor.visit(m_create_script);
    visitor.visit(m_create_script_url);
}

Utf16String to_string(TrustedTypeName trusted_type_name)
{
    switch (trusted_type_name) {
    case TrustedTypeName::TrustedHTML:
        return "TrustedHTML"_utf16;
    case TrustedTypeName::TrustedScript:
        return "TrustedScript"_utf16;
    case TrustedTypeName::TrustedScriptURL:
        return "TrustedScriptURL"_utf16;
    default:
        VERIFY_NOT_REACHED();
    }
}
// https://w3c.github.io/trusted-types/dist/spec/#dom-trustedtypepolicy-createhtml
WebIDL::ExceptionOr<GC::Root<TrustedHTML>> TrustedTypePolicy::create_html(Utf16String const& input, GC::RootVector<JS::Value> const& arguments)
{
    // 1. Returns the result of executing the Create a Trusted Type algorithm, with the following arguments:
    //    policy
    //      this value
    //    trustedTypeName
    //      "TrustedHTML"
    //    value
    //      input
    //    arguments
    //      arguments
    auto const trusted_type = TRY(create_a_trusted_type(TrustedTypeName::TrustedHTML, input, arguments));
    return trusted_type.get<GC::Root<TrustedHTML>>();
}

// https://w3c.github.io/trusted-types/dist/spec/#dom-trustedtypepolicy-createscript
WebIDL::ExceptionOr<GC::Root<TrustedScript>> TrustedTypePolicy::create_script(Utf16String const& input, GC::RootVector<JS::Value> const& arguments)
{
    // 1. Returns the result of executing the Create a Trusted Type algorithm, with the following arguments:
    //    policy
    //      this value
    //    trustedTypeName
    //      "TrustedScript"
    //    value
    //      input
    //    arguments
    //      arguments
    auto const trusted_type = TRY(create_a_trusted_type(TrustedTypeName::TrustedScript, input, arguments));
    return trusted_type.get<GC::Root<TrustedScript>>();
}

// https://w3c.github.io/trusted-types/dist/spec/#dom-trustedtypepolicy-createscripturl
WebIDL::ExceptionOr<GC::Root<TrustedScriptURL>> TrustedTypePolicy::create_script_url(Utf16String const& input, GC::RootVector<JS::Value> const& arguments)
{
    // 1. Returns the result of executing the Create a Trusted Type algorithm, with the following arguments:
    //    policy
    //      this value
    //    trustedTypeName
    //      "TrustedScriptURL"
    //    value
    //      input
    //    arguments
    //      arguments
    auto const trusted_type = TRY(create_a_trusted_type(TrustedTypeName::TrustedScriptURL, input, arguments));
    return trusted_type.get<GC::Root<TrustedScriptURL>>();
}

// https://w3c.github.io/trusted-types/dist/spec/#create-a-trusted-type-algorithm
TrustedTypesVariants TrustedTypePolicy::create_a_trusted_type(TrustedTypeName trusted_type_name, Utf16String const& value, GC::RootVector<JS::Value> const& arguments)
{
    auto& vm = this->vm();
    auto& realm = this->realm();

    // 1. Let policyValue be the result of executing Get Trusted Type policy value with the same arguments
    // as this algorithm and additionally true as throwIfMissing.
    // 2. If the algorithm threw an error, rethrow the error and abort the following steps.
    auto const policy_value = TRY(get_trusted_type_policy_value(trusted_type_name, value, arguments, ThrowIfCallbackMissing::Yes));

    // 3. Let dataString be the result of stringifying policyValue.
    Utf16String data_string;
    switch (trusted_type_name) {
    case TrustedTypeName::TrustedHTML:
    case TrustedTypeName::TrustedScript:
        data_string = TRY(WebIDL::to_utf16_string(vm, policy_value));
        break;
    case TrustedTypeName::TrustedScriptURL:
        data_string = TRY(WebIDL::to_utf16_usv_string(vm, policy_value));
        break;
    default:
        VERIFY_NOT_REACHED();
    }

    // 4. If policyValue is null or undefined, set dataString to the empty string.
    if (policy_value.is_nullish())
        data_string = ""_utf16;

    // 5. Return a new instance of an interface with a type name trustedTypeName, with its associated data value set to dataString.
    switch (trusted_type_name) {
    case TrustedTypeName::TrustedHTML:
        return realm.create<TrustedHTML>(realm, move(data_string));
    case TrustedTypeName::TrustedScript:
        return realm.create<TrustedScript>(realm, move(data_string));
    case TrustedTypeName::TrustedScriptURL:
        return realm.create<TrustedScriptURL>(realm, move(data_string));
    default:
        VERIFY_NOT_REACHED();
    }
}

// https://w3c.github.io/trusted-types/dist/spec/#get-trusted-type-policy-value
WebIDL::ExceptionOr<JS::Value> TrustedTypePolicy::get_trusted_type_policy_value(TrustedTypeName trusted_type_name, Utf16String const& value, GC::RootVector<JS::Value> const& values, ThrowIfCallbackMissing throw_if_missing)
{
    auto& vm = this->vm();

    // 1. Let functionName be a function name for the given trustedTypeName, based on the following table:
    // 2. Let function be policy’s options[functionName].
    GC::Ptr<WebIDL::CallbackType> function;
    switch (trusted_type_name) {
    case TrustedTypeName::TrustedHTML:
        function = m_create_html;
        break;
    case TrustedTypeName::TrustedScript:
        function = m_create_script;
        break;
    case TrustedTypeName::TrustedScriptURL:
        function = m_create_script_url;
        break;
    default:
        VERIFY_NOT_REACHED();
    }

    // 3. If function is null, then:
    if (!function) {
        // 1. If throwIfMissing throw a TypeError.
        if (throw_if_missing == ThrowIfCallbackMissing::Yes)
            return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Trying to create a trusted type without a callback"_string };

        // 2. Else return null
        return JS::js_null();
    }

    // 4. Let args be << value >>.
    GC::RootVector<JS::Value> args(heap());
    args.append(JS::PrimitiveString::create(vm, value));

    // 5. Append each item in arguments to args.
    args.extend(values);

    // 6. Let policyValue be the result of invoking function with args and "rethrow".
    auto const policy_value = TRY(WebIDL::invoke_callback(*function, {}, WebIDL::ExceptionBehavior::Rethrow, args));

    // 7. Return policyValue.
    return policy_value;
}

// https://www.w3.org/TR/trusted-types/#process-value-with-a-default-policy-algorithm
WebIDL::ExceptionOr<Optional<TrustedType>> process_value_with_a_default_policy(TrustedTypeName trusted_type_name, JS::Object& global, Variant<GC::Root<TrustedHTML>, GC::Root<TrustedScript>, GC::Root<TrustedScriptURL>, Utf16String> input, InjectionSink sink)
{
    auto& vm = global.vm();
    auto& realm = HTML::relevant_realm(global);

    // 1. Let defaultPolicy be the value of global’s trusted type policy factory’s default policy.
    auto const& default_policy = as<HTML::WindowOrWorkerGlobalScopeMixin>(global).trusted_types()->default_policy();

    // This algorithm routes a value to be assigned to an injection sink through a default policy, should one exist.
    // FIXME: Open an issue upstream. It is not immediately clear what to do if the default policy does not exist.
    // Ref: https://github.com/w3c/trusted-types/issues/595
    if (!default_policy)
        return Optional<TrustedType> {};

    // 2. Let policyValue be the result of executing Get Trusted Type policy value, with the following arguments:
    //    policy:
    //      defaultPolicy
    //    value:
    //      stringified input
    //    trustedTypeName:
    //      expectedType’s type name
    //    arguments:
    //      « trustedTypeName, sink »
    //    throwIfMissing:
    //      false
    //  3. If the algorithm threw an error, rethrow the error and abort the following steps.
    auto arguments = GC::RootVector<JS::Value>(vm.heap());
    arguments.append(JS::PrimitiveString::create(vm, to_string(trusted_type_name)));
    arguments.append(JS::PrimitiveString::create(vm, to_string(sink)));
    auto policy_value = TRY(default_policy->get_trusted_type_policy_value(
        trusted_type_name,
        input.visit(
            [](auto& value) { return value->to_string(); },
            [](Utf16String& value) { return value; }),
        arguments,
        ThrowIfCallbackMissing::No));

    //  4. If policyValue is null or undefined, return policyValue.
    if (policy_value.is_nullish())
        return Optional<TrustedType> {};

    //  5. Let dataString be the result of stringifying policyValue.
    Utf16String data_string;
    switch (trusted_type_name) {
    case TrustedTypeName::TrustedHTML:
    case TrustedTypeName::TrustedScript:
        data_string = TRY(WebIDL::to_utf16_string(vm, policy_value));
        break;
    case TrustedTypeName::TrustedScriptURL:
        data_string = TRY(WebIDL::to_utf16_usv_string(vm, policy_value));
        break;
    default:
        VERIFY_NOT_REACHED();
    }

    //  6. Return a new instance of an interface with a type name trustedTypeName, with its associated data value set to dataString.
    switch (trusted_type_name) {
    case TrustedTypeName::TrustedHTML:
        return realm.create<TrustedHTML>(realm, move(data_string));
    case TrustedTypeName::TrustedScript:
        return realm.create<TrustedScript>(realm, move(data_string));
    case TrustedTypeName::TrustedScriptURL:
        return realm.create<TrustedScriptURL>(realm, move(data_string));
    }
    VERIFY_NOT_REACHED();
}

// https://www.w3.org/TR/trusted-types/#get-trusted-type-compliant-string-algorithm
WebIDL::ExceptionOr<Utf16String> get_trusted_type_compliant_string(TrustedTypeName expected_type, JS::Object& global, Variant<GC::Root<TrustedHTML>, GC::Root<TrustedScript>, GC::Root<TrustedScriptURL>, Utf16String> input, InjectionSink sink, String sink_group)
{
    // 1. If input is an instance of expectedType, return stringified input and abort these steps.
    switch (expected_type) {
    case TrustedTypeName::TrustedHTML:
        if (auto* const value = input.get_pointer<GC::Root<TrustedHTML>>(); value)
            return (*value)->to_string();
        break;
    case TrustedTypeName::TrustedScript:
        if (auto* const value = input.get_pointer<GC::Root<TrustedScript>>(); value)
            return (*value)->to_string();
        break;
    case TrustedTypeName::TrustedScriptURL:
        if (auto* const value = input.get_pointer<GC::Root<TrustedScriptURL>>(); value)
            return (*value)->to_string();
        break;
    }

    // 2. Let requireTrustedTypes be the result of executing Does sink type require trusted types? algorithm, passing global, sinkGroup, and true.
    auto const require_trusted_types = does_sink_require_trusted_types(global, sink_group, IncludeReportOnlyPolicies::Yes);

    // 3. If requireTrustedTypes is false, return stringified input and abort these steps.
    if (!require_trusted_types)
        return input.visit(
            [](auto const& value) {
                return value->to_string();
            },
            [](Utf16String const& value) {
                return value;
            });

    // 4. Let convertedInput be the result of executing Process value with a default policy with the same arguments as this algorithm.
    // 5. If the algorithm threw an error, rethrow the error and abort the following steps.
    auto const converted_input = TRY(process_value_with_a_default_policy(expected_type, global, input, sink));

    // 6. If convertedInput is null or undefined, execute the following steps:
    if (!converted_input.has_value()) {
        // 1. Let disposition be the result of executing Should sink type mismatch violation be blocked by Content Security Policy?
        //    algorithm, passing global, stringified input as source, sinkGroup and sink.
        auto const disposition = should_sink_type_mismatch_violation_be_blocked_by_content_security_policy(
            global,
            sink,
            sink_group,
            input.visit(
                [](auto const& value) {
                    return value->to_string();
                },
                [](Utf16String const& value) {
                    return value;
                }));

        // 2. If disposition is “Allowed”, return stringified input and abort further steps.
        if (disposition == ContentSecurityPolicy::Directives::Directive::Result::Allowed) {
            return input.visit(
                [](auto const& value) {
                    return value->to_string();
                },
                [](Utf16String const& value) {
                    return value;
                });
        }

        // 3. Throw a TypeError and abort further steps.
        return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Sink {} of type {} requires a TrustedType to be used", to_string(sink), sink_group)) };
    }

    // 7. Assert: convertedInput is an instance of expectedType.
    // 8. Return stringified convertedInput.
    VERIFY(converted_input.has_value());
    return converted_input.value().visit([&]<typename Type>(Type const& trusted_type) {
        switch (expected_type) {
        case TrustedTypeName::TrustedHTML:
            VERIFY(IsSame<Type, GC::Root<TrustedHTML>>);
            break;
        case TrustedTypeName::TrustedScript:
            VERIFY(IsSame<Type, GC::Root<TrustedScript>>);
            break;
        case TrustedTypeName::TrustedScriptURL:
            VERIFY(IsSame<Type, GC::Root<TrustedScriptURL>>);
            break;
        }
        return trusted_type->to_string();
    });
}

// https://w3c.github.io/trusted-types/dist/spec/#validate-attribute-mutation
WebIDL::ExceptionOr<Utf16String> get_trusted_types_compliant_attribute_value(FlyString const& attribute_name, Optional<Utf16String> attribute_ns, DOM::Element const& element, Variant<GC::Root<TrustedHTML>, GC::Root<TrustedScript>, GC::Root<TrustedScriptURL>, Utf16String> const& new_value)
{
    // 1. If attributeNs is the empty string, set attributeNs to null.
    if (attribute_ns.has_value() && attribute_ns.value().is_empty())
        attribute_ns.clear();

    // 2. Set attributeData to the result of Get Trusted Type data for attribute algorithm, with the following arguments:
    //    element
    //    attributeName
    //    attributeNs
    auto const attribute_data = get_trusted_type_data_for_attribute(
        element_interface(
            Utf16String::from_utf8(element.local_name()),
            element.namespace_uri().value_or(Namespace::HTML)),
        Utf16String::from_utf8(attribute_name),
        attribute_ns);

    // 3. If attributeData is null, then:
    if (!attribute_data.has_value()) {
        // 1. If newValue is a string, return newValue.
        if (new_value.has<Utf16String>())
            return new_value.get<Utf16String>();

        // 2. Assert: newValue is TrustedHTML or TrustedScript or TrustedScriptURL.
        VERIFY(new_value.has<GC::Root<TrustedHTML>>() || new_value.has<GC::Root<TrustedScript>>() || new_value.has<GC::Root<TrustedScriptURL>>());

        // 3. Return value’s associated data.
        // FIXME: This is badly worded in the spec it should say "Return stringified newvalues's"
        return new_value.downcast<TrustedType>().visit([](auto& value) { return value->to_string(); });
    }

    // 4. Let expectedType be the value of the fourth member of attributeData.
    auto const expected_type = attribute_data->trusted_type;

    // 5. Let sink be the value of the fifth member of attributeData.
    auto const sink = attribute_data->sink;

    // 6. Return the result of executing Get Trusted Type compliant string with the following arguments:
    //      expectedType
    //      newValue as input
    //      element’s node document’s relevant global object as global
    //      sink
    //      'script' as sinkGroup
    return get_trusted_type_compliant_string(
        expected_type,
        HTML::relevant_global_object(element.document()),
        new_value,
        sink,
        Script.to_string());
}

ElementInterface element_interface(Utf16String const& local_name, FlyString const& element_ns)
{
    // FIXME: We don't have a method in ElementFactory that can give us the interface name but these are all the cases
    // we care about in the table in get_trusted_type_data_for_attribute function
    if (local_name == HTML::TagNames::iframe && element_ns == Namespace::HTML)
        return { "HTMLIFrameElement"_utf16, element_ns };
    if (local_name == HTML::TagNames::script && element_ns == Namespace::HTML)
        return { "HTMLScriptElement"_utf16, element_ns };
    if (local_name == SVG::TagNames::script && element_ns == Namespace::SVG)
        return { "SVGScriptElement"_utf16, element_ns };

    return { "Element"_utf16, element_ns };
}

}
