// Copyright Contributors to the DNF5 project.
// Copyright Contributors to the libdnf project.
// SPDX-License-Identifier: LGPL-2.1-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 Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with libdnf.  If not, see <https://www.gnu.org/licenses/>.


#include "rpm/transaction.hpp"

#include "base_impl.hpp"

#ifdef WITH_MODULEMD
#include "module/module_db.hpp"
#include "module/module_sack_impl.hpp"
#endif
#include "../repo/repo_sack_private.hpp"
#include "repo/temp_files_memory.hpp"
#include "rpm/package_set_impl.hpp"
#include "solv/pool.hpp"
#include "solver_problems_internal.hpp"
#include "transaction/transaction_sr.hpp"
#include "transaction_impl.hpp"
#include "transaction_module_impl.hpp"
#include "transaction_package_impl.hpp"
#include "utils/on_scope_exit.hpp"
#include "utils/string.hpp"

#include "libdnf5/base/active_transaction_info.hpp"
#include "libdnf5/base/base.hpp"
#include "libdnf5/common/exception.hpp"
#include "libdnf5/common/sack/exclude_flags.hpp"
#include "libdnf5/common/sack/query_cmp.hpp"
#include "libdnf5/comps/group/query.hpp"
#include "libdnf5/conf/const.hpp"
#include "libdnf5/repo/package_downloader.hpp"
#include "libdnf5/rpm/package_query.hpp"
#include "libdnf5/utils/bgettext/bgettext-lib.h"
#include "libdnf5/utils/bgettext/bgettext-mark-domain.h"
#include "libdnf5/utils/format.hpp"
#include "libdnf5/utils/locker.hpp"

#include <fcntl.h>
#include <fmt/format.h>
#include <poll.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include <filesystem>
#include <iostream>
#include <set>
#include <sstream>
#include <string_view>
#include <thread>


namespace libdnf5::base {

namespace {

// Maps the string representation of transaction flags to the rpmtransFlags_e enum.
constexpr std::pair<const char *, rpmtransFlags_e> string_tsflag_map[]{
    {"test", RPMTRANS_FLAG_TEST},
    {"nodocs", RPMTRANS_FLAG_NODOCS},
    {"noscripts", RPMTRANS_FLAG_NOSCRIPTS},
    {"notriggers", RPMTRANS_FLAG_NOTRIGGERS},
    {"justdb", RPMTRANS_FLAG_JUSTDB},
    {"nocontexts", RPMTRANS_FLAG_NOCONTEXTS},
    {"nocaps", RPMTRANS_FLAG_NOCAPS},
    {"nocrypto", RPMTRANS_FLAG_NOFILEDIGEST},
    {"deploops", RPMTRANS_FLAG_DEPLOOPS},
    {"noplugins", RPMTRANS_FLAG_NOPLUGINS},
};

const std::map<base::Transaction::TransactionRunResult, BgettextMessage> TRANSACTION_RUN_RESULT_DICT = {
    {base::Transaction::TransactionRunResult::ERROR_RERUN, M_("This transaction has been already run before.")},
    {base::Transaction::TransactionRunResult::ERROR_RESOLVE, M_("Cannot run transaction with resolving problems.")},
    {base::Transaction::TransactionRunResult::ERROR_CHECK, M_("Rpm transaction check failed.")},
    {base::Transaction::TransactionRunResult::ERROR_LOCK,
     M_("Failed to obtain rpm transaction lock. Another transaction is in progress.")},
    {base::Transaction::TransactionRunResult::ERROR_RPM_RUN, M_("Rpm transaction failed.")},
    {base::Transaction::TransactionRunResult::ERROR_GPG_CHECK, M_("Signature verification failed.")},
};

const std::map<base::ImportRepoKeysResult, BgettextMessage> IMPORT_REPO_KEYS_RESULT_DICT = {
    {base::ImportRepoKeysResult::NO_KEYS, M_("The repository does not have any OpenPGP keys configured.")},
    {base::ImportRepoKeysResult::ALREADY_PRESENT, M_("Public key is not installed.")},
    {base::ImportRepoKeysResult::IMPORT_DECLINED, M_("Canceled by the user.")},
    {base::ImportRepoKeysResult::IMPORT_FAILED, M_("Public key import failed.")},
};

std::filesystem::path build_comps_xml_path(std::filesystem::path path, const std::string & id) {
    path = path / id;
    path.replace_extension("xml");
    return path;
}

static std::vector<std::pair<ProblemRules, std::vector<std::string>>> get_removal_of_protected(
    rpm::solv::GoalPrivate & solved_goal, const libdnf5::rpm::PackageQuery & broken_installed_query) {
    auto & pool = solved_goal.get_rpm_pool();

    auto protected_running_kernel = solved_goal.get_protect_running_kernel();
    std::vector<std::pair<ProblemRules, std::vector<std::string>>> problem_output;

    std::set<std::string> names;
    auto removal_of_protected = solved_goal.get_removal_of_protected();
    if (removal_of_protected && !removal_of_protected->empty()) {
        for (auto protected_id : *removal_of_protected) {
            if (protected_id == protected_running_kernel.id) {
                std::vector<std::string> elements;
                elements.emplace_back(pool.get_full_nevra(protected_id));
                if (is_unique(problem_output, ProblemRules::RULE_PKG_REMOVAL_OF_RUNNING_KERNEL, elements)) {
                    problem_output.push_back(
                        std::make_pair(ProblemRules::RULE_PKG_REMOVAL_OF_RUNNING_KERNEL, std::move(elements)));
                }
                continue;
            }
            names.emplace(pool.get_name(protected_id));
        }
        if (!names.empty()) {
            std::vector<std::string> names_vector(names.begin(), names.end());
            if (is_unique(problem_output, ProblemRules::RULE_PKG_REMOVAL_OF_PROTECTED, names_vector)) {
                problem_output.push_back(
                    std::make_pair(ProblemRules::RULE_PKG_REMOVAL_OF_PROTECTED, std::move(names_vector)));
            }
        }
        return problem_output;
    }
    auto protected_packages = solved_goal.get_protected_packages();

    if ((!protected_packages || protected_packages->empty()) && protected_running_kernel.id <= 0) {
        return problem_output;
    }

    for (const auto & broken_pkg : broken_installed_query) {
        if (broken_pkg.get_id() == protected_running_kernel) {
            std::vector<std::string> elements;
            elements.emplace_back(broken_pkg.get_full_nevra());
            if (is_unique(problem_output, ProblemRules::RULE_PKG_REMOVAL_OF_RUNNING_KERNEL, elements)) {
                problem_output.push_back(
                    std::make_pair(ProblemRules::RULE_PKG_REMOVAL_OF_RUNNING_KERNEL, std::move(elements)));
            }
        } else if (protected_packages && protected_packages->contains_unsafe(broken_pkg.get_id().id)) {
            names.emplace(broken_pkg.get_name());
        }
    }
    if (!names.empty()) {
        std::vector<std::string> names_vector(names.begin(), names.end());
        if (is_unique(problem_output, ProblemRules::RULE_PKG_BROKEN_DEPS_OF_PROTECTED, names_vector)) {
            problem_output.push_back(
                std::make_pair(ProblemRules::RULE_PKG_BROKEN_DEPS_OF_PROTECTED, std::move(names_vector)));
        }
    }
    return problem_output;
}

class TransactionLocker : public libdnf5::utils::Locker {
public:
    TransactionLocker(const std::filesystem::path & path, const ActiveTransactionInfo & info);

