// Copyright Contributors to the DNF5 project.
// Copyright Contributors to the libdnf project.
// SPDX-License-Identifier: GPL-2.0-or-later
//
// This file is part of libdnf: https://github.com/rpm-software-management/libdnf/
//
// Libdnf is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// Libdnf 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 libdnf.  If not, see <https://www.gnu.org/licenses/>.

#include "offline.hpp"

#include "dbus.hpp"
#include "utils/string.hpp"

#include <libdnf5/transaction/offline.hpp>
#include <sdbus-c++/sdbus-c++.h>

#include <exception>
#include <filesystem>

const char * const ERR_ANOTHER_TOOL = "Offline transaction was initiated by another tool.";

std::optional<libdnf5::offline::OfflineTransactionState> Offline::read_transaction_state(std::string & error_msg) {
    const std::filesystem::path state_path{get_datadir() / libdnf5::offline::TRANSACTION_STATE_FILENAME};
    std::error_code ec;
    if (!std::filesystem::exists(state_path, ec)) {
        error_msg = "No offline transaction is stored.";
        return std::nullopt;
    }
    libdnf5::offline::OfflineTransactionState state{state_path};
    const auto & read_exception = state.get_read_exception();
    if (read_exception != nullptr) {
        try {
            std::rethrow_exception(read_exception);
        } catch (const std::exception & ex) {
            error_msg = ex.what();
        }
        return std::nullopt;
    }
    return state;
}

std::filesystem::path Offline::get_datadir() {
    auto base = session.get_base();
    const auto & installroot = base->get_config().get_installroot_option().get_value();
    return installroot / libdnf5::offline::DEFAULT_DATADIR.relative_path();
}

std::filesystem::path Offline::get_destdir() {
    auto base = session.get_base();
    const auto & installroot = base->get_config().get_installroot_option().get_value();
    return installroot / libdnf5::offline::DEFAULT_DESTDIR.relative_path();
}

Offline::Scheduled Offline::offline_transaction_scheduled() {
    std::error_code ec;
    // magic symlink exists
    if (std::filesystem::exists(libdnf5::offline::MAGIC_SYMLINK, ec)) {
        // and points to dnf5 location
        if (std::filesystem::equivalent(libdnf5::offline::MAGIC_SYMLINK, get_datadir())) {
            return Scheduled::SCHEDULED;
        } else {
            return Scheduled::ANOTHER_TOOL;
        }
    }
    return Scheduled::NOT_SCHEDULED;
}

