// 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 "session.hpp"

#include "callbacks.hpp"
#include "const.hpp"
#include "dbus.hpp"
#include "services/advisory/advisory.hpp"
#include "services/base/base.hpp"
#include "services/comps/group.hpp"
#include "services/goal/goal.hpp"
#include "services/history/history.hpp"
#include "services/offline/offline.hpp"
#include "services/repo/repo.hpp"
#include "services/rpm/rpm.hpp"
#include "utils.hpp"

#include <libdnf5/conf/const.hpp>
#include <libdnf5/logger/stream_logger.hpp>
#include <libdnf5/repo/package_downloader.hpp>
#include <libdnf5/transaction/offline.hpp>
#include <libdnf5/utils/fs/file.hpp>
#include <sdbus-c++/sdbus-c++.h>

#include <chrono>
#include <filesystem>
#include <iostream>
#include <optional>
#include <string>

// config options that regular user can override for their session.
static const std::unordered_set<std::string> ALLOWED_MAIN_CONF_OVERRIDES = {
    "allow_downgrade",
    "allow_vendor_change",
    "best",
    "clean_requirements_on_remove",
    "disable_excludes",
    "exclude_from_weak",
    "exclude_from_weak_autodetect",
    "excludepkgs",
    "ignorearch",
    "includepkgs",
    "installonly_limit",
    "installonlypkgs",
    "install_weak_deps",
    "keepcache",
    "module_obsoletes",
    "module_platform_id",
    "module_stream_switch",
    "multilib_policy",
    "obsoletes",
    "optional_metadata_types",
    "protect_running_kernel",
    "skip_broken",
    "skip_if_unavailable",
    "skip_unavailable",
    "strict",
};

void Session::setup_base() {
    std::vector<std::unique_ptr<libdnf5::Logger>> loggers;
    loggers.emplace_back(std::make_unique<libdnf5::StdCStreamLogger>(std::cerr));
    base = std::make_unique<libdnf5::Base>(std::move(loggers));

    auto & config = base->get_config();

    // adjust base.config from session_configuration config overrides
    auto conf_overrides =
        session_configuration_value<std::map<std::string, std::string>>("config", std::map<std::string, std::string>{});
    auto opt_binds = config.opt_binds();
    std::optional<bool> am_i_root;
    for (auto & opt : conf_overrides) {
        auto key = opt.first;
        auto value = opt.second;
        auto bind = opt_binds.find(key);
        if (bind != opt_binds.end()) {
            if (ALLOWED_MAIN_CONF_OVERRIDES.find(key) != ALLOWED_MAIN_CONF_OVERRIDES.end()) {
                bind->second.new_string(libdnf5::Option::Priority::RUNTIME, value);
            } else {
                if (!am_i_root.has_value()) {
                    // check the authorization lazily only once really needed
                    am_i_root = check_authorization(dnfdaemon::POLKIT_CONFIG_OVERRIDE, sender, false);
                }
                // restricted config options override is allowed only for root
                if (am_i_root.value()) {
                    bind->second.new_string(libdnf5::Option::Priority::RUNTIME, value);
                } else {
                    base->get_logger()->warning("Config option {} not allowed.", key);
                }
            }
        } else {
            base->get_logger()->warning("Unknown config option: {}", key);
        }
    }

    // load configuration
    base->load_config();

    // load dnf5daemon-server specific configuration file
    std::filesystem::path dnfdaemon_conf_file_path{dnfdaemon::CONF_FILENAME};
    if (std::filesystem::exists(dnfdaemon_conf_file_path)) {
        libdnf5::ConfigParser parser;
        parser.read(dnfdaemon_conf_file_path);
        // Load options from the dnf5daemon-server.conf with greater priority than
        // Priority::MAINCONFIG to bypass cachedir option being automatically overridden
        // from system_cachedir. With different cachedir and system_cachedir, quick
        // clone_root_metadata() method is available.
        base->get_config().load_from_parser(
            parser, "main", *base->get_vars(), *base->get_logger(), libdnf5::Option::Priority::AUTOMATICCONFIG);
    }

    // set variables
    if (session_configuration.find("releasever") != session_configuration.end()) {
        auto releasever = session_configuration_value<std::string>("releasever");
        base->get_vars()->set("releasever", releasever);
    }
    if (session_configuration.find("releasever_major") != session_configuration.end()) {
        auto releasever_major = session_configuration_value<std::string>("releasever_major");
        base->get_vars()->set("releasever_major", releasever_major);
    }
    if (session_configuration.find("releasever_minor") != session_configuration.end()) {
        auto releasever_minor = session_configuration_value<std::string>("releasever_minor");
        base->get_vars()->set("releasever_minor", releasever_minor);
    }

    base->setup();

    // load repo configuration
    base->get_repo_sack()->create_repos_from_system_configuration();
    repositories_status = dnfdaemon::RepoStatus::NOT_READY;

    base->set_download_callbacks(std::make_unique<dnf5daemon::DownloadCB>(*this));

    // Goal and Transaction instances depend on the base, so reset them as well
    goal = std::make_unique<libdnf5::Goal>(*base);
    transaction.reset(nullptr);
}