    void write_metadata();

private:
    ActiveTransactionInfo transaction_info;
};

TransactionLocker::TransactionLocker(const std::filesystem::path & path, const ActiveTransactionInfo & info)
    : Locker(path.string()),
      transaction_info(info) {}

void TransactionLocker::write_metadata() {
    write_content(transaction_info.to_toml());
}

}  // namespace

Transaction::Transaction(const BaseWeakPtr & base) : p_impl(new Impl(*this, base)) {}
Transaction::Transaction(const Transaction & transaction) : p_impl(new Impl(*this, *transaction.p_impl)) {}

Transaction::Transaction(Transaction && transaction) : p_impl(std::move(transaction.p_impl)) {
    p_impl->transaction = this;
}

Transaction::~Transaction() = default;

Transaction::Impl::~Impl() {
    if (libsolv_transaction) {
        transaction_free(libsolv_transaction);
    }
}

Transaction::Impl::Impl(Transaction & transaction, const BaseWeakPtr & base)
    : transaction(&transaction),
      base(base),
      rpm_signature(base) {}

Transaction::Impl::Impl(Transaction & transaction, const Impl & src)
    : transaction(&transaction),
      base(src.base),
      libsolv_transaction(src.libsolv_transaction ? transaction_create_clone(src.libsolv_transaction) : nullptr),
      problems(src.problems),
      rpm_signature(src.rpm_signature),
      packages(src.packages),
      groups(src.groups),
      environments(src.environments),
#ifdef WITH_MODULEMD
      modules(src.modules),
      module_db(src.module_db),
#endif
      resolve_logs(src.resolve_logs),
      transaction_problems(src.transaction_problems),
      signature_problems(src.signature_problems),
      broken_dependency_packages(src.broken_dependency_packages),
      conflicting_packages(src.conflicting_packages),
      vendor_change_skipped_packages(src.vendor_change_skipped_packages) {
}

Transaction::Impl & Transaction::Impl::operator=(const Impl & other) {
    base = other.base;
    libsolv_transaction = other.libsolv_transaction ? transaction_create_clone(other.libsolv_transaction) : nullptr;
    problems = other.problems;
    rpm_signature = other.rpm_signature;
    packages = other.packages;
    groups = other.groups;
    environments = other.environments;
#ifdef WITH_MODULEMD
    modules = other.modules;
    module_db = other.module_db;
#endif
    resolve_logs = other.resolve_logs;
    transaction_problems = other.transaction_problems;
    signature_problems = other.signature_problems;
    broken_dependency_packages = other.broken_dependency_packages;
    conflicting_packages = other.conflicting_packages;
    vendor_change_skipped_packages = other.vendor_change_skipped_packages;
    return *this;
}

GoalProblem Transaction::get_problems() {
    return p_impl->problems;
}

std::vector<TransactionPackage> Transaction::get_transaction_packages() const {
    return p_impl->packages;
}

std::size_t Transaction::get_transaction_packages_count() const {
    return p_impl->packages.size();
}

std::vector<TransactionGroup> & Transaction::get_transaction_groups() const {
    return p_impl->groups;
}

std::vector<TransactionEnvironment> & Transaction::get_transaction_environments() const {
    return p_impl->environments;
}

std::vector<TransactionModule> & Transaction::get_transaction_modules() const {
    return p_impl->modules;
}

bool Transaction::empty() const {
    return p_impl->packages.empty() && p_impl->groups.empty() && p_impl->environments.empty() &&
           p_impl->modules.empty();
}

GoalProblem Transaction::Impl::report_not_found(
    GoalAction action,
    const std::string & pkg_spec,
    const GoalJobSettings & settings,
    libdnf5::Logger::Level log_level) {
    auto sack = base->get_rpm_package_sack();
    rpm::PackageQuery query(base, rpm::PackageQuery::ExcludeFlags::IGNORE_EXCLUDES);
    if (action == GoalAction::REMOVE || action == GoalAction::REPLAY_REMOVE) {
        query.filter_installed();
    }
    auto nevra_pair_reports = query.resolve_pkg_spec(pkg_spec, settings, true);
    if (!nevra_pair_reports.first) {
        // RPM was not excluded or there is no related srpm
        add_resolve_log(
            action,
            GoalProblem::NOT_FOUND,
            settings,
            libdnf5::transaction::TransactionItemType::PACKAGE,
            pkg_spec,
            {},
            log_level);
        if (settings.get_report_hint()) {
            rpm::PackageQuery hints(base);
            if (action == GoalAction::REMOVE || action == GoalAction::REPLAY_REMOVE) {
                hints.filter_installed();
            }
            if (!settings.get_ignore_case() && settings.get_with_nevra()) {
                rpm::PackageQuery icase(hints);
                ResolveSpecSettings settings_copy(settings);
                settings_copy.set_ignore_case(true);
                settings_copy.set_with_provides(false);
                settings_copy.set_with_filenames(false);
                settings_copy.set_with_binaries(false);
                auto nevra_pair_icase = icase.resolve_pkg_spec(pkg_spec, settings_copy, false);
                if (nevra_pair_icase.first) {
                    add_resolve_log(
                        action,
                        GoalProblem::HINT_ICASE,
                        settings,
                        libdnf5::transaction::TransactionItemType::PACKAGE,
                        pkg_spec,
                        {(*icase.begin()).get_name()},
                        libdnf5::Logger::Level::WARNING);
                }
            }
            rpm::PackageQuery alternatives(hints);
            std::string alternatives_provide = fmt::format("alternative-for({})", pkg_spec);
            alternatives.filter_provides({alternatives_provide});
            if (!alternatives.empty()) {
                std::set<std::string> hints;
                for (auto pkg : alternatives) {
                    hints.emplace(pkg.get_name());
                }
                add_resolve_log(
                    action,
                    GoalProblem::HINT_ALTERNATIVES,
                    settings,
                    libdnf5::transaction::TransactionItemType::PACKAGE,
                    pkg_spec,
                    hints,
                    libdnf5::Logger::Level::WARNING);
            }
        }
        return GoalProblem::NOT_FOUND;
    }
    query.filter_arch(std::vector<std::string>{"src", "nosrc"}, sack::QueryCmp::NEQ);
    if (query.empty()) {
        add_resolve_log(
            action,
            GoalProblem::ONLY_SRC,
            settings,
            libdnf5::transaction::TransactionItemType::PACKAGE,
            pkg_spec,
            {},
            log_level);
        return GoalProblem::ONLY_SRC;
    }
    query.filter_versionlock();
    if (query.empty()) {
        add_resolve_log(
            action,
            GoalProblem::EXCLUDED_VERSIONLOCK,
            settings,
            libdnf5::transaction::TransactionItemType::PACKAGE,
            pkg_spec,
            {},
            log_level);
        return GoalProblem::EXCLUDED_VERSIONLOCK;
    } else {
        // TODO(jmracek) make difference between regular excludes and modular excludes
        add_resolve_log(
            action,
            GoalProblem::EXCLUDED,
            settings,
            libdnf5::transaction::TransactionItemType::PACKAGE,
            pkg_spec,
            {},
            log_level);
        return GoalProblem::EXCLUDED;
    }
}

void Transaction::Impl::add_resolve_log(
    GoalAction action,
    GoalProblem problem,
    const GoalJobSettings & settings,
    const libdnf5::transaction::TransactionItemType spec_type,
    const std::string & spec,
    const std::set<std::string> & additional_data,
    libdnf5::Logger::Level log_level) {
    resolve_logs.emplace_back(LogEvent(action, problem, additional_data, settings, spec_type, spec));
    auto & logger = *base->get_logger();
    logger.log_line(log_level, resolve_logs.back().to_string());
}

void Transaction::Impl::add_resolve_log(
    GoalProblem problem,
    std::vector<std::vector<std::pair<libdnf5::ProblemRules, std::vector<std::string>>>> problems) {
    add_resolve_log(problem, SolverProblems(std::move(problems)));
}

void Transaction::Impl::add_resolve_log(GoalProblem problem, const SolverProblems & problems) {
    resolve_logs.emplace_back(LogEvent(problem, problems));
    auto & logger = *base->get_logger();
    logger.log_line(libdnf5::Logger::Level::ERROR, resolve_logs.back().to_string());
}

const std::vector<LogEvent> & Transaction::get_resolve_logs() const {
    return p_impl->resolve_logs;
}

std::vector<std::string> Transaction::get_resolve_logs_as_strings() const {
    std::vector<std::string> logs;
    for (const auto & log : get_resolve_logs()) {
        logs.emplace_back(log.to_string());
    }
    return logs;
}

std::vector<libdnf5::rpm::Package> Transaction::get_broken_dependency_packages() const {
    return p_impl->broken_dependency_packages;
}

std::vector<libdnf5::rpm::Package> Transaction::get_conflicting_packages() const {
    return p_impl->conflicting_packages;
}

const std::vector<std::pair<libdnf5::rpm::Package, std::string>> & Transaction::get_vendor_change_skipped_packages()
    const {
    return p_impl->vendor_change_skipped_packages;
}

std::string Transaction::transaction_result_to_string(const TransactionRunResult result) {
    switch (result) {
        case TransactionRunResult::SUCCESS:
            return {};
        case TransactionRunResult::ERROR_RERUN:
        case TransactionRunResult::ERROR_RESOLVE:
        case TransactionRunResult::ERROR_CHECK:
        case TransactionRunResult::ERROR_LOCK:
        case TransactionRunResult::ERROR_RPM_RUN:
        case TransactionRunResult::ERROR_GPG_CHECK:
            return TM_(TRANSACTION_RUN_RESULT_DICT.at(result), 1);
    }
    return {};
}

void Transaction::download() {
    libdnf5::repo::PackageDownloader downloader(p_impl->base);
    for (auto & tspkg : this->get_transaction_packages()) {
        if (transaction_item_action_is_inbound(tspkg.get_action()) &&
            (get_download_local_pkgs() ||
             tspkg.get_package().get_repo()->get_type() != libdnf5::repo::Repo::Type::COMMANDLINE)) {
            downloader.add(tspkg.get_package());
        }
    }
    downloader.download();
}

Transaction::TransactionRunResult Transaction::test() {
    return p_impl->test();
}

Transaction::TransactionRunResult Transaction::run() {
    return p_impl->run(std::move(callbacks), description, user_id, comment);
}

std::vector<std::string> Transaction::get_transaction_problems() const noexcept {
    return p_impl->transaction_problems;
}

void Transaction::set_callbacks(std::unique_ptr<libdnf5::rpm::TransactionCallbacks> && callbacks) {
    this->callbacks = std::move(callbacks);
}

void Transaction::set_description(const std::string & description) {
    this->description = description;
}

void Transaction::set_user_id(const uint32_t user_id) {
    this->user_id = user_id;
}

void Transaction::set_comment(const std::string & comment) {
    this->comment = comment;
}

void Transaction::set_persistence(libdnf5::base::TransactionPersistence persistence) {
    this->persistence = persistence;
}

bool Transaction::check_gpg_signatures() {
    return p_impl->check_gpg_signatures();
}

std::vector<std::string> Transaction::get_gpg_signature_problems() const noexcept {
    return p_impl->signature_problems;
}

void Transaction::Impl::process_solver_problems(rpm::solv::GoalPrivate & solved_goal) {
    auto & pool = get_rpm_pool(base);
    auto * installed_repo = pool->installed;

    libdnf5::rpm::PackageQuery skip_broken(base, libdnf5::sack::ExcludeFlags::APPLY_EXCLUDES, true);
    libdnf5::rpm::PackageQuery skip_conflict(base, libdnf5::sack::ExcludeFlags::APPLY_EXCLUDES, true);

    auto goal_solver_problems = solved_goal.get_problems();

    solver_problems.clear();

    for (auto & problem : goal_solver_problems) {
        std::set<std::pair<int, int>> current_conflicting_rules;
        std::vector<std::pair<ProblemRules, std::vector<std::string>>> problem_output;

        for (auto & [rule, source, dep, target, description] : problem) {
            std::vector<std::string> elements;
            ProblemRules tmp_rule = rule;
            bool installed_conflicts = false;
            switch (rule) {
                case ProblemRules::RULE_INFARCH:
                case ProblemRules::RULE_PKG_NOT_INSTALLABLE_2:
                case ProblemRules::RULE_PKG_NOT_INSTALLABLE_3: {
                    auto * src_solvable = pool.id2solvable(source);
                    elements.push_back(pool.solvable2str(src_solvable));
                    elements.push_back(src_solvable->repo->name);
                    break;
                }
                case ProblemRules::RULE_DISTUPGRADE:
                case ProblemRules::RULE_UPDATE:
                case ProblemRules::RULE_BEST_1:
                    elements.push_back(pool.solvid2str(source));
                    break;
                case ProblemRules::RULE_JOB:
                case ProblemRules::RULE_JOB_UNSUPPORTED:
                case ProblemRules::RULE_PKG:
                case ProblemRules::RULE_BEST_2:
                    break;
                case ProblemRules::RULE_JOB_NOTHING_PROVIDES_DEP:
                case ProblemRules::RULE_JOB_UNKNOWN_PACKAGE:
                case ProblemRules::RULE_JOB_PROVIDED_BY_SYSTEM:
                    elements.push_back(pool.dep2str(dep));
                    break;
                case ProblemRules::RULE_PKG_NOT_INSTALLABLE_1:
                case ProblemRules::RULE_PKG_NOT_INSTALLABLE_4: {
                    if (false) {
                        // TODO (jmracek) (modularExclude && modularExclude->has(source))
                    } else {
                        tmp_rule = ProblemRules::RULE_PKG_NOT_INSTALLABLE_4;
                    }
                    auto * src_solvable = pool.id2solvable(source);
                    elements.push_back(pool.solvable2str(src_solvable));
                    elements.push_back(src_solvable->repo->name);
                    break;
                }
                case ProblemRules::RULE_PKG_SELF_CONFLICT: {
                    skip_conflict.add(libdnf5::rpm::Package(base, libdnf5::rpm::PackageId(source)));
                    auto * src_solvable = pool.id2solvable(source);
                    elements.push_back(pool.solvable2str(src_solvable));
                    elements.push_back(src_solvable->repo->name);
                    elements.push_back(pool.dep2str(dep));
                    break;
                }
                case ProblemRules::RULE_PKG_INSTALLED_REQUIRES:
                    skip_broken.add(libdnf5::rpm::Package(base, libdnf5::rpm::PackageId(source)));
                    elements.push_back(pool.dep2str(dep));
                    elements.push_back(pool.solvid2str(source));
                    break;
                case ProblemRules::RULE_PKG_NOTHING_PROVIDES_DEP:
                case ProblemRules::RULE_PKG_REQUIRES: {
                    skip_broken.add(libdnf5::rpm::Package(base, libdnf5::rpm::PackageId(source)));
                    auto * src_solvable = pool.id2solvable(source);
                    elements.push_back(pool.dep2str(dep));
                    elements.push_back(pool.solvable2str(src_solvable));
                    elements.push_back(src_solvable->repo->name);
                    break;
                }
                case ProblemRules::RULE_PKG_SAME_NAME: {
                    skip_conflict.add(libdnf5::rpm::Package(base, libdnf5::rpm::PackageId(source)));
                    skip_conflict.add(libdnf5::rpm::Package(base, libdnf5::rpm::PackageId(target)));
                    auto * src_solvable = pool.id2solvable(source);
                    elements.push_back(pool.solvable2str(src_solvable));
                    elements.push_back(src_solvable->repo->name);
                    auto * tgt_solvable = pool.id2solvable(target);
                    elements.push_back(pool.solvable2str(tgt_solvable));
                    elements.push_back(tgt_solvable->repo->name);
                    break;
                }
                case ProblemRules::RULE_PKG_INSTALLED_CONFLICTS:
                    installed_conflicts = true;
                    [[fallthrough]];
                case ProblemRules::RULE_PKG_CONFLICTS:
                    if (current_conflicting_rules.contains(std::pair<int, int>(target, source))) {
                        // do not add pkgA conflicts with pkgB rule if pkgB conflicts with PkgA rule is already present
                        continue;
                    }
                    skip_conflict.add(libdnf5::rpm::Package(base, libdnf5::rpm::PackageId(source)));
                    skip_conflict.add(libdnf5::rpm::Package(base, libdnf5::rpm::PackageId(target)));
                    current_conflicting_rules.emplace(source, target);
                    if (installed_conflicts) {
                        // RULE_PKG_INSTALLED_CONFLICTS expects the first(source)
                        // element to be an installed package
                        if (pool.id2solvable(source)->repo != installed_repo) {
                            std::swap(source, target);
                        }
                    }
                    [[fallthrough]];
                case ProblemRules::RULE_PKG_OBSOLETES:
                case ProblemRules::RULE_PKG_IMPLICIT_OBSOLETES:
                case ProblemRules::RULE_YUMOBS: {
                    auto * src_solvable = pool.id2solvable(source);
                    elements.push_back(pool.solvable2str(src_solvable));
                    if (!installed_conflicts) {
                        elements.push_back(src_solvable->repo->name);
                    }
                    elements.push_back(pool.dep2str(dep));
                    auto * tgt_solvable = pool.id2solvable(target);
                    elements.push_back(pool.solvable2str(tgt_solvable));
                    elements.push_back(tgt_solvable->repo->name);
                    break;
                }
                case ProblemRules::RULE_PKG_INSTALLED_OBSOLETES: {
                    elements.push_back(pool.solvid2str(source));
                    elements.push_back(pool.dep2str(dep));
                    auto * tgt_solvable = pool.id2solvable(target);
                    elements.push_back(pool.solvable2str(tgt_solvable));
                    elements.push_back(tgt_solvable->repo->name);
                    break;
                }
                case ProblemRules::RULE_UNKNOWN:
                    elements.push_back(description);
                    break;
                case ProblemRules::RULE_PKG_REMOVAL_OF_PROTECTED:
                case ProblemRules::RULE_PKG_REMOVAL_OF_RUNNING_KERNEL:
                case ProblemRules::RULE_PKG_BROKEN_DEPS_OF_PROTECTED:
                    // Rules are not generated by libsolv
                    break;
                case ProblemRules::RULE_MODULE_DISTUPGRADE:
                case ProblemRules::RULE_MODULE_INFARCH:
                case ProblemRules::RULE_MODULE_UPDATE:
                case ProblemRules::RULE_MODULE_JOB:
                case ProblemRules::RULE_MODULE_JOB_UNSUPPORTED:
                case ProblemRules::RULE_MODULE_JOB_NOTHING_PROVIDES_DEP:
                case ProblemRules::RULE_MODULE_JOB_UNKNOWN_PACKAGE:
                case ProblemRules::RULE_MODULE_JOB_PROVIDED_BY_SYSTEM:
                case ProblemRules::RULE_MODULE_PKG:
                case ProblemRules::RULE_MODULE_BEST_1:
                case ProblemRules::RULE_MODULE_BEST_2:
                case ProblemRules::RULE_MODULE_PKG_NOT_INSTALLABLE_1:
                case ProblemRules::RULE_MODULE_PKG_NOT_INSTALLABLE_2:
                case ProblemRules::RULE_MODULE_PKG_NOT_INSTALLABLE_3:
                case ProblemRules::RULE_MODULE_PKG_NOT_INSTALLABLE_4:
                case ProblemRules::RULE_MODULE_PKG_NOTHING_PROVIDES_DEP:
                case ProblemRules::RULE_MODULE_PKG_SAME_NAME:
                case ProblemRules::RULE_MODULE_PKG_CONFLICTS:
                case ProblemRules::RULE_MODULE_PKG_OBSOLETES:
                case ProblemRules::RULE_MODULE_PKG_INSTALLED_OBSOLETES:
                case ProblemRules::RULE_MODULE_PKG_IMPLICIT_OBSOLETES:
                case ProblemRules::RULE_MODULE_PKG_REQUIRES:
                case ProblemRules::RULE_MODULE_PKG_SELF_CONFLICT:
                case ProblemRules::RULE_MODULE_YUMOBS:
                case ProblemRules::RULE_MODULE_UNKNOWN:
                    libdnf_throw_assertion("Unexpected module problem rule in rpm goal");
            }
            if (is_unique(problem_output, tmp_rule, elements)) {
                problem_output.push_back(std::make_pair(tmp_rule, std::move(elements)));
            }
        }
        if (is_unique(solver_problems, problem_output)) {
            solver_problems.push_back(std::move(problem_output));
        }
    }

    libdnf5::rpm::PackageQuery broken_installed(skip_broken);
    broken_installed.filter_installed();
    auto problem_protected = get_removal_of_protected(solved_goal, broken_installed);
    if (!problem_protected.empty()) {
        if (is_unique(solver_problems, problem_protected)) {
            solver_problems.insert(solver_problems.begin(), std::move(problem_protected));
        }
    }
    // packages skipped due to broken dependencies
    // only available packages, filter out installed packages with the same NEVRA
    skip_broken.filter_available();
    skip_broken.filter_nevra(broken_installed, libdnf5::sack::QueryCmp::NEQ);
    broken_dependency_packages.clear();
    for (auto pkg : skip_broken) {
        broken_dependency_packages.push_back(std::move(pkg));
    }

    // packages skipped due to the conflict
    // only available packages, filter out installed packages with the same NEVRA
    libdnf5::rpm::PackageQuery conflict_installed(skip_conflict);
    conflict_installed.filter_installed();
    skip_conflict.filter_available();
    skip_conflict.filter_nevra(conflict_installed, libdnf5::sack::QueryCmp::NEQ);
    conflicting_packages.clear();
    for (auto pkg : skip_conflict) {
        conflicting_packages.push_back(std::move(pkg));
    }
}

void Transaction::Impl::set_transaction(
    rpm::solv::GoalPrivate & solved_goal,
#ifdef WITH_MODULEMD
    module::ModuleSack & module_sack,
#endif
    GoalProblem problems) {
    process_solver_problems(solved_goal);

    // Snapshot blocked_vendor_changes from the first solve before the strict-mode
    // re-solve below can add entries from its own vendor-change denials.
    const auto blocked_vendor_changes = get_rpm_pool(base).get_blocked_vendor_changes();

    if (!solver_problems.empty()) {
        add_resolve_log(GoalProblem::SOLVER_ERROR, solver_problems);
    } else {
        // TODO(jmracek) To improve performance add a test whether it make sense to resolve transaction in strict mode
        // Test whether there were skipped jobs or used not the best candidates due to broken dependencies
        rpm::solv::GoalPrivate solved_goal_copy(solved_goal);
        solved_goal_copy.set_run_in_strict_mode(true);
        solved_goal_copy.resolve();
        process_solver_problems(solved_goal_copy);
        if (!solver_problems.empty()) {
            add_resolve_log(GoalProblem::SOLVER_PROBLEM_STRICT_RESOLVEMENT, solver_problems);
        }
    }

    // Third solve: detect packages skipped due to vendor change restriction.
    // Only run when vendor change is disallowed and the solver actually
    // rejected at least one vendor change during the first solve.
    if (!base->get_config().get_allow_vendor_change_option().get_value() && !blocked_vendor_changes.empty()) {
        rpm::solv::GoalPrivate vendor_goal(solved_goal);
        vendor_goal.set_allow_vendor_change(true);
        const auto vendor_problems = vendor_goal.resolve();
        if ((vendor_problems & GoalProblem::SOLVER_ERROR) != GoalProblem::SOLVER_ERROR &&
            vendor_goal.get_transaction()) {
            auto & pool = get_rpm_pool(base);
            std::set<std::string> original_nevras;
            if (solved_goal.get_transaction()) {
                for (auto id : solved_goal.list_upgrades()) {
                    original_nevras.emplace(pool.get_nevra(id));
                }
                for (auto id : solved_goal.list_downgrades()) {
                    original_nevras.emplace(pool.get_nevra(id));
                }
                for (auto id : solved_goal.list_reinstalls()) {
                    original_nevras.emplace(pool.get_nevra(id));
                }
                for (auto id : solved_goal.list_installs()) {
                    original_nevras.emplace(pool.get_nevra(id));
                }
            }

            // A candidate was skipped due to vendor change restriction when:
            //   1. vendor_goal (vendor change allowed) would include it, and
            //   2. the first solve excluded it, and
            //   3. the first solve's vendor-change callback denied the transition.
            // blocked_vendor_changes maps incoming solvable IDs to the installed
            // vendor string for every transition the callback denied, covering all
            // categories — upgrades, downgrades, reinstalls, installs-via-obsolete,
            // and installonly packages.
            auto check = [&](libdnf5::solv::IdQueue && ids) {
                for (auto id : ids) {
                    if (original_nevras.contains(pool.get_nevra(id))) {
                        continue;
                    }
                    auto it = blocked_vendor_changes.find(id);
                    if (it != blocked_vendor_changes.end()) {
                        vendor_change_skipped_packages.emplace_back(rpm::Package(base, rpm::PackageId(id)), it->second);
                    }
                }
            };

            check(vendor_goal.list_upgrades());
            check(vendor_goal.list_downgrades());
            check(vendor_goal.list_reinstalls());
            check(vendor_goal.list_installs());
        }
    }

    this->problems = problems;

    if ((problems & GoalProblem::MODULE_SOLVER_ERROR) != GoalProblem::NO_PROBLEM ||
        ((problems & GoalProblem::MODULE_SOLVER_ERROR_LATEST) != GoalProblem::NO_PROBLEM &&
         base->get_config().get_best_option().get_value())) {
        // There is a fatal error in resolving modules
        return;
    }

    auto transaction = solved_goal.get_transaction();
    libsolv_transaction = transaction ? transaction_create_clone(transaction) : nullptr;
    if (!libsolv_transaction) {
        return;
    }

    // std::map<replaced, replaced_by>
    std::map<Id, std::vector<Id>> replaced;

    rpm::PackageQuery installed_query(base, rpm::PackageQuery::ExcludeFlags::IGNORE_EXCLUDES);
    installed_query.filter_installed();

    // The order of packages in the vector matters, we rely on outbound actions
    // being at the end in Transaction::Impl::run()
    for (auto id : solved_goal.list_installs()) {
        packages.emplace_back(
            make_transaction_package(id, TransactionPackage::Action::INSTALL, solved_goal, replaced, installed_query));
    }

    for (auto id : solved_goal.list_reinstalls()) {
        packages.emplace_back(make_transaction_package(
            id, TransactionPackage::Action::REINSTALL, solved_goal, replaced, installed_query));
    }

    for (auto id : solved_goal.list_upgrades()) {
        packages.emplace_back(
            make_transaction_package(id, TransactionPackage::Action::UPGRADE, solved_goal, replaced, installed_query));
    }

    for (auto id : solved_goal.list_downgrades()) {
        packages.emplace_back(make_transaction_package(
            id, TransactionPackage::Action::DOWNGRADE, solved_goal, replaced, installed_query));
    }

    for (auto id : solved_goal.list_removes()) {
        packages.emplace_back(TransactionPackage(
            rpm::Package(base, rpm::PackageId(id)), TransactionPackage::Action::REMOVE, solved_goal.get_reason(id)));
    }

    // Add replaced packages to transaction
    for (const auto & [replaced_id, replaced_by_ids] : replaced) {
        rpm::Package obsoleted(base, rpm::PackageId(replaced_id));
        TransactionPackage tspkg(obsoleted, TransactionPackage::Action::REPLACED, obsoleted.get_reason());
        for (auto id : replaced_by_ids) {
            tspkg.p_impl->replaced_by_append(rpm::Package(base, rpm::PackageId(id)));
        }
        packages.emplace_back(std::move(tspkg));
    }

    // Add environmental groups to the transaction
    for (auto & [environment, action, reason, with_optional] : solved_goal.list_environments()) {
        TransactionEnvironment tsenv(environment, action, reason, with_optional);
        environments.emplace_back(std::move(tsenv));
    }

    // Add groups to the transaction
    for (auto & [group, action, reason, package_types] : solved_goal.list_groups()) {
        TransactionGroup tsgrp(group, action, reason, package_types);
        groups.emplace_back(std::move(tsgrp));
    }

#ifdef WITH_MODULEMD
    // Add modules to the transaction
    module_db = module_sack.p_impl->module_db->get_weak_ptr();
    for (auto & [name, stream] : module_db->get_all_newly_enabled_streams()) {
        TransactionModule tsmodule(
            name, stream, transaction::TransactionItemAction::ENABLE, transaction::TransactionItemReason::USER);
        modules.emplace_back(std::move(tsmodule));
    }
    for (auto & name : module_db->get_all_newly_disabled_modules()) {
        TransactionModule tsmodule(
            name, "", transaction::TransactionItemAction::DISABLE, transaction::TransactionItemReason::USER);
        modules.emplace_back(std::move(tsmodule));
    }
    for (auto & name : module_db->get_all_newly_reset_modules()) {
        TransactionModule tsmodule(
            name, "", transaction::TransactionItemAction::RESET, transaction::TransactionItemReason::USER);
        modules.emplace_back(std::move(tsmodule));
    }
    for (auto & name_streams : module_db->get_all_newly_switched_streams()) {
        TransactionModule tsmodule(
            name_streams.first,
            name_streams.second.first,
            transaction::TransactionItemAction::SWITCH,
            transaction::TransactionItemReason::USER);
        tsmodule.p_impl->replaces_append(std::string(name_streams.first), std::string(name_streams.second.second));
        modules.emplace_back(std::move(tsmodule));
    }
#endif

    // Add reason change actions to the transaction
    for (auto & [pkg, reason, group_id] : solved_goal.list_reason_changes()) {
        TransactionPackage tspkg(pkg, TransactionPackage::Action::REASON_CHANGE, reason, group_id);
        packages.emplace_back(std::move(tspkg));
    }

    // After all packages were added check for extra packages and rpm reason overrides
    if (!rpm_reason_overrides.empty() || !rpm_replays_nevra_cache.empty()) {
        for (auto & pkg : packages) {
            auto nevra = pkg.get_package().get_nevra();
            for (const auto & [rpm_cache, settings] : rpm_replays_nevra_cache) {
                if (!rpm_cache.contains(nevra)) {
                    // If ignore_installed is true we don't want to check for REPLACED extras in the
                    // transaction. Some operation had to be done on an installed package but we are
                    // ignoring it.
                    if (!(settings.get_ignore_installed() &&
                          pkg.get_action() == transaction::TransactionItemAction::REPLACED)) {
                        auto log_level = libdnf5::Logger::Level::WARNING;
                        if (!settings.get_ignore_extras()) {
                            this->problems |= GoalProblem::EXTRA;
                            log_level = libdnf5::Logger::Level::ERROR;
                        }
                        // report this as an error or a warning depending on the setting
                        this->add_resolve_log(
                            GoalAction::REPLAY_REASON_OVERRIDE,
                            GoalProblem::EXTRA,
                            GoalJobSettings(),
                            libdnf5::transaction::TransactionItemType::PACKAGE,
                            nevra,
                            {transaction_item_action_to_string(pkg.get_action())},
                            log_level);
                    }
                }
            }

            const auto reason_override = rpm_reason_overrides.find(nevra);
            if (reason_override != rpm_reason_overrides.end()) {
                // For UPGRADE, DOWNGRADE and REINSTALL change the reason only if it stronger.
                // This is required because we don't want to for example mark some user installed
                // package as a dependency (except when the user specifically asks for it - action REASON_CHANGE).
                if (pkg.get_action() == transaction::TransactionItemAction::INSTALL ||
                    pkg.get_action() == transaction::TransactionItemAction::REMOVE ||
                    (reason_override->second > pkg.get_reason() &&
                     pkg.get_action() != transaction::TransactionItemAction::REASON_CHANGE)) {
                    pkg.p_impl->set_reason(reason_override->second);
                }
            }
        }
    }
}


TransactionPackage Transaction::Impl::make_transaction_package(
    Id id,
    TransactionPackage::Action action,
    rpm::solv::GoalPrivate & solved_goal,
    std::map<Id, std::vector<Id>> & replaced,
    rpm::PackageQuery installed_query) {
    auto obs = solved_goal.list_obsoleted_by_package(id);

    rpm::Package new_package(base, rpm::PackageId(id));

    transaction::TransactionItemReason reason;
    if (action == TransactionPackage::Action::INSTALL) {
        reason = std::max(new_package.get_reason(), solved_goal.get_reason(id));

        installed_query.filter_name({new_package.get_name()});
        installed_query.filter_arch({new_package.get_arch()});
        // For installonly packages: if the NA is already on the system, but
        // not recorded in system state as installed, it was installed outside
        // DNF and we want to preserve NONE as reason
        // TODO(lukash) this is still required even with having EXTERNAL_USER
        // as a reason, because with --best the reason returned from the goal
        // is USER, which is wrong here
        if (!installed_query.empty() && new_package.get_reason() == transaction::TransactionItemReason::EXTERNAL_USER) {
            reason = transaction::TransactionItemReason::EXTERNAL_USER;
        }
    } else {
        reason = new_package.get_reason();
    }

    TransactionPackage tspkg(new_package, action, reason);

    for (auto replaced_id : obs) {
        rpm::Package replaced_pkg(base, rpm::PackageId(replaced_id));
        reason = std::max(reason, replaced_pkg.get_reason());
        tspkg.p_impl->replaces_append(std::move(replaced_pkg));
        replaced[replaced_id].push_back(id);
    }
    tspkg.p_impl->set_reason(reason);

    return tspkg;
}

// Reads the output of scriptlets from the file descriptor and processes them.
void Transaction::Impl::process_scriptlets_output(int fd) {
    utils::OnScopeExit close_fd([this, fd]() noexcept {
        std::unique_lock<std::mutex> lock(scriptlet_pipe_sync_mutex);
        scriptlet_read_end_pipe_fd = -1;
        scriptlet_pipe_sync_cv.notify_all();
        close(fd);
    });
    auto logger = base->get_logger().get();
    // Set the pipe fd as not blocking so that we can later read all current pipe content without blocking
    if (fcntl(fd, F_SETFL, O_NONBLOCK) == -1) {
        logger->error("Transaction::Run: Cannot set scriptlet output pipe as non-blocking: {}", std::strerror(errno));
        return;
    }
    struct pollfd pfd{fd, POLLIN, 0};
    try {
        char buf[512];
        while (true) {
            const int poll_result = poll(&pfd, 1, -1);
            if (poll_result == -1 && errno != EINTR) {
                throw SystemError(errno, M_("poll failed during scriptlets pipe processing"));
            }
            if (pfd.revents & (POLLHUP | POLLIN)) {
                std::unique_lock<std::mutex> lock(scriptlet_pipe_sync_mutex);

                bool got_data = false;
                ssize_t bytes_read = 0;
                while ((bytes_read = read(fd, buf, sizeof(buf))) > 0) {
                    got_data = true;
                    std::string_view str(buf, static_cast<size_t>(bytes_read));
                    append_last_script_output(str);
                    std::string_view::size_type start = 0;
                    do {
                        auto end = str.find('\n', start);
                        logger->info("[scriptlet] {}", str.substr(start, end - start));
                        if (end == std::string_view::npos) {
                            break;
                        }
                        start = end + 1;
                    } while (start < str.size());
                }

                if (bytes_read < 0 && errno != EAGAIN && errno != EINTR) {
                    throw SystemError(errno, M_("failed to read scriptlet output from pipe"));
                }
                if (got_data) {
                    scriptlet_pipe_sync_cv.notify_all();
                }

                // EOF - pipe is closed
                if (bytes_read == 0) {
                    return;
                }
            }
            if (pfd.revents & (POLLHUP | POLLERR)) {
                return;
            }
        }
    } catch (const std::exception & ex) {
        // The thread must not throw exceptions.
        logger->error("Transaction::Run: Exception while processing scriptlet output: {}", ex.what());
    }
}

void Transaction::Impl::flush_last_script_output() {
    std::unique_lock<std::mutex> lock(scriptlet_pipe_sync_mutex);
    while (true) {
        if (scriptlet_read_end_pipe_fd == -1) {
            break;
        }
        int bytes = 0;
        if (ioctl(scriptlet_read_end_pipe_fd, FIONREAD, &bytes) == -1) {
            // If we fail to read number of bytes don't wait since the reader thread likely has problems as well.
            break;
        }
        if (bytes == 0) {
            break;
        }
        scriptlet_pipe_sync_cv.wait(lock);
    }
}

Transaction::TransactionRunResult Transaction::Impl::test() {
    return this->_run(std::make_unique<libdnf5::rpm::TransactionCallbacks>(), "", std::nullopt, "", true);
}

Transaction::TransactionRunResult Transaction::Impl::run(
    std::unique_ptr<libdnf5::rpm::TransactionCallbacks> && callbacks,
    const std::string & description,
    const std::optional<uint32_t> user_id,
    const std::string & comment) {
    return this->_run(std::move(callbacks), description, user_id, comment, false);
}

Transaction::TransactionRunResult Transaction::Impl::_run(
    std::unique_ptr<libdnf5::rpm::TransactionCallbacks> && callbacks,
    const std::string & description,
    const std::optional<uint32_t> user_id,
    const std::string & comment,
    const bool test_only) {
    concurrent_transaction.reset();

    // do not allow running a transaction multiple times
    if (history_db_id > 0) {
        return TransactionRunResult::ERROR_RERUN;
    }

    // only successfully resolved transaction can be run
    if (transaction->get_problems() != libdnf5::GoalProblem::NO_PROBLEM) {
        return TransactionRunResult::ERROR_RESOLVE;
    }

    if (!check_gpg_signatures()) {
        return TransactionRunResult::ERROR_GPG_CHECK;
    }

    auto & config = base->get_config();

    // prepare transaction info
    ActiveTransactionInfo info;
    info.set_description(description);
    info.set_comment(comment);
    info.set_pid(getpid());
    info.set_start_time(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()));