void Offline::dbus_register() {
    auto dbus_object = session.get_dbus_object();
#ifdef SDBUS_CPP_VERSION_2
    dbus_object
        ->addVTable(
            sdbus::MethodVTableItem{
                sdbus::MethodName{"cancel_with_options"},
                sdbus::Signature{"a{sv}"},
                {"options"},
                sdbus::Signature{"bs"},
                {"success", "error_msg"},
                [this](sdbus::MethodCall call) -> void {
                    session.get_threads_manager().handle_method(
                        *this, &Offline::cancel_with_options, call, session.session_locale);
                },
                {}},
            sdbus::MethodVTableItem{
                sdbus::MethodName{"cancel"},
                {},
                {},
                sdbus::Signature{"bs"},
                {"success", "error_msg"},
                [this](sdbus::MethodCall call) -> void {
                    session.get_threads_manager().handle_method(*this, &Offline::cancel, call, session.session_locale);
                },
                {}},
            sdbus::MethodVTableItem{
                sdbus::MethodName{"get_status"},
                {},
                {},
                sdbus::Signature{"ba{sv}"},
                {"is_pending", "transaction_status"},
                [this](sdbus::MethodCall call) -> void {
                    session.get_threads_manager().handle_method(
                        *this, &Offline::get_status, call, session.session_locale);
                },
                {}},
            sdbus::MethodVTableItem{
                sdbus::MethodName{"clean_with_options"},
                sdbus::Signature{"a{sv}"},
                {"options"},
                sdbus::Signature{"bs"},
                {"success", "error_msg"},
                [this](sdbus::MethodCall call) -> void {
                    session.get_threads_manager().handle_method(
                        *this, &Offline::clean_with_options, call, session.session_locale);
                },
                {}},
            sdbus::MethodVTableItem{
                sdbus::MethodName{"clean"},
                {},
                {},
                sdbus::Signature{"bs"},
                {"success", "error_msg"},
                [this](sdbus::MethodCall call) -> void {
                    session.get_threads_manager().handle_method(*this, &Offline::clean, call, session.session_locale);
                },
                {}},
            sdbus::MethodVTableItem{
                sdbus::MethodName{"set_finish_action_with_options"},
                sdbus::Signature{"sa{sv}"},
                {"action", "options"},
                sdbus::Signature{"bs"},
                {"success", "error_msg"},
                [this](sdbus::MethodCall call) -> void {
                    session.get_threads_manager().handle_method(
                        *this, &Offline::set_finish_action_with_options, call, session.session_locale);
                },
                {}},
            sdbus::MethodVTableItem{
                sdbus::MethodName{"set_finish_action"},
                sdbus::Signature{"s"},
                {"action"},
                sdbus::Signature{"bs"},
                {"success", "error_msg"},
                [this](sdbus::MethodCall call) -> void {
                    session.get_threads_manager().handle_method(
                        *this, &Offline::set_finish_action, call, session.session_locale);
                },
                {}},
            sdbus::MethodVTableItem{
                sdbus::MethodName{"schedule_for_next_boot"},
                sdbus::Signature{"a{sv}"},
                {"options"},
                sdbus::Signature{"bs"},
                {"success", "error_msg"},
                [this](sdbus::MethodCall call) -> void {
                    session.get_threads_manager().handle_method(
                        *this, &Offline::schedule_for_next_boot, call, session.session_locale);
                },
                {}})
        .forInterface(dnfdaemon::INTERFACE_OFFLINE);
#else
    dbus_object->registerMethod(
        dnfdaemon::INTERFACE_OFFLINE,
        "cancel_with_options",
        {"a{sv}"},
        {"options"},
        "bs",
        {"success", "error_msg"},
        [this](sdbus::MethodCall call) -> void {
            session.get_threads_manager().handle_method(
                *this, &Offline::cancel_with_options, call, session.session_locale);
        });
    dbus_object->registerMethod(
        dnfdaemon::INTERFACE_OFFLINE,
        "cancel",
        {},
        {},
        "bs",
        {"success", "error_msg"},
        [this](sdbus::MethodCall call) -> void {
            session.get_threads_manager().handle_method(*this, &Offline::cancel, call, session.session_locale);
        });
    dbus_object->registerMethod(
        dnfdaemon::INTERFACE_OFFLINE,
        "get_status",
        {},
        {},
        "ba{sv}",
        {"is_pending", "transaction_status"},
        [this](sdbus::MethodCall call) -> void {
            session.get_threads_manager().handle_method(*this, &Offline::get_status, call, session.session_locale);
        });
    dbus_object->registerMethod(
        dnfdaemon::INTERFACE_OFFLINE,
        "clean_with_options",
        {"a{sv}"},
        {"options"},
        "bs",
        {"success", "error_msg"},
        [this](sdbus::MethodCall call) -> void {
            session.get_threads_manager().handle_method(
                *this, &Offline::clean_with_options, call, session.session_locale);
        });
    dbus_object->registerMethod(
        dnfdaemon::INTERFACE_OFFLINE,
        "clean",
        {},
        {},
        "bs",
        {"success", "error_msg"},
        [this](sdbus::MethodCall call) -> void {
            session.get_threads_manager().handle_method(*this, &Offline::clean, call, session.session_locale);
        });
    dbus_object->registerMethod(
        dnfdaemon::INTERFACE_OFFLINE,
        "set_finish_action_with_options",
        "sa{sv}",
        {"action", "options"},
        "bs",
        {"success", "error_msg"},
        [this](sdbus::MethodCall call) -> void {
            session.get_threads_manager().handle_method(
                *this, &Offline::set_finish_action_with_options, call, session.session_locale);
        });
    dbus_object->registerMethod(
        dnfdaemon::INTERFACE_OFFLINE,
        "set_finish_action",
        "s",
        {"action"},
        "bs",
        {"success", "error_msg"},
        [this](sdbus::MethodCall call) -> void {
            session.get_threads_manager().handle_method(
                *this, &Offline::set_finish_action, call, session.session_locale);
        });
    dbus_object->registerMethod(
        dnfdaemon::INTERFACE_OFFLINE,
        "schedule_for_next_boot",
        {"a{sv}"},
        {"options"},
        "bs",
        {"success", "error_msg"},
        [this](sdbus::MethodCall call) -> void {
            session.get_threads_manager().handle_method(
                *this, &Offline::schedule_for_next_boot, call, session.session_locale);
        });
#endif
}

