/*
 * Copyright (C) 2016 Canonical Ltd.
 * Copyright (C) 2026 UBports Foundation.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "RcloneProvider.h"

#include "RcloneClient.h"
#include "RcloneDownloadJob.h"
#include "RcloneItem.h"
#include "RcloneUploadJob.h"
#include "item_id.h"
#include "rclone_ops.h"
#include "vendors/LocalVendor.h"
#include "vendors/Vendor.h"

#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <lomiri/storage/provider/Exceptions.h>

using namespace std;
using namespace lomiri::storage::provider;

RcloneProvider::RcloneProvider()
    : client_(make_shared<RcloneClient>()) {}

RcloneProvider::RcloneProvider(string const& local_path)
    : client_(make_shared<RcloneClient>())
    , test_ctx_(LocalVendor::test_context(local_path)) {}

RcloneProvider::~RcloneProvider() = default;

// TODO: support multiple roots
boost::future<ItemList> RcloneProvider::roots(
    vector<string> const& metadata_keys, Context const& ctx)
{
    Q_UNUSED(metadata_keys);
    if (!vendor_) vendor_ = Vendor::for_context(test_ctx_ ? *test_ctx_ : ctx);
    auto promise = make_shared<boost::promise<ItemList>>();

    vendor_->with_fs(client_, promise, [client = client_, promise](string const& fs) {
        client->call(QStringLiteral("operations/stat"), RcloneClient::make_params(fs, ""),
                     [promise](QJsonObject const& result,
                               boost::exception_ptr const& error) {
            if (error)
            {
                promise->set_exception(error);
                return;
            }
            Item root = make_root_item(
                result.value(QStringLiteral("item")).toObject());
            promise->set_value(ItemList{root});
        });
    });
    return promise->get_future();
}

boost::future<tuple<ItemList, string>> RcloneProvider::list(
    string const& item_id, string const& page_token,
    vector<string> const& metadata_keys, Context const& ctx)
{
    Q_UNUSED(metadata_keys);
    if (!is_folder(item_id))
        throw LogicException(item_id + " is not a folder");
    if (!page_token.empty())
        throw InvalidArgumentException("Paging unsupported, but page token given: " + page_token);

    if (!vendor_) vendor_ = Vendor::for_context(test_ctx_ ? *test_ctx_ : ctx);
    string remote = id_to_remote_path(item_id);
    auto promise = make_shared<boost::promise<tuple<ItemList, string>>>();

    vendor_->with_fs(client_, promise, [client = client_, promise, item_id, remote](string const& fs) {
        client->call(QStringLiteral("operations/list"), RcloneClient::make_params(fs, remote),
                     item_id,
                     [promise, item_id](QJsonObject const& result,
                                        boost::exception_ptr const& error) {
            if (error)
            {
                promise->set_exception(error);
                return;
            }

            try
            {
                ItemList items;
                for (auto const& entry : result.value(QStringLiteral("list")).toArray())
                    items.push_back(make_item(entry.toObject(), item_id, true));
                promise->set_value(make_tuple(std::move(items), string()));
            }
            catch (...)
            {
                promise->set_exception(boost::current_exception());
            }
        });
    });
    return promise->get_future();
}

boost::future<ItemList> RcloneProvider::lookup(
    string const& parent_id, string const& name,
    vector<string> const& metadata_keys, Context const& ctx)
{
    Q_UNUSED(metadata_keys);
    if (!vendor_) vendor_ = Vendor::for_context(test_ctx_ ? *test_ctx_ : ctx);
    string probe_id = make_child_id(parent_id, name);
    string remote = id_to_remote_path(probe_id);
    auto promise = make_shared<boost::promise<ItemList>>();

    vendor_->with_fs(client_, promise, [client = client_, promise, parent_id, name, remote](string const& fs) {
        client->call(QStringLiteral("operations/stat"), RcloneClient::make_params(fs, remote), name,
                     [promise, parent_id, name](QJsonObject const& result,
                                                boost::exception_ptr const& error) {
            if (error)
            {
                promise->set_exception(error);
                return;
            }
            if (!stat_exists(result))
            {
                promise->set_exception(boost::copy_exception(
                    NotExistsException(name + " does not exist", name)));
                return;
            }
            QJsonObject item_json = result.value(QStringLiteral("item")).toObject();
            bool is_dir = item_json.value(QStringLiteral("IsDir")).toBool();
            string item_id = make_child_id(parent_id, name, is_dir);
            promise->set_value(ItemList{make_item(item_json, item_id)});
        });
    });
    return promise->get_future();
}

boost::future<Item> RcloneProvider::metadata(
    string const& item_id, vector<string> const& metadata_keys,
    Context const& ctx)
{
    Q_UNUSED(metadata_keys);
    if (!vendor_) vendor_ = Vendor::for_context(test_ctx_ ? *test_ctx_ : ctx);
    string remote = id_to_remote_path(item_id);
    auto promise = make_shared<boost::promise<Item>>();

    vendor_->with_fs(client_, promise, [client = client_, promise, remote, item_id](string const& fs) {
        fetch_item_metadata(client, promise, fs, remote, item_id);
    });
    return promise->get_future();
}

boost::future<Item> RcloneProvider::create_folder(
    string const& parent_id, string const& name,
    vector<string> const& metadata_keys, Context const& ctx)
{
    Q_UNUSED(metadata_keys);
    if (!vendor_) vendor_ = Vendor::for_context(test_ctx_ ? *test_ctx_ : ctx);
    string folder_id = make_child_id(parent_id, name, true);
    string remote = id_to_remote_path(folder_id);
    auto promise = make_shared<boost::promise<Item>>();

    vendor_->with_fs(client_, promise, [client = client_, promise, remote, folder_id, name](string const& fs) {
        // Check if already exists, otherwise rclone mkdir will silently succeed if it does
        verify_not_exists(client, promise, fs, remote, folder_id, name,
                          [client, promise, fs, remote, folder_id] {
            // Create the folder
            client->call(QStringLiteral("operations/mkdir"), RcloneClient::make_params(fs, remote),
                         folder_id,
                         [client, promise, fs, remote, folder_id]
                         (QJsonObject const&, boost::exception_ptr const& mkdir_err) {
                if (mkdir_err)
                {
                    promise->set_exception(mkdir_err);
                    return;
                }

                // Verify and fetch the folder metadata after creation
                fetch_item_metadata(client, promise, fs, remote, folder_id);
            });
        });
    });
    return promise->get_future();
}

boost::future<unique_ptr<UploadJob>> RcloneProvider::create_file(
    string const& parent_id, string const& name, int64_t size,
    string const& content_type, bool allow_overwrite,
    vector<string> const& metadata_keys, Context const& ctx)
{
    Q_UNUSED(metadata_keys);
    if (size < 0)
        throw InvalidArgumentException("Invalid size: " + to_string(size));
    
    if (!vendor_) vendor_ = Vendor::for_context(test_ctx_ ? *test_ctx_ : ctx);
    string item_id = make_child_id(parent_id, name);
    id_to_remote_path(item_id); // Reject bad id

    auto promise = make_shared<boost::promise<unique_ptr<UploadJob>>>();

    vendor_->with_fs(client_, promise,
            [client = client_, promise, item_id, size, content_type, allow_overwrite]
            (string const& fs) {
        promise->set_value(unique_ptr<UploadJob>(new RcloneUploadJob(
            client, fs, item_id, size, content_type, allow_overwrite, string())));
    });
    return promise->get_future();
}

boost::future<unique_ptr<UploadJob>> RcloneProvider::update(
    string const& item_id, int64_t size, string const& old_etag,
    vector<string> const& metadata_keys, Context const& ctx)
{
    Q_UNUSED(metadata_keys);
    if (size < 0)
        throw InvalidArgumentException("Invalid size: " + to_string(size));
    
    if (!vendor_) vendor_ = Vendor::for_context(test_ctx_ ? *test_ctx_ : ctx);
    id_to_remote_path(item_id); // Reject bad id

    auto promise = make_shared<boost::promise<unique_ptr<UploadJob>>>();

    vendor_->with_fs(client_, promise,
            [client = client_, promise, item_id, size, old_etag](string const& fs) {
        promise->set_value(unique_ptr<UploadJob>(new RcloneUploadJob(
            client, fs, item_id, size, string(), true, old_etag)));
    });
    return promise->get_future();
}

boost::future<unique_ptr<DownloadJob>> RcloneProvider::download(
    string const& item_id, string const& match_etag, Context const& ctx)
{
    if (!vendor_) vendor_ = Vendor::for_context(test_ctx_ ? *test_ctx_ : ctx);
    id_to_remote_path(item_id); // Reject bad id

    auto promise = make_shared<boost::promise<unique_ptr<DownloadJob>>>();

    vendor_->with_fs(client_, promise,
            [client = client_, promise, item_id, match_etag](string const& fs) {
        promise->set_value(unique_ptr<DownloadJob>(new RcloneDownloadJob(
            client, fs, item_id, match_etag)));
    });
    return promise->get_future();
}

boost::future<void> RcloneProvider::delete_item(
    string const& item_id, Context const& ctx)
{
    if (!vendor_) vendor_ = Vendor::for_context(test_ctx_ ? *test_ctx_ : ctx);
    string remote = id_to_remote_path(item_id);
    QString endpoint = is_folder(item_id)
        ? QStringLiteral("operations/purge")
        : QStringLiteral("operations/deletefile");
    auto promise = make_shared<boost::promise<void>>();

    vendor_->with_fs(client_, promise, [client = client_, promise, endpoint, remote, item_id](string const& fs) {
        client->call(endpoint, RcloneClient::make_params(fs, remote), item_id,
                     [promise](QJsonObject const&,
                               boost::exception_ptr const& error) {
            if (error)
            {
                promise->set_exception(error);
                return;
            }
            promise->set_value();
        });
    });
    return promise->get_future();
}

boost::future<Item> RcloneProvider::move(
    string const& item_id, string const& new_parent_id, string const& new_name,
    vector<string> const& metadata_keys, Context const& ctx)
{
    Q_UNUSED(metadata_keys);
    if (!vendor_) vendor_ = Vendor::for_context(test_ctx_ ? *test_ctx_ : ctx);
    bool folder = is_folder(item_id);
    string src_remote = id_to_remote_path(item_id);
    string dst_id = make_child_id(new_parent_id, new_name, folder);
    string dst_remote = id_to_remote_path(dst_id);
    auto promise = make_shared<boost::promise<Item>>();

    vendor_->with_fs(client_, promise,
            [client = client_, promise, folder, src_remote, dst_remote, dst_id, new_name]
            (string const& fs) {
        // avoid overwrite / directory merge, check if destination exists first
        verify_not_exists(client, promise, fs, dst_remote, dst_id, new_name,
                          [client, promise, fs, folder, src_remote, dst_remote, dst_id] {
            auto finish = [client, promise, fs, dst_remote, dst_id]
                          (QJsonObject const&, boost::exception_ptr const& op_err) {
                if (op_err)
                {
                    promise->set_exception(op_err);
                    return;
                }

                // Verify and fetch the moved item metadata after operation
                fetch_item_metadata(client, promise, fs, dst_remote, dst_id);
            };

            if (folder)
            {
                QJsonObject params = RcloneClient::make_sync_params(
                    fs, src_remote, dst_remote, true);
                params.insert(QStringLiteral("createEmptySrcDirs"), true);
                params.insert(QStringLiteral("deleteEmptySrcDirs"), true);
                client->call(QStringLiteral("sync/move"), params, dst_id, finish);
            }
            else
            {
                client->call(QStringLiteral("operations/movefile"),
                             RcloneClient::make_sync_params(fs, src_remote, dst_remote),
                             dst_id, finish);
            }
        });
    });
    return promise->get_future();
}

boost::future<Item> RcloneProvider::copy(
    string const& item_id, string const& new_parent_id, string const& new_name,
    vector<string> const& metadata_keys, Context const& ctx)
{
    Q_UNUSED(metadata_keys);
    if (!vendor_) vendor_ = Vendor::for_context(test_ctx_ ? *test_ctx_ : ctx);
    bool folder = is_folder(item_id);
    string src_remote = id_to_remote_path(item_id);
    string dst_id = make_child_id(new_parent_id, new_name, folder);
    string dst_remote = id_to_remote_path(dst_id);
    auto promise = make_shared<boost::promise<Item>>();

    vendor_->with_fs(client_, promise,
            [client = client_, promise, folder, src_remote, dst_remote, dst_id, new_name]
            (string const& fs) {
        // avoid overwrite / directory merge, check if destination exists first
        verify_not_exists(client, promise, fs, dst_remote, dst_id, new_name,
                          [client, promise, fs, folder, src_remote, dst_remote, dst_id] {
            auto finish = [client, promise, fs, dst_remote, dst_id]
                          (QJsonObject const&, boost::exception_ptr const& op_err) {
                if (op_err)
                {
                    promise->set_exception(op_err);
                    return;
                }

                // Verify and fetch the copied item metadata after operation
                fetch_item_metadata(client, promise, fs, dst_remote, dst_id);
            };

            if (folder)
            {
                QJsonObject params = RcloneClient::make_sync_params(
                    fs, src_remote, dst_remote, true);
                params.insert(QStringLiteral("createEmptySrcDirs"), true);
                client->call(QStringLiteral("sync/copy"), params, dst_id, finish);
            }
            else
            {
                client->call(QStringLiteral("operations/copyfile"),
                             RcloneClient::make_sync_params(fs, src_remote, dst_remote),
                             dst_id, finish);
            }
        });
    });
    return promise->get_future();
}