    // acquire the lock
    std::filesystem::path lock_file_path = config.get_installroot_option().get_value();
    lock_file_path /= std::filesystem::path(libdnf5::TRANSACTION_LOCK_FILEPATH).relative_path();
    std::filesystem::create_directories(lock_file_path.parent_path());

    auto logger = base->get_logger().get();
    TransactionLocker transaction_locker(lock_file_path, info);
    if (!transaction_locker.write_lock()) {
        auto content = transaction_locker.read_content();
        try {
            concurrent_transaction = std::make_unique<ActiveTransactionInfo>(ActiveTransactionInfo::from_toml(content));
        } catch (const ActiveTransactionInfoParseError & ex) {
            logger->warning(ex.what());
        }
        return TransactionRunResult::ERROR_LOCK;
    }
    transaction_locker.write_metadata();

    libdnf5::rpm::Transaction rpm_transaction(base);

    rpmtransFlags rpm_transaction_flags{RPMTRANS_FLAG_NONE};
    for (const auto & tsflag : config.get_tsflags_option().get_value()) {
        bool found = false;
        for (const auto & [string_name, enum_item] : string_tsflag_map) {
            if (tsflag == string_name) {
                rpm_transaction_flags |= static_cast<rpmtransFlags>(enum_item);
                found = true;
                break;
            }
        }
        if (!found) {
            throw TransactionError(M_("Invalid tsflag: {}"), tsflag);
        }

        // The "nocrypto" option will also set signature verify flags.
        if (tsflag == "nocrypto") {
            rpm_transaction.set_signature_verify_flags(
                rpm_transaction.get_signature_verify_flags() | RPMVSF_MASK_NOSIGNATURES | RPMVSF_MASK_NODIGESTS);
        }
    }