Session::Session(
    sdbus::IConnection & connection,
    dnfdaemon::KeyValueMap session_configuration,
    const sdbus::ObjectPath & object_path,
    const std::string & sender)
    : connection(connection),
      session_configuration(session_configuration),
      object_path(object_path),
      sender(sender) {
    if (session_configuration.find("locale") != session_configuration.end()) {
        session_locale = session_configuration_value<std::string>("locale");
    }

    dbus_object = sdbus::createObject(connection, object_path);
    setup_base();

    // instantiate all services provided by the daemon
    services.emplace_back(std::make_unique<Base>(*this));
    services.emplace_back(std::make_unique<Repo>(*this));
    services.emplace_back(std::make_unique<Rpm>(*this));
    services.emplace_back(std::make_unique<Goal>(*this));
    services.emplace_back(std::make_unique<Offline>(*this));
    services.emplace_back(std::make_unique<Group>(*this));
    services.emplace_back(std::make_unique<History>(*this));
    services.emplace_back(std::make_unique<dnfdaemon::Advisory>(*this));

    // Register all provided services on d-bus
    for (auto & s : services) {
        s->dbus_register();
    }

#ifndef SDBUS_CPP_VERSION_2
    dbus_object->finishRegistration();
#endif
}

Session::~Session() {
    dbus_object->unregister();
    threads_manager.finish();
}

void Session::confirm_key(const std::string & key_id, const bool confirmed) {
    std::lock_guard<std::mutex> lock(key_import_mutex);
    auto status_it = key_import_status.find(key_id);
    // ignore replies which were not requested
    if (status_it != key_import_status.end()) {
        // ignore confirmations that has been already answered
        if (status_it->second == KeyConfirmationStatus::PENDING) {
            key_import_status[key_id] = confirmed ? KeyConfirmationStatus::CONFIRMED : KeyConfirmationStatus::REJECTED;
            key_import_condition.notify_all();
        }
    }
}

bool Session::wait_for_key_confirmation(const std::string & key_id, sdbus::Signal & request_signal) {
    std::unique_lock<std::mutex> key_import_lock(key_import_mutex);
    if (key_import_status.find(key_id) == key_import_status.end()) {
        // signal client that repository key import confirmation is required
        key_import_status[key_id] = KeyConfirmationStatus::PENDING;
        dbus_object->emitSignal(request_signal);
    }

    // wait for a confirmation for <timeout>
    auto timeout = std::chrono::minutes(5);
    auto wait = key_import_condition.wait_for(key_import_lock, timeout, [this, &key_id]() {
        return key_import_status.at(key_id) != KeyConfirmationStatus::PENDING;
    });
    if (!wait) {
        key_import_lock.unlock();
        throw sdbus::Error(dnfdaemon::ERROR, "Timeout while waiting for the repository key import confirmation.");
    }
    auto confirmation = key_import_status.at(key_id);
    key_import_lock.unlock();

    return confirmation == KeyConfirmationStatus::CONFIRMED;
}

void Session::fill_sack(bool interactive) {
    if (!read_all_repos(interactive)) {
        throw std::runtime_error("Cannot load repositories.");
    }
}