sdbus::MethodReply Offline::get_status(sdbus::MethodCall & call) {
    dnfdaemon::KeyValueMap transaction_state;

    std::string state_error;
    auto state = read_transaction_state(state_error);
    if (state) {
        const auto & state_data = state->get_data();
        transaction_state["status"] = sdbus::Variant(state_data.get_status());
        transaction_state["cachedir"] = sdbus::Variant(state_data.get_cachedir());
        transaction_state["target_releasever"] = sdbus::Variant(state_data.get_target_releasever());
        transaction_state["system_releasever"] = sdbus::Variant(state_data.get_system_releasever());
        transaction_state["verb"] = sdbus::Variant(state_data.get_verb());
        transaction_state["cmd_line"] = sdbus::Variant(state_data.get_cmd_line());
        transaction_state["poweroff_after"] = sdbus::Variant(state_data.get_poweroff_after());
        transaction_state["module_platform_id"] = sdbus::Variant(state_data.get_module_platform_id());
    }

    auto reply = call.createReply();
    reply << (offline_transaction_scheduled() == Scheduled::SCHEDULED);
    reply << transaction_state;
    return reply;
}

sdbus::MethodReply Offline::impl_cancel(sdbus::MethodCall & call, const dnfdaemon::KeyValueMap & options) {
    bool interactive = dnfdaemon::key_value_map_get<bool>(options, "interactive", true);
    if (!session.check_authorization(
            dnfdaemon::POLKIT_EXECUTE_RPM_TRUSTED_TRANSACTION, call.getSender(), interactive)) {
        throw std::runtime_error("Not authorized");
    }
    bool success = true;
    std::string error_msg;
    switch (offline_transaction_scheduled()) {
        case Scheduled::SCHEDULED: {
            std::error_code ec;
            if (!std::filesystem::remove(libdnf5::offline::MAGIC_SYMLINK, ec) && ec) {
                success = false;
                error_msg = ec.message();
            } else {
                // Reset the status back to download-complete since the transaction
                // is no longer scheduled for the next boot.
                std::string state_error;
                auto state = read_transaction_state(state_error);
                if (state && state->get_data().get_status() == libdnf5::offline::STATUS_READY) {
                    state->get_data().set_status(libdnf5::offline::STATUS_DOWNLOAD_COMPLETE);
                    state->write();
                }
            }
        } break;
        case Scheduled::ANOTHER_TOOL:
            success = false;
            error_msg = ERR_ANOTHER_TOOL;
            break;
        case Scheduled::NOT_SCHEDULED:
            break;
    }
    auto reply = call.createReply();
    reply << success;
    reply << error_msg;
    return reply;
}

sdbus::MethodReply Offline::cancel_with_options(sdbus::MethodCall & call) {
    dnfdaemon::KeyValueMap options;
    call >> options;
    return impl_cancel(call, options);
}

sdbus::MethodReply Offline::cancel(sdbus::MethodCall & call) {
    dnfdaemon::KeyValueMap options{};
    return impl_cancel(call, options);
}

sdbus::MethodReply Offline::impl_clean(sdbus::MethodCall & call, const dnfdaemon::KeyValueMap & options) {
    bool interactive = dnfdaemon::key_value_map_get<bool>(options, "interactive", true);
    if (!session.check_authorization(
            dnfdaemon::POLKIT_EXECUTE_RPM_TRUSTED_TRANSACTION, call.getSender(), interactive)) {
        throw std::runtime_error("Not authorized");
    }
    std::vector<std::string> error_msgs;
    bool success = true;
    if (offline_transaction_scheduled() == Scheduled::SCHEDULED) {
        // remove the magic symlink if it was created by dnf5
        std::error_code ec;
        if (!std::filesystem::remove(libdnf5::offline::MAGIC_SYMLINK, ec) && ec) {
            success = false;
            error_msgs.push_back(ec.message());
        }
    }
    // clean dnf5 offline transaction files
    for (const auto & entry : std::filesystem::directory_iterator(get_datadir())) {
        std::error_code ec;
        std::filesystem::remove_all(entry.path(), ec);
        if (ec) {
            success = false;
            error_msgs.push_back(ec.message());
        }
    }
    // clean dnf5 offline packages destdir
    for (const auto & entry : std::filesystem::directory_iterator(get_destdir())) {
        std::error_code ec;
        std::filesystem::remove_all(entry.path(), ec);
        if (ec) {
            success = false;
            error_msgs.push_back(ec.message());
        }
    }
    auto reply = call.createReply();
    reply << success;
    reply << libdnf5::utils::string::join(error_msgs, ", ");
    return reply;
}