    // Set flags to ensure "noplugins" option is set before transactions are started
    rpm_transaction.set_flags(rpm_transaction_flags);

    // fill and check the rpm transaction
    try {
        rpm_transaction.fill(*transaction);
    } catch (const rpm::TransactionError & e) {
        transaction_problems.emplace_back(e.what());
        return TransactionRunResult::ERROR_CHECK;
    }
    if (!rpm_transaction.check()) {
        for (auto it : rpm_transaction.get_problems()) {
            transaction_problems.emplace_back(it.to_string());
        }
        return TransactionRunResult::ERROR_CHECK;
    }

    // Run rpm transaction test
    rpm_transaction.set_flags(rpm_transaction_flags | RPMTRANS_FLAG_TEST);
    //TODO(jrohel): Do we want callbacks for transaction test?
    //rpm_transaction.set_callbacks(std::move(callbacks));
    auto ret = rpm_transaction.run();
    if (ret != 0) {
        for (auto it : rpm_transaction.get_problems()) {
            transaction_problems.emplace_back(it.to_string());
        }
        return TransactionRunResult::ERROR_RPM_RUN;
    }

    // With RPMTRANS_FLAG_TEST return just before anything is stored permanently
    if (test_only || rpm_transaction_flags & RPMTRANS_FLAG_TEST) {
        return TransactionRunResult::SUCCESS;
    }