bool Session::read_all_repos(bool interactive) {
    while (repositories_status == dnfdaemon::RepoStatus::PENDING) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    if (repositories_status == dnfdaemon::RepoStatus::READY) {
        return true;
    } else if (repositories_status == dnfdaemon::RepoStatus::ERROR) {
        return false;
    }
    repositories_status = dnfdaemon::RepoStatus::PENDING;

    bool retval = true;

    bool load_available_repos = session_configuration_value<bool>("load_available_repos", true);
    bool load_system_repo = session_configuration_value<bool>("load_system_repo", true);
    std::vector<std::string> optional_metadata_str =
        session_configuration_value<std::vector<std::string>>("optional_metadata_types", {});
    if (load_available_repos) {
        auto optional_metadata_types_opt = base->get_config().get_optional_metadata_types_option();
        for (const auto & type : optional_metadata_str) {
            optional_metadata_types_opt.add_item(libdnf5::Option::Priority::RUNTIME, type);
        }
        //auto & logger = base->get_logger();
        libdnf5::repo::RepoQuery enabled_repos(*base);
        enabled_repos.filter_enabled(true);
        enabled_repos.filter_type(libdnf5::repo::Repo::Type::AVAILABLE);
        // container is owner of package callbacks user_data
        std::vector<std::unique_ptr<dnf5daemon::DownloadUserData>> repos_user_data;
        for (auto & repo : enabled_repos) {
            auto & user_data = repos_user_data.emplace_back(std::make_unique<dnf5daemon::DownloadUserData>());
            user_data->download_id = "repo:" + repo->get_id();
            repo->set_user_data(user_data.get());
            repo->set_callbacks(std::make_unique<dnf5daemon::KeyImportRepoCB>(*this, interactive));
        }

        try {
            if (load_system_repo) {
                base->get_repo_sack()->load_repos();
            } else {
                base->get_repo_sack()->load_repos(libdnf5::repo::Repo::Type::AVAILABLE);
            }
        } catch (const std::runtime_error & ex) {
            retval = false;
        }
    } else if (load_system_repo) {
        base->get_repo_sack()->load_repos(libdnf5::repo::Repo::Type::SYSTEM);
    }

    repositories_status = retval ? dnfdaemon::RepoStatus::READY : dnfdaemon::RepoStatus::ERROR;
    return retval;
}

bool Session::check_authorization(
    const std::string & actionid, const std::string & sender, bool allow_user_interaction) {
    // create proxy for PolicyKit1 object
    const SDBUS_SERVICE_NAME_TYPE destination_name{"org.freedesktop.PolicyKit1"};
    const sdbus::ObjectPath object_path{"/org/freedesktop/PolicyKit1/Authority"};
    const SDBUS_INTERFACE_NAME_TYPE interface_name{"org.freedesktop.PolicyKit1.Authority"};
    auto polkit_proxy = sdbus::createProxy(connection, destination_name, object_path);

#ifndef SDBUS_CPP_VERSION_2
    polkit_proxy->finishRegistration();
#endif

    // call CheckAuthorization method
    sdbus::Struct<bool, bool, std::map<std::string, std::string>> auth_result;
    sdbus::Struct<std::string, dnfdaemon::KeyValueMap> subject{"system-bus-name", {{"name", sdbus::Variant(sender)}}};
    std::map<std::string, std::string> details{};
    // allow polkit to ask user to enter root password
    uint flags = allow_user_interaction ? 1 : 0;
    std::string cancellation_id = "";
    try {
        polkit_proxy->callMethod("CheckAuthorization")
            .onInterface(interface_name)
            .withArguments(subject, actionid, details, flags, cancellation_id)
            .withTimeout(std::chrono::minutes(2))
            .storeResultsTo(auth_result);
    } catch (const sdbus::Error & ex) {
        auto name = ex.getName();
        if (name == "org.freedesktop.DBus.Error.Timeout" || name == "org.freedesktop.DBus.Error.NoReply") {
            // in case of timeout return "not authorized"
            return false;
        }
        throw sdbus::Error(dnfdaemon::ERROR, fmt::format("Failed to check authorization: \"{}\"", ex.what()));
    }

    // get results
    bool res_is_authorized = std::get<0>(auth_result);
    /*
    bool res_is_challenge = std::get<1>(auth_result);
    std::map<std::string, std::string> res_details = std::get<2>(auth_result);
    */

    return res_is_authorized;
}