sdbus::MethodReply Offline::clean_with_options(sdbus::MethodCall & call) {
    dnfdaemon::KeyValueMap options;
    call >> options;
    return impl_clean(call, options);
}

sdbus::MethodReply Offline::clean(sdbus::MethodCall & call) {
    dnfdaemon::KeyValueMap options{};
    return impl_clean(call, options);
}

sdbus::MethodReply Offline::impl_set_finish_action(
    sdbus::MethodCall & call, const std::string & action, const dnfdaemon::KeyValueMap & options) {
    bool interactive = dnfdaemon::key_value_map_get<bool>(options, "interactive", true);
    if (!session.check_authorization(
            dnfdaemon::POLKIT_EXECUTE_RPM_TRUSTED_TRANSACTION, call.getSender(), interactive)) {
        throw std::runtime_error("Not authorized");
    }
    bool success{false};
    std::string error_msg{};

    // check finish_action validity
    if (action != "poweroff" && action != "reboot") {
        error_msg =
            fmt::format("Unsupported finish action \"{}\". Valid options are \"reboot\", or \"poweroff\".", action);
    } else {
        auto state = read_transaction_state(error_msg);
        if (state) {
            // set the poweroff_after item accordingly
            state->get_data().set_poweroff_after(action == "poweroff");
            // write the new state
            state->write();
            success = true;
        }
    }
    auto reply = call.createReply();
    reply << success;
    reply << error_msg;
    return reply;
}

sdbus::MethodReply Offline::set_finish_action_with_options(sdbus::MethodCall & call) {
    std::string action;
    dnfdaemon::KeyValueMap options;
    call >> action >> options;
    return impl_set_finish_action(call, action, options);
}

sdbus::MethodReply Offline::set_finish_action(sdbus::MethodCall & call) {
    std::string action;
    dnfdaemon::KeyValueMap options{};
    call >> action;
    return impl_set_finish_action(call, action, options);
}

sdbus::MethodReply Offline::schedule_for_next_boot(sdbus::MethodCall & call) {
    dnfdaemon::KeyValueMap options;
    call >> options;

    bool interactive = dnfdaemon::key_value_map_get<bool>(options, "interactive", true);
    if (!session.check_authorization(
            dnfdaemon::POLKIT_EXECUTE_RPM_TRUSTED_TRANSACTION, call.getSender(), interactive)) {
        throw std::runtime_error("Not authorized");
    }
    bool success{false};
    std::string error_msg{};

    auto state = read_transaction_state(error_msg);
    if (state) {
        const auto & status = state->get_data().get_status();
        if (status != libdnf5::offline::STATUS_DOWNLOAD_COMPLETE && status != libdnf5::offline::STATUS_READY) {
            error_msg =
                fmt::format("Cannot schedule offline transaction for next boot. Transaction status is \"{}\".", status);
        } else {
            const auto scheduled = offline_transaction_scheduled();
            if (scheduled == Scheduled::ANOTHER_TOOL) {
                error_msg = ERR_ANOTHER_TOOL;
            } else {
                // Handle both NOT_SCHEDULED (create symlink) and
                // SCHEDULED (symlink already exists)
                std::error_code ec;
                if (scheduled == Scheduled::NOT_SCHEDULED) {
                    std::filesystem::create_symlink(get_datadir(), libdnf5::offline::MAGIC_SYMLINK, ec);
                }
                if (ec) {
                    error_msg = ec.message();
                } else {
                    state->get_data().set_status(libdnf5::offline::STATUS_READY);
                    state->write();
                    success = true;
                }
            }
        }
    }
    auto reply = call.createReply();
    reply << success;
    reply << error_msg;
    return reply;
}