    auto & plugins = base->p_impl->get_plugins();
    plugins.pre_transaction(*transaction);

    // start history db transaction
    auto db_transaction = libdnf5::transaction::Transaction(base);
    // save history db transaction id
    history_db_id = db_transaction.get_id();

    auto vars = base->get_vars();
    if (vars->contains("releasever")) {
        db_transaction.set_releasever(vars->get_value("releasever"));
    }

    db_transaction.set_comment(comment);
    db_transaction.set_description(description);

    if (user_id) {
        db_transaction.set_user_id(*user_id);
    } else {
        db_transaction.set_user_id(get_login_uid());
    }

    //
    // TODO(jrohel): nevra of running dnf5?
    //db_transaction.add_runtime_package("dnf5");

    db_transaction.set_rpmdb_version_begin(rpm_transaction.get_db_cookie());
    db_transaction.fill_transaction_packages(packages);

    if (!environments.empty()) {
        comps::GroupQuery installed_query(base);
        installed_query.filter_installed(true);
        std::set<std::string> installed_group_ids{};
        for (const auto & grp : installed_query) {
            installed_group_ids.emplace(grp.get_groupid());
        }
        for (const auto & tsgrp : groups) {
            if (transaction_item_action_is_inbound(tsgrp.get_action())) {
                installed_group_ids.emplace(tsgrp.get_group().get_groupid());
            }
        }
        db_transaction.fill_transaction_environments(environments, installed_group_ids);
    }