void Session::download_transaction_packages() {
    // container is owner of package callbacks user_data;
    std::vector<std::unique_ptr<dnf5daemon::DownloadUserData>> user_data;
    // make sure the user_data is freed after the downloader, because the
    // downloader can call download_end signal during its destructor
    {
        libdnf5::repo::PackageDownloader downloader(base->get_weak_ptr());

        for (auto & tspkg : transaction->get_transaction_packages()) {
            if (transaction_item_action_is_inbound(tspkg.get_action()) &&
                tspkg.get_package().get_repo()->get_type() != libdnf5::repo::Repo::Type::COMMANDLINE) {
                auto & data = user_data.emplace_back(std::make_unique<dnf5daemon::DownloadUserData>());
                data->download_id = "package:" + std::to_string(tspkg.get_package().get_id().id);
                downloader.add(tspkg.get_package(), data.get());
            }
        }

        downloader.download();
    }
}

void Session::store_transaction_offline(bool downloadonly) {
    const auto & installroot = base->get_config().get_installroot_option().get_value();
    const auto & offline_datadir = installroot / libdnf5::offline::DEFAULT_DATADIR.relative_path();
    const auto & offline_destdir = installroot / libdnf5::offline::DEFAULT_DESTDIR.relative_path();
    std::filesystem::create_directories(offline_datadir);
    const std::filesystem::path state_path{offline_datadir / libdnf5::offline::TRANSACTION_STATE_FILENAME};
    libdnf5::offline::OfflineTransactionState state{state_path};
    auto & state_data = state.get_data();
    state_data.set_status(libdnf5::offline::STATUS_DOWNLOAD_INCOMPLETE);
    state.write();

    // Download transaction packages
    const auto & dest_dir = offline_destdir / "packages";
    std::filesystem::create_directories(dest_dir);
    base->get_config().get_destdir_option().set(dest_dir);
    download_transaction_packages();
    set_cancel_download(Session::CancelDownload::NOT_ALLOWED);

    // Test the transaction
    // TODO(mblaha): store transaction test/run problems in the session and add an API
    // to retrieve it
    base->get_config().get_tsflags_option().set(libdnf5::Option::Priority::RUNTIME, "test");
    auto result = transaction->run();
    if (result != libdnf5::base::Transaction::TransactionRunResult::SUCCESS) {
        throw sdbus::Error(
            dnfdaemon::ERROR_TRANSACTION,
            fmt::format(
                "offline rpm transaction test failed with code {}.",
                static_cast<std::underlying_type_t<libdnf5::base::Transaction::TransactionRunResult>>(result)));
    }

    // Serialize the transaction
    const auto & comps_location = offline_destdir / "comps";

    transaction->store_comps(comps_location);

    const auto transaction_json_path = offline_datadir / "transaction.json";
    libdnf5::utils::fs::File transaction_json_file{transaction_json_path, "w"};
    transaction_json_file.write(transaction->serialize(dest_dir, comps_location));
    transaction_json_file.close();

    // Download and transaction test complete. Fill out entries in offline
    // transaction state file.
    state_data.set_cachedir(base->get_config().get_cachedir_option().get_value());

    state_data.set_cmd_line("dnf5daemon-server");

    const auto & detected_releasever = libdnf5::Vars::detect_release(base->get_weak_ptr(), installroot);
    if (detected_releasever != nullptr) {
        state_data.set_system_releasever(*detected_releasever);
    }
    state_data.set_target_releasever(base->get_vars()->get_value("releasever"));

    const auto module_platform_id = base->get_config().get_module_platform_id_option();
    if (!module_platform_id.empty()) {
        state_data.set_module_platform_id(module_platform_id.get_value());
    }

    // create the magic symlink /system-update -> datadir
    if (downloadonly) {
        state_data.set_status(libdnf5::offline::STATUS_DOWNLOAD_COMPLETE);
    } else {
        if (!std::filesystem::is_symlink(libdnf5::offline::MAGIC_SYMLINK)) {
            std::filesystem::create_symlink(offline_datadir, libdnf5::offline::MAGIC_SYMLINK);
        }
        state_data.set_status(libdnf5::offline::STATUS_READY);
    }
    state.write();
}

void Session::reset_goal() {
    transaction.reset(nullptr);
    goal->reset();
}

void Session::reset_base() {
    setup_base();
}
