/*
 * Copyright (c) 2026-present, the Ladybird developers.
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/QuickSort.h>
#include <LibDevTools/Actors/StorageActor.h>
#include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/DevToolsDelegate.h>
#include <LibDevTools/DevToolsServer.h>
#include <LibDevTools/StorageHelpers.h>

namespace DevTools {

static constexpr auto max_store_object_count = 50uz;

static JsonObject serialize_storage_item(DevToolsDelegate::StorageItem const& item)
{
    JsonObject object;
    object.set("name"sv, item.name);
    object.set("value"sv, item.value);
    return object;
}

static Optional<String> string_from_devtools_value(JsonValue const& value)
{
    if (value.is_string())
        return value.as_string();
    if (value.is_bool())
        return value.as_bool() ? "true"_string : "false"_string;
    if (value.is_number())
        return MUST(String::formatted("{}", value.get_number_with_precision_loss<double>().release_value()));
    if (value.is_null())
        return {};
    return value.serialized();
}

NonnullRefPtr<StorageActor> StorageActor::create(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab, Web::StorageAPI::StorageEndpointType storage_endpoint)
{
    return adopt_ref(*new StorageActor(devtools, move(name), move(tab), storage_endpoint));
}

StorageActor::StorageActor(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab, Web::StorageAPI::StorageEndpointType storage_endpoint)
    : Actor(devtools, move(name))
    , m_tab(move(tab))
    , m_storage_endpoint(storage_endpoint)
{
    if (auto tab = m_tab.strong_ref()) {
        m_storage_change_listener_id = devtools.delegate().add_storage_change_listener(
            tab->description(),
            weak_callback(*this, [](auto& self, DevToolsDelegate::StorageChange change) {
                self.on_storage_changed(move(change));
            }));
    }
}

StorageActor::~StorageActor()
{
    if (m_storage_change_listener_id == 0)
        return;

    if (auto tab = m_tab.strong_ref())
        devtools().delegate().remove_storage_change_listener(tab->description(), m_storage_change_listener_id);
}

StringView StorageActor::resource_type() const
{
    if (m_storage_endpoint == Web::StorageAPI::StorageEndpointType::LocalStorage)
        return "local-storage"sv;
    VERIFY(m_storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage);
    return "session-storage"sv;
}

StringView StorageActor::resource_key() const
{
    if (m_storage_endpoint == Web::StorageAPI::StorageEndpointType::LocalStorage)
        return "localStorage"sv;
    VERIFY(m_storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage);
    return "sessionStorage"sv;
}

Optional<String> StorageActor::host() const
{
    auto tab = m_tab.strong_ref();
    if (!tab)
        return {};
    return storage_host_for_url(tab->description().url);
}

JsonObject StorageActor::serialize_storage() const
{
    JsonObject hosts;
    if (auto storage_host = host(); storage_host.has_value())
        hosts.set(*storage_host, JsonArray {});

    JsonObject traits;
    traits.set("supportsAddItem"sv, true);
    traits.set("supportsRemoveAll"sv, true);
    traits.set("supportsRemoveAllSessionCookies"sv, false);
    traits.set("supportsRemoveItem"sv, true);

    JsonObject storage;
    storage.set("actor"sv, name());
    if (auto tab = m_tab.strong_ref()) {
        storage.set("browsingContextID"sv, tab->description().id);
        storage.set("innerWindowId"sv, tab->inner_window_id());
        storage.set("resourceId"sv, MUST(String::formatted("{}-{}", resource_key(), tab->inner_window_id())));
    }
    storage.set("hosts"sv, move(hosts));
    storage.set("resourceKey"sv, resource_key());
    storage.set("traits"sv, move(traits));
    return storage;
}

void StorageActor::handle_message(Message const& message)
{
    if (message.type == "getFields"sv) {
        get_fields(message);
        return;
    }

    if (message.type == "getStoreObjects"sv) {
        get_store_objects(message);
        return;
    }

    if (message.type == "editItem"sv) {
        edit_item(message);
        return;
    }

    if (message.type == "addItem"sv) {
        add_item(message);
        return;
    }

    if (message.type == "removeItem"sv) {
        remove_item(message);
        return;
    }

    if (message.type == "removeAll"sv) {
        remove_all(message);
        return;
    }

    send_unrecognized_packet_type_error(message);
}

void StorageActor::get_fields(Message const& message)
{
    JsonArray fields;
    fields.must_append(define_storage_field("name"sv, StorageFieldType::Mutable));
    fields.must_append(define_storage_field("value"sv, StorageFieldType::Mutable));

    JsonObject response;
    response.set("value"sv, move(fields));
    send_response(message, move(response));
}

void StorageActor::get_store_objects(Message const& message)
{
    auto requested_host_ref = get_required_parameter<String>(message, "host"sv);
    if (!requested_host_ref.has_value())
        return;
    String requested_host = *requested_host_ref;

    auto names_value = message.data.get("names"sv);
    Optional<JsonArray> requested_names;
    if (names_value.has_value() && names_value->is_array())
        requested_names = names_value->as_array();

    JsonObject options;
    auto options_value = message.data.get("options"sv);
    if (options_value.has_value() && options_value->is_object())
        options = options_value->as_object();

    auto tab = m_tab.strong_ref();
    if (!tab) {
        send_store_objects(message, requested_host, move(requested_names), move(options), Vector<DevToolsDelegate::StorageItem> {});
        return;
    }

    devtools().delegate().inspect_storage(
        tab->description(),
        m_storage_endpoint,
        weak_callback(*this, [message, requested_host, requested_names, options](auto& self, ErrorOr<Vector<DevToolsDelegate::StorageItem>> storage_items) {
            self.send_store_objects(message, requested_host, requested_names, options, move(storage_items));
        }));
}

void StorageActor::send_store_objects(Message const& message, Optional<String> requested_host, Optional<JsonArray> requested_names, JsonObject options, ErrorOr<Vector<DevToolsDelegate::StorageItem>> storage_items_or_error)
{
    if (storage_items_or_error.is_error()) {
        JsonObject response;
        response.set("error"sv, "storageError"sv);
        response.set("message"sv, storage_items_or_error.error().string_literal());
        send_response(message, move(response));
        return;
    }

    Vector<DevToolsDelegate::StorageItem> storage_items = storage_items_or_error.release_value();
    if (!host().has_value() || !requested_host.has_value() || *host() != *requested_host)
        storage_items.clear();

    if (requested_names.has_value()) {
        storage_items.remove_all_matching([&](auto const& item) {
            return !requested_names->values().contains([&](auto const& value) {
                return value.is_string() && value.as_string() == item.name;
            });
        });
    }

    quick_sort(storage_items, [](auto const& a, auto const& b) {
        return a.name < b.name;
    });

    auto total = storage_items.size();
    auto offset = options.get_integer<size_t>("offset"sv).value_or(0);
    auto size = options.get_integer<size_t>("size"sv).value_or(max_store_object_count);
    if (size > max_store_object_count)
        size = max_store_object_count;

    JsonArray data;
    if (offset > total) {
        offset = total;
    } else {
        auto end = min(total, offset + size);
        for (auto i = offset; i < end; ++i)
            data.must_append(serialize_storage_item(storage_items[i]));
    }

    JsonObject response;
    response.set("offset"sv, offset);
    response.set("total"sv, total);
    response.set("data"sv, move(data));
    send_response(message, move(response));
}

void StorageActor::edit_item(Message const& message)
{
    auto data = get_required_parameter<JsonObject>(message, "data"sv);
    if (!data.has_value())
        return;

    auto requested_host = data->get_string("host"sv);
    if (!requested_host.has_value()) {
        send_missing_parameter_error(message, "host"sv);
        return;
    }

    if (!host().has_value() || *host() != *requested_host) {
        send_response(message, to_storage_operation_result(Error::from_string_literal("Storage host is not available")));
        return;
    }

    auto field = data->get_string("field"sv);
    if (!field.has_value()) {
        send_missing_parameter_error(message, "field"sv);
        return;
    }

    auto items = data->get_object("items"sv);
    if (!items.has_value()) {
        send_missing_parameter_error(message, "items"sv);
        return;
    }

    auto key = items->get_string("name"sv);
    if (!key.has_value()) {
        send_missing_parameter_error(message, "items.name"sv);
        return;
    }

    auto value_json = items->get("value"sv);
    if (!value_json.has_value()) {
        send_missing_parameter_error(message, "items.value"sv);
        return;
    }

    auto value = string_from_devtools_value(*value_json);
    if (!value.has_value()) {
        send_response(message, to_storage_operation_result(Error::from_string_literal("Missing storage value")));
        return;
    }

    auto tab = m_tab.strong_ref();
    if (!tab)
        return;

    if (*field == "name"sv) {
        auto old_value_json = data->get("oldValue"sv);
        if (!old_value_json.has_value()) {
            send_missing_parameter_error(message, "oldValue"sv);
            return;
        }

        auto old_key = string_from_devtools_value(*old_value_json);
        if (!old_key.has_value()) {
            send_response(message, to_storage_operation_result(Error::from_string_literal("Missing old storage key")));
            return;
        }

        if (*old_key != *key) {
            auto remove_result = devtools().delegate().remove_storage_item(tab->description(), m_storage_endpoint, *requested_host, *old_key);
            if (remove_result.is_error()) {
                send_response(message, to_storage_operation_result(remove_result.release_error()));
                return;
            }
        }
    }

    auto result = devtools().delegate().set_storage_item(tab->description(), m_storage_endpoint, *requested_host, *key, *value);
    if (result.is_error())
        send_response(message, to_storage_operation_result(result.release_error()));
    else
        send_response(message, to_storage_operation_result(ErrorOr<void> {}));
}

void StorageActor::add_item(Message const& message)
{
    auto guid = get_required_parameter<String>(message, "guid"sv);
    if (!guid.has_value())
        return;

    auto requested_host = message.data.get_string("host"sv)
                              .value_or_lazy_evaluated_optional([this] { return host(); });
    if (!requested_host.has_value()) {
        send_response(message, to_storage_operation_result(Error::from_string_literal("No storage host is available for this page")));
        return;
    }

    if (!host().has_value() || *host() != *requested_host) {
        send_response(message, to_storage_operation_result(Error::from_string_literal("Storage host is not available")));
        return;
    }

    auto tab = m_tab.strong_ref();
    if (!tab)
        return;

    auto result = devtools().delegate().set_storage_item(tab->description(), m_storage_endpoint, *requested_host, *guid, "value"_string);
    if (result.is_error())
        send_response(message, to_storage_operation_result(result.release_error()));
    else
        send_response(message, to_storage_operation_result(ErrorOr<void> {}));
}

void StorageActor::remove_item(Message const& message)
{
    auto requested_host = get_required_parameter<String>(message, "host"sv);
    if (!requested_host.has_value())
        return;

    auto name = get_required_parameter<String>(message, "name"sv);
    if (!name.has_value())
        return;

    if (!host().has_value() || *host() != *requested_host) {
        send_response(message, {});
        return;
    }

    auto tab = m_tab.strong_ref();
    if (!tab)
        return;

    auto result = devtools().delegate().remove_storage_item(tab->description(), m_storage_endpoint, *requested_host, *name);
    if (result.is_error())
        send_response(message, to_storage_operation_result(result.release_error()));
    else
        send_response(message, {});
}

void StorageActor::remove_all(Message const& message)
{
    auto requested_host = get_required_parameter<String>(message, "host"sv);
    if (!requested_host.has_value())
        return;

    if (!host().has_value() || *host() != *requested_host) {
        send_response(message, {});
        return;
    }

    auto tab = m_tab.strong_ref();
    if (!tab)
        return;

    auto result = devtools().delegate().clear_storage(tab->description(), m_storage_endpoint, *requested_host);
    if (result.is_error())
        send_response(message, to_storage_operation_result(result.release_error()));
    else
        send_response(message, {});
}

void StorageActor::on_storage_changed(DevToolsDelegate::StorageChange change)
{
    if (change.storage_endpoint != m_storage_endpoint)
        return;

    auto storage_host = host();
    if (!storage_host.has_value() || *storage_host != change.host)
        return;

    if (change.type == DevToolsDelegate::StorageChange::Type::Cleared) {
        send_store_cleared(change);
        return;
    }

    if (!change.key.has_value())
        return;

    send_store_update(change);
}

void StorageActor::send_store_update(DevToolsDelegate::StorageChange const& change)
{
    StringView update_type;
    switch (change.type) {
    case DevToolsDelegate::StorageChange::Type::Added:
        update_type = "added"sv;
        break;
    case DevToolsDelegate::StorageChange::Type::Changed:
        update_type = "changed"sv;
        break;
    case DevToolsDelegate::StorageChange::Type::Deleted:
        update_type = "deleted"sv;
        break;
    case DevToolsDelegate::StorageChange::Type::Cleared:
        VERIFY_NOT_REACHED();
    }

    JsonArray keys;
    keys.must_append(*change.key);

    JsonObject hosts;
    hosts.set(change.host, move(keys));

    JsonObject storage_updates;
    storage_updates.set(resource_key(), move(hosts));

    JsonObject data;
    data.set(update_type, move(storage_updates));

    JsonObject message;
    message.set("type"sv, "storesUpdate"sv);
    message.set("data"sv, move(data));
    send_message(move(message));
}

void StorageActor::send_store_cleared(DevToolsDelegate::StorageChange const& change)
{
    JsonArray hosts;
    hosts.must_append(change.host);

    JsonObject data;
    data.set("clearedHostsOrPaths"sv, move(hosts));

    JsonObject message;
    message.set("type"sv, "storesCleared"sv);
    message.set("data"sv, move(data));
    send_message(move(message));
}

}