    if (!groups.empty()) {
        // consider currently installed packages + inbound packages as installed for group members
        rpm::PackageQuery installed_query(base);
        installed_query.filter_installed();
        std::set<std::string> installed_names{};
        for (const auto & pkg : installed_query) {
            installed_names.emplace(pkg.get_name());
        }
        for (const auto & tspkg : packages) {
            if (transaction_item_action_is_inbound(tspkg.get_action())) {
                installed_names.emplace(tspkg.get_package().get_name());
            }
        }
        db_transaction.fill_transaction_groups(groups, installed_names);
    }

    int64_t source_date_epoch = -1;
    const char * sde = std::getenv("SOURCE_DATE_EPOCH");
    if (sde != nullptr) {
        std::istringstream iss(sde);
        iss >> source_date_epoch;
        if (iss.fail() || !iss.eof() || source_date_epoch < 0) {
            auto logger = base->get_logger().get();
            logger->warning("Invalid SOURCE_DATE_EPOCH value '{}', using current time", sde);
            source_date_epoch = -1;
        }
    }

    int64_t dt_start;
    if (source_date_epoch >= 0) {
        dt_start = source_date_epoch;
    } else {
        auto time = std::chrono::system_clock::now().time_since_epoch();
        dt_start = std::chrono::duration_cast<std::chrono::seconds>(time).count();
    }
    db_transaction.set_dt_start(dt_start);
    db_transaction.start();

    int pipe_out_from_scriptlets[2];
    if (pipe2(pipe_out_from_scriptlets, O_CLOEXEC) == -1) {
        logger->error("Transaction::Run: Cannot create pipe: {}", std::strerror(errno));
        return TransactionRunResult::ERROR_RPM_RUN;
    }

    scriptlet_read_end_pipe_fd = pipe_out_from_scriptlets[0];

    // This thread processes the output of RPM scriptlets.
    std::thread thread_processes_scriptlets_output(
        &Transaction::Impl::process_scriptlets_output, this, pipe_out_from_scriptlets[0]);

    // Set file descriptor for output of scriptlets in transaction.
    rpm_transaction.set_script_out_fd(pipe_out_from_scriptlets[1]);
    // set_script_out_fd() copies the file descriptor using dup(). Closing the original fd.
    close(pipe_out_from_scriptlets[1]);

    rpm_transaction.set_base_transaction(transaction);
    rpm_transaction.set_callbacks(std::move(callbacks));
    rpm_transaction.set_flags(rpm_transaction_flags);

    // execute rpm transaction
    ret = rpm_transaction.run();

    // Reset/close file descriptor for output of RPM scriptlets. Required to end thread_processes_scriptlets_output.
    rpm_transaction.set_script_out_fd(-1);

    thread_processes_scriptlets_output.join();

    // TODO(mblaha): Handle ret == -1 and ret > 0, fill problems list

    if (ret == 0) {
        // set the new system state
        auto & system_state = base->p_impl->get_system_state();

#ifdef WITH_MODULEMD
        if (!modules.empty()) {
            // this updates the module system state
            module_db->save();
        }
#endif

        rpm::PackageQuery installed_query(base, rpm::PackageQuery::ExcludeFlags::IGNORE_EXCLUDES);
        installed_query.filter_installed();
        std::set<std::string> inbound_packages_reason_group{};

        // Iterate in reverse, inbound actions are first in the vector, we want to process outbound first
        for (auto it = packages.rbegin(); it != packages.rend(); ++it) {
            auto tspkg = *it;
            const auto & pkg = tspkg.get_package();
            auto tspkg_reason = tspkg.get_reason();
            if (transaction_item_action_is_inbound(tspkg.get_action())) {
                switch (tspkg_reason) {
                    case transaction::TransactionItemReason::DEPENDENCY:
                    case transaction::TransactionItemReason::WEAK_DEPENDENCY:
                    case transaction::TransactionItemReason::USER:
                    case transaction::TransactionItemReason::EXTERNAL_USER:
                        system_state.set_package_reason(pkg.get_na(), tspkg_reason);
                        break;
                    case transaction::TransactionItemReason::GROUP:
                        inbound_packages_reason_group.emplace(pkg.get_name());
                        break;
                    case transaction::TransactionItemReason::NONE:
                    case transaction::TransactionItemReason::CLEAN:
                        break;
                }
                system_state.set_package_from_repo(pkg.get_nevra(), tspkg.get_package().get_repo_id());
            } else if (transaction_item_action_is_outbound(tspkg.get_action())) {
                // Check if the NA is still installed. We do this check for all outbound actions.
                //
                // For REMOVE, if there's a package with the same NA still on the system,
                // it's an installonly package and we're keeping the reason.
                //
                // For REPLACED, we don't know if it was UPGRADE/DOWNGRADE/REINSTALL
                // (we're keeping the reason) or it's an obsolete (we're removing the reason)

                // We need to filter out packages that are being removed in the transaction
                // (the installed query still contains the packages before this transaction)
                installed_query.filter_nevra({pkg.get_nevra()}, libdnf5::sack::QueryCmp::NEQ);

                rpm::PackageQuery query(installed_query);
                query.filter_name({pkg.get_name()});
                query.filter_arch({pkg.get_arch()});
                if (query.empty()) {
                    system_state.remove_package_na_state(pkg.get_na());
                }

                // for a REINSTALL, the remove needs to happen first, hence the reverse iteration of the for loop
                system_state.remove_package_nevra_state(pkg.get_nevra());
            } else if (tspkg.get_action() == TransactionPackage::Action::REASON_CHANGE) {
                if (tspkg_reason == transaction::TransactionItemReason::GROUP) {
                    // group packages are not stored in packages.toml but in groups.toml using its name
                    system_state.set_package_reason(pkg.get_na(), transaction::TransactionItemReason::DEPENDENCY);
                    auto group_id = *tspkg.get_reason_change_group_id();
                    auto state = system_state.get_group_state(group_id);
                    auto pkg_name = pkg.get_name();
                    // add package name to the list of group packages if it's not there yet
                    if (std::find(state.packages.begin(), state.packages.end(), pkg_name) == state.packages.end()) {
                        state.packages.emplace_back(pkg_name);
                        system_state.set_group_state(group_id, state);
                    }
                } else {
                    if (tspkg_reason <= transaction::TransactionItemReason::DEPENDENCY) {
                        // remove package from all group lists in groups.toml
                        auto pkg_name = pkg.get_name();
                        for (const auto & group_id : system_state.get_package_groups(pkg_name)) {
                            try {
                                auto state = system_state.get_group_state(group_id);
                                if (std::erase(state.packages, pkg_name) != 0) {
                                    system_state.set_group_state(group_id, state);
                                }
                            } catch (const system::StateNotFoundError &) {
                                // group state doesn't exist, skip it
                            }
                        }
                    }
                    system_state.set_package_reason(pkg.get_na(), tspkg_reason);
                }
            }
        }

        // Set correct system state for groups in the transaction
        auto comps_xml_dir = system_state.get_group_xml_dir();
        auto comps_xml_dir_groups = comps_xml_dir / "groups";
        auto comps_xml_dir_environments = comps_xml_dir / "environments";
        std::filesystem::create_directories(comps_xml_dir_groups);
        std::filesystem::create_directories(comps_xml_dir_environments);
        for (const auto & tsgroup : groups) {
            auto group = tsgroup.get_group();
            auto group_xml_path = comps_xml_dir_groups / (group.get_groupid() + ".xml");
            if (transaction_item_action_is_inbound(tsgroup.get_action()) ||
                tsgroup.get_action() == transaction::TransactionItemAction::REASON_CHANGE) {
                libdnf5::system::GroupState state;
                state.userinstalled = tsgroup.get_reason() == transaction::TransactionItemReason::USER;
                state.package_types = tsgroup.get_package_types();
                // Remember packages installed by this group
                for (const auto & pkg : group.get_packages()) {
                    auto pkg_name = pkg.get_name();
                    if (inbound_packages_reason_group.find(pkg_name) != inbound_packages_reason_group.end()) {
                        // inbound package with reason GROUP from this transaction
                        state.packages.emplace_back(pkg_name);
                    } else {
                        // also group packages that were installed before this transaction
                        // system state considered as installed by group
                        rpm::PackageQuery query(installed_query);
                        query.filter_name({pkg_name});
                        if (!query.empty()) {
                            state.packages.emplace_back(pkg_name);
                        }
                    }
                }
                system_state.set_group_state(group.get_groupid(), state);
                // save the current xml group definition
                group.serialize(group_xml_path);
            } else {
                // delete system state data for removed groups
                system_state.remove_group_state(group.get_groupid());
                std::error_code ec;
                std::filesystem::remove(group_xml_path, ec);
                if (ec) {
                    logger->warning(
                        "Failed to remove installed group definition file \"{}\": {}",
                        group_xml_path.string(),
                        ec.message());
                }
            }
        }

        // Set correct system state for environmental groups in the transaction
        for (const auto & tsenvironment : environments) {
            auto environment = tsenvironment.get_environment();
            auto environment_xml_path = comps_xml_dir_environments / (environment.get_environmentid() + ".xml");
            if (transaction_item_action_is_inbound(tsenvironment.get_action())) {
                libdnf5::system::EnvironmentState state;
                // Remember groups installed by this environmental group
                for (const auto & grpid : environment.get_groups()) {
                    state.groups.emplace_back(grpid);
                }
                for (const auto & grpid : environment.get_default_groups()) {
                    if (std::find(state.groups.begin(), state.groups.end(), grpid) == state.groups.end()) {
                        state.groups.emplace_back(grpid);
                    }
                }
                system_state.set_environment_state(environment.get_environmentid(), state);
                // save the current xml group definition
                environment.serialize(environment_xml_path);
            } else {
                // delete system state data for removed groups
                system_state.remove_environment_state(environment.get_environmentid());
                std::error_code ec;
                std::filesystem::remove(environment_xml_path, ec);
                if (ec) {
                    logger->warning(
                        "Failed to remove installed environmental group definition file \"{}\": {}",
                        environment_xml_path.string(),
                        ec.message());
                }
            }
        }

        system_state.set_rpmdb_cookie(rpm_transaction.get_db_cookie());

        system_state.save();
    }

    // finish history db transaction
    int64_t dt_end;
    if (source_date_epoch >= 0) {
        dt_end = source_date_epoch;
    } else {
        auto time = std::chrono::system_clock::now().time_since_epoch();
        dt_end = std::chrono::duration_cast<std::chrono::seconds>(time).count();
    }
    db_transaction.set_dt_end(dt_end);
    // TODO(jrohel): Also save the rpm db cookie to system state.
    //               Possibility to detect rpm database change without the need for a history database.
    db_transaction.set_rpmdb_version_end(rpm_transaction.get_db_cookie());
    db_transaction.finish(
        ret == 0 ? libdnf5::transaction::TransactionState::OK : libdnf5::transaction::TransactionState::ERROR);

    plugins.post_transaction(*transaction);

    if (ret == 0) {
        // Remove any temporarily stored packages that are no longer needed
        auto keepcache = config.get_keepcache_option().get_value();

        std::set<std::string> package_files_to_remove;
        if (!keepcache) {
            // All package files for inbound packages should be marked for removal
            for (const auto & tspkg : packages) {
                if (transaction_item_action_is_inbound(tspkg.get_action())) {
                    package_files_to_remove.insert(tspkg.get_package().get_package_path());
                }
            }
        }

        // Get all temp files
        libdnf5::repo::TempFilesMemory temp_files_memory(base, config.get_cachedir_option().get_value());
        const auto & temp_files = temp_files_memory.get_files();

        std::vector<std::string> removed;
        for (const auto & file : temp_files) {
            // If a file was marked for removal, remove it from the filesystem
            if (package_files_to_remove.contains(file)) {
                try {
                    if (!std::filesystem::remove(file)) {
                        logger->debug("Temporary file \"{}\" doesn't exist.", file);
                    }
                } catch (const std::filesystem::filesystem_error & ex) {
                    logger->debug(
                        "An error occurred when trying to remove a temporary file \"{}\": {}", file, ex.what());
                }
            }
            try {
                // All temp files that no longer exist should be removed from the temp_files_memory,
                // whether they were removed just now or by someone else.
                if (!std::filesystem::exists(file)) {
                    removed.push_back(file);
                }
            } catch (const std::filesystem::filesystem_error &) {
                // Silently continue if we can't determine whether a file still exists.
            }
        }
        // Remove all nonexistent files from the temp_files_memory
        temp_files_memory.remove_files(removed);
        // Remove the temp_files_memory file if it's empty
        if (temp_files_memory.get_files().empty()) {
            temp_files_memory.clear();
        }

        return TransactionRunResult::SUCCESS;
    } else {
        for (auto it : rpm_transaction.get_problems()) {
            transaction_problems.emplace_back(it.to_string());
        }
        return TransactionRunResult::ERROR_RPM_RUN;
    }
}

static std::string import_repo_keys_result_to_string(const ImportRepoKeysResult result) {
    switch (result) {
        case ImportRepoKeysResult::OK:
            return {};
        case ImportRepoKeysResult::NO_KEYS:
        case ImportRepoKeysResult::ALREADY_PRESENT:
        case ImportRepoKeysResult::IMPORT_DECLINED:
        case ImportRepoKeysResult::IMPORT_FAILED:
            return TM_(IMPORT_REPO_KEYS_RESULT_DICT.at(result), 1);
    }
    return {};
}

ImportRepoKeysResult Transaction::Impl::import_repo_keys(libdnf5::repo::Repo & repo) {
    auto key_urls = repo.get_config().get_gpgkey_option().get_value();
    if (!key_urls.size()) {
        return ImportRepoKeysResult::NO_KEYS;
    }

    bool all_keys_already_present{true};
    bool some_key_import_failed{false};
    bool some_key_declined{false};
    for (auto const & key_url : key_urls) {
        for (auto & key_info : rpm_signature.parse_key_file(key_url)) {
            if (rpm_signature.key_present(key_info)) {
                signature_problems.push_back(
                    utils::sformat(_("Public key \"{}\" is already present, not importing."), key_url));
                continue;
            }

            all_keys_already_present = false;

            auto & callbacks = repo.get_callbacks();
            if (callbacks && !callbacks->repokey_import(key_info)) {
                some_key_declined = true;
                continue;
            }

            try {
                if (rpm_signature.import_key(key_info)) {
                    if (callbacks) {
                        callbacks->repokey_imported(key_info);
                    }
                    continue;
                }
            } catch (const libdnf5::rpm::KeyImportError & ex) {
                signature_problems.push_back(
                    utils::sformat(_("An error occurred importing key \"{}\": {}"), key_url, ex.what()));
            }

            some_key_import_failed = true;
        }
    }

    if (some_key_import_failed) {
        return ImportRepoKeysResult::IMPORT_FAILED;
    }

    if (some_key_declined) {
        return ImportRepoKeysResult::IMPORT_DECLINED;
    }

    if (all_keys_already_present) {
        return ImportRepoKeysResult::ALREADY_PRESENT;
    }

    return ImportRepoKeysResult::OK;
}

bool Transaction::Impl::check_gpg_signatures() {
    bool result{true};
    // TODO(mblaha): DNSsec key verification
    libdnf5::rpm::RpmSignature rpm_signature(base);
    std::set<std::string> processed_repos{};
    unsigned long num_checks_skipped = 0;
    std::set<std::string> repos_with_skipped_checks;
    for (const auto & trans_pkg : packages) {
        if (transaction_item_action_is_inbound(trans_pkg.get_action())) {
            auto const & pkg = trans_pkg.get_package();
            auto repo = pkg.get_repo();
            auto err_msg = utils::sformat(
                _("OpenPGP check for package \"{}\" ({}) from repo \"{}\" has failed: "),
                pkg.get_nevra(),
                pkg.get_package_path(),
                repo->get_id());
            auto check_result = rpm_signature.check_package_signature(pkg);
            if (check_result == libdnf5::rpm::RpmSignature::CheckResult::SKIPPED) {
                num_checks_skipped += 1;
                repos_with_skipped_checks.insert(pkg.get_repo_id());
            } else if (check_result != libdnf5::rpm::RpmSignature::CheckResult::OK) {
                // these two errors are possibly recoverable by importing the correct public key
                auto is_error_recoverable =
                    check_result == libdnf5::rpm::RpmSignature::CheckResult::FAILED_KEY_MISSING ||
                    check_result == libdnf5::rpm::RpmSignature::CheckResult::FAILED_NOT_TRUSTED;
                if (is_error_recoverable) {
                    // do not try to import keys for the same repo twice
                    auto repo_id = repo->get_id();
                    if (processed_repos.contains(repo_id)) {
                        signature_problems.push_back(
                            err_msg + import_repo_keys_result_to_string(ImportRepoKeysResult::ALREADY_PRESENT));
                        result = false;
                        break;
                    }
                    processed_repos.emplace(repo_id);

                    auto import_result = import_repo_keys(*repo);
                    if (import_result == ImportRepoKeysResult::OK) {
                        auto check_again = rpm_signature.check_package_signature(pkg);
                        if (check_again != libdnf5::rpm::RpmSignature::CheckResult::OK) {
                            signature_problems.push_back(err_msg + _("Import of the key didn't help, wrong key?"));
                            result = false;
                            break;
                        }
                    } else {
                        signature_problems.push_back(err_msg + import_repo_keys_result_to_string(import_result));
                        result = false;
                        break;
                    }
                } else {
                    signature_problems.push_back(err_msg + rpm_signature.check_result_to_string(check_result));
                    result = false;
                    break;
                }
            }
        }
    }
    if (num_checks_skipped > 0) {
        auto repo_string = libdnf5::utils::string::join(
            repos_with_skipped_checks, C_("It is a joining character for repositories IDs", ", "));
        auto warning_msg = utils::sformat(
            (repos_with_skipped_checks.size() == 1)
                ? P_("Warning: skipped OpenPGP checks for {0} package from repository: {1}",
                     "Warning: skipped OpenPGP checks for {0} packages from repository: {1}",
                     num_checks_skipped)
                : P_("Warning: skipped OpenPGP checks for {0} package from repositories: {1}",
                     "Warning: skipped OpenPGP checks for {0} packages from repositories: {1}",
                     num_checks_skipped),
            num_checks_skipped,
            repo_string);
        signature_problems.push_back(warning_msg);
    }
    return result;
}

std::string Transaction::Impl::get_last_script_output() {
    std::lock_guard<std::mutex> lock(last_script_output_mutex);
    return last_script_output;
}

void Transaction::Impl::clear_last_script_output() {
    std::lock_guard<std::mutex> lock(last_script_output_mutex);
    last_script_output.clear();
}

void Transaction::Impl::append_last_script_output(std::string_view output) {
    std::lock_guard<std::mutex> lock(last_script_output_mutex);
    last_script_output.append(output);
}

std::vector<std::string> Transaction::Impl::get_rpm_messages() {
    return rpm_messages;
}

void Transaction::Impl::set_rpm_messages(std::vector<std::string> && rpm_messages) {
    this->rpm_messages = std::move(rpm_messages);
}

std::string Transaction::serialize(
    const std::filesystem::path & packages_path,
    const std::filesystem::path & comps_path,
    const std::string repo_prefix) const {
    transaction::TransactionReplay transaction_replay;

    for (const auto & pkg : get_transaction_packages()) {
        transaction::PackageReplay package_replay;

        const auto & rpm_pkg = pkg.get_package();
        package_replay.nevra = rpm_pkg.get_nevra();
        package_replay.action = pkg.get_action();
        package_replay.reason = pkg.get_reason();
        if (repo_prefix.empty() || rpm_pkg.get_repo_id() == std::string(libdnf5::repo::SYSTEM_REPO_NAME)) {
            package_replay.repo_id = rpm_pkg.get_repo_id();
        } else {
            package_replay.repo_id = repo_prefix + "(" + rpm_pkg.get_repo_id() + ")";
        }
        if (pkg.get_reason_change_group_id()) {
            package_replay.group_id = *pkg.get_reason_change_group_id();
        }
        if (!packages_path.empty()) {
            if (libdnf5::transaction::transaction_item_action_is_inbound(package_replay.action)) {
                package_replay.package_path = packages_path / std::filesystem::path(rpm_pkg.get_location()).filename();
            }
        }
        transaction_replay.packages.push_back(package_replay);
    }

    for (const auto & group : get_transaction_groups()) {
        transaction::GroupReplay group_replay;

        auto xml_group = group.get_group();
        group_replay.group_id = xml_group.get_groupid();
        group_replay.action = group.get_action();
        group_replay.reason = group.get_reason();
        // TODO(amatej): does each group has to have at least one repo?
        std::string first_repo_id = *(group.get_group().get_repos().begin());
        if (repo_prefix.empty() || first_repo_id == std::string(libdnf5::repo::SYSTEM_REPO_NAME)) {
            group_replay.repo_id = first_repo_id;
        } else {
            group_replay.repo_id = repo_prefix + "(" + first_repo_id + ")";
        }
        group_replay.package_types = group.get_package_types();

        if (!comps_path.empty()) {
            group_replay.group_path = build_comps_xml_path(comps_path / "groups", xml_group.get_groupid());
        }

        transaction_replay.groups.push_back(group_replay);
    }

    for (const auto & environment : get_transaction_environments()) {
        transaction::EnvironmentReplay environment_replay;

        auto xml_environment = environment.get_environment();
        environment_replay.environment_id = xml_environment.get_environmentid();
        environment_replay.action = environment.get_action();
        // TODO(amatej): does each environment has to have at least one repo?
        std::string first_repo_id = *(environment.get_environment().get_repos().begin());
        if (repo_prefix.empty() || first_repo_id == std::string(libdnf5::repo::SYSTEM_REPO_NAME)) {
            environment_replay.repo_id = first_repo_id;
        } else {
            environment_replay.repo_id = repo_prefix + "(" + first_repo_id + ")";
        }

        if (!comps_path.empty()) {
            environment_replay.environment_path =
                build_comps_xml_path(comps_path / "environments", xml_environment.get_environmentid());
        }

        transaction_replay.environments.push_back(environment_replay);
    }

    //TODO(amatej): potentially add modules

    return transaction::json_serialize(transaction_replay);
}

void Transaction::store_comps(const std::filesystem::path & comps_path) const {
    auto groups = get_transaction_groups();
    auto envs = get_transaction_environments();
    if (!groups.empty()) {
        auto comps_xml_dir_groups = comps_path / "groups";
        std::filesystem::create_directories(comps_xml_dir_groups);
        for (const auto & group : groups) {
            auto xml_group = group.get_group();
            xml_group.serialize(build_comps_xml_path(comps_xml_dir_groups, xml_group.get_groupid()));
        }
    }
    if (!envs.empty()) {
        auto comps_xml_dir_environments = comps_path / "environments";
        std::filesystem::create_directories(comps_xml_dir_environments);
        for (const auto & environment : envs) {
            auto xml_environment = environment.get_environment();
            xml_environment.serialize(
                build_comps_xml_path(comps_xml_dir_environments, xml_environment.get_environmentid()));
        }
    }
}

bool Transaction::get_download_local_pkgs() const noexcept {
    return p_impl->download_local_pkgs;
}

void Transaction::set_download_local_pkgs(bool value) {
    p_impl->download_local_pkgs = value;
}

std::string Transaction::get_last_script_output() {
    return p_impl->get_last_script_output();
}

void Transaction::clear_last_script_output() {
    p_impl->clear_last_script_output();
}

void Transaction::flush_last_script_output() {
    p_impl->flush_last_script_output();
}

std::vector<std::string> Transaction::get_rpm_messages() {
    return p_impl->get_rpm_messages();
}

void Transaction::set_rpm_messages(std::vector<std::string> && rpm_messages) {
    p_impl->set_rpm_messages(std::move(rpm_messages));
}

const ActiveTransactionInfo * Transaction::get_concurrent_transaction() const noexcept {
    return p_impl->concurrent_transaction.get();
}

}  // namespace libdnf5::base
