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

#include "solv/pool.hpp"

#include "libdnf5/common/exception.hpp"
#include "libdnf5/utils/bgettext/bgettext-mark-domain.h"

extern "C" {
#include <solv/evr.h>
#include <solv/testcase.h>
}

namespace {


void allow_uninstall_all_but_protected(
    Pool * pool,
    libdnf5::solv::IdQueue & job,
    const libdnf5::solv::SolvMap * protected_packages,
    libdnf5::rpm::PackageId protected_kernel) {
    libdnf5::solv::SolvMap not_protected_pkgs(pool->nsolvables);
    not_protected_pkgs.set_all();
    if (protected_packages) {
        not_protected_pkgs -= *protected_packages;
    }
    if (protected_kernel.id > 0) {
        not_protected_pkgs.remove_unsafe(protected_kernel.id);
    }
    if (pool->considered) {
        not_protected_pkgs &= *pool->considered;
    }

    for (Id id = 1; id < pool->nsolvables; ++id) {
        Solvable * s = pool_id2solvable(pool, id);
        if (pool->installed == s->repo && not_protected_pkgs.contains_unsafe(id)) {
            job.push_back(SOLVER_ALLOWUNINSTALL | SOLVER_SOLVABLE, id);
        }
    }
}

void construct_job(
    Pool * pool,
    libdnf5::solv::IdQueue & job,
    const libdnf5::solv::IdQueue & install_only,
    bool allow_erasing,
    const libdnf5::solv::SolvMap * protected_packages,
    libdnf5::rpm::PackageId protected_kernel,
    const libdnf5::solv::IdQueue * user_installed_packages,
    const libdnf5::solv::SolvMap * exclude_from_weak) {
    // turn off implicit obsoletes for installonly packages
    for (int i = 0; i < install_only.size(); ++i) {
        job.push_back(SOLVER_MULTIVERSION | SOLVER_SOLVABLE_PROVIDES, install_only[i]);
    }

    // Mark to solver to not use them to satisfy weak dependencies
    if (exclude_from_weak) {
        for (auto id : *exclude_from_weak) {
            job.push_back(SOLVER_SOLVABLE | SOLVER_EXCLUDEFROMWEAK, id);
        }
    }

    // Automatically mark all protected packages as user installed.
    // When a protected package is installed as a dependency it can block
    // removal of the last package that depends on it (because the protected
    // package cannot be removed, not even as an unused dependency).
    // To prevent this and still correctly resolve dependencies of the protected
    // packages mark them all as user installed.
    if (protected_packages != nullptr) {
        for (auto id : *protected_packages) {
            job.push_back(SOLVER_SOLVABLE | SOLVER_USERINSTALLED, id);
        }
    }

    if (allow_erasing) {
        allow_uninstall_all_but_protected(pool, job, protected_packages, protected_kernel);
    }

    // mark user-installed packages
    if (user_installed_packages != nullptr) {
        for (auto id : *user_installed_packages) {
            job.push_back(SOLVER_SOLVABLE | SOLVER_USERINSTALLED, id);
        }
    }

    //     if (flags & DNF_VERIFY)
    //         job->pushBack(SOLVER_VERIFY|SOLVER_SOLVABLE_ALL, 0);
}

void init_solver(libdnf5::solv::Pool & pool, libdnf5::solv::Solver & solver) {
    solver.init(pool);

    /* don't remove packages that are no longer in repo during distupgrade */
    solver.set_flag(SOLVER_FLAG_KEEP_ORPHANS, 1);
    /* no arch change for forcebest */
    solver.set_flag(SOLVER_FLAG_BEST_OBEY_POLICY, 1);
    /* support package splits via obsoletes */
    solver.set_flag(SOLVER_FLAG_YUM_OBSOLETES, 1);

#if defined(LIBSOLV_FLAG_URPMREORDER)
    /* support urpm-like solution reordering */
    solver.set_flag(SOLVER_FLAG_URPM_REORDER, 1);
#endif
}


/// @brief return false when does not depend on anything from b
bool can_depend_on(Pool * pool, Solvable * sa, Id b) {
    libdnf5::solv::IdQueue dep_requires;

    solvable_lookup_idarray(sa, SOLVABLE_REQUIRES, &dep_requires.get_queue());
    for (int i = 0; i < dep_requires.size(); ++i) {
        Id req_dep = dep_requires[i];
        Id p, pp;

        FOR_PROVIDES(p, pp, req_dep)
        if (p == b)
            return true;
    }

    return false;
}


/// @brief It takes an `in` Queue which contains packages sorted by name (see `installonly_cmp`).
///        It selects the first name and moves all packages with that name to the `out` Queue.
///        Additionally libsolv Id of the selected name is returned.
static Id same_name_subqueue(libdnf5::solv::RpmPool & pool, Queue * in, Queue * out) {
    Id el = queue_pop(in);
    Id name = pool.id2solvable(el)->name;
    queue_empty(out);
    queue_push(out, el);
    while (in->count && pool.id2solvable(in->elements[in->count - 1])->name == name)
        // reverses the order so packages are sorted by descending version
        queue_push(out, queue_pop(in));
    return name;
}


struct InstallonlyCmpData {
    libdnf5::solv::RpmPool & pool;
    Id running_kernel;
};

struct ObsoleteCmpData {
    libdnf5::solv::RpmPool & pool;
    Id obsolete;
};

int installonly_cmp(const Id * ap, const Id * bp, const InstallonlyCmpData * s_cb) {
    Id a = *ap;
    Id b = *bp;
    auto & pool = s_cb->pool;
    Id kernel = s_cb->running_kernel;
    Solvable * sa = pool.id2solvable(a);
    Solvable * sb = pool.id2solvable(b);

    // if the names are different sort them differently, particular order does not matter as long as it's consistent.
    int name_diff = sa->name - sb->name;
    if (name_diff)
        return name_diff;

    // same name, if one is/depends on the running kernel put it last

    // move available packages to end of the list
    if (pool->installed != sa->repo)
        return 1;

    if (pool->installed != sb->repo)
        return -1;

    if (kernel >= 0) {
        if (a == kernel || can_depend_on(*pool, sa, kernel))
            return 1;
        if (b == kernel || can_depend_on(*pool, sb, kernel))
            return -1;
        // if package has same evr as kernel try them to keep (kernel-devel packages)
        Solvable * kernelSolvable = pool.id2solvable(kernel);
        if (sa->evr == kernelSolvable->evr) {
            return 1;
        }
        if (sb->evr == kernelSolvable->evr) {
            return -1;
        }
    }
    return pool.evrcmp(sa->evr, sb->evr, EVRCMP_COMPARE);
}

int obsq_cmp(const Id * ap, const Id * bp, const ObsoleteCmpData * s_cb) {
    auto & pool = s_cb->pool;

    if (*ap == *bp) {
        return 0;
    }

    Solvable * to_compare_solvable = pool.id2solvable(s_cb->obsolete);
    Solvable * ap_solvable = pool.id2solvable(*ap);
    Solvable * obs = pool.id2solvable(*bp);
    if (ap_solvable->name != obs->name) {
        // bring "same name" obsoleters (i.e. upgraders) to front
        if (ap_solvable->name == to_compare_solvable->name) {
            return -1;
        }
        if (obs->name == to_compare_solvable->name) {
            return 1;
        }
        return strcmp(pool.id2str(ap_solvable->name), pool.id2str(obs->name));
    }
    int r = pool.evrcmp(ap_solvable->evr, obs->evr, EVRCMP_COMPARE);
    if (r) {
        return -r; /* highest version first */
    }
    if (ap_solvable->arch != obs->arch) {
        /* bring same arch to front */
        if (ap_solvable->arch == to_compare_solvable->arch) {
            return -1;
        }
        if (obs->arch == to_compare_solvable->arch) {
            return 1;
        }
    }
    return *ap - *bp;
}

inline bool name_solvable_cmp_key(const Solvable * first, const Solvable * second) {
    return first->name < second->name;
}

inline bool name_solvable_id_name_cmp_key(const Solvable * first, const Id second) {
    return first->name < second;
}

}  // namespace


namespace libdnf5::rpm::solv {


bool GoalPrivate::limit_installonly_packages(libdnf5::solv::IdQueue & job, Id running_kernel) {
    if (installonly_limit == 0) {
        return 0;
    }

    auto & spool = get_rpm_pool();
    ::Pool * pool = *spool;
    bool reresolve = false;

    // Use maps to handle each package just once
    libdnf5::solv::SolvMap marked_for_install_providers_map(spool.get_nsolvables());
    libdnf5::solv::SolvMap available_unused_providers_map(spool.get_nsolvables());

    for (Id install_only_provide : installonly) {
        Id p;
        Id pp;

        // Add all providers of installonly provides that are marked for install
        // to `marked_for_install_providers_map` SolvMap those that are not marked
        // for install and are not already installed are added to available_unused_providers_map.
        FOR_PROVIDES(p, pp, install_only_provide) {
            // TODO(jmracek)  Replace the test by cached data from sack.p_impl->get_solvables()
            if (!spool.is_package(p)) {
                continue;
            }
            // According to libsolv-bindings the decision level is positive for installs
            // and negative for conflicts (conflicts with another package or dependency
            // conflicts = dependencies cannot be met).
            if (libsolv_solver.get_decisionlevel(p) > 0) {
                marked_for_install_providers_map.add_unsafe(p);
            } else {
                available_unused_providers_map.add_unsafe(p);
            }
        }
    }

    libdnf5::solv::IdQueue marked_for_install_providers;
    for (const auto & pkg_id : marked_for_install_providers_map) {
        marked_for_install_providers.push_back(pkg_id);
    }

    std::vector<Solvable *> available_unused_providers;
    for (const auto & pkg_id : available_unused_providers_map) {
        Solvable * solvable = spool.id2solvable(pkg_id);
        if (!spool.is_installed(solvable)) {
            available_unused_providers.push_back(solvable);
        }
    }

    const InstallonlyCmpData installonly_cmp_data{spool, running_kernel};
    marked_for_install_providers.sort(&installonly_cmp, &installonly_cmp_data);
    std::sort(available_unused_providers.begin(), available_unused_providers.end(), name_solvable_cmp_key);

    libdnf5::solv::IdQueue same_names;
    // For each set of `marked_for_install_providers` with the same name ensure at most `installonly_limit`
    // are installed. Also for each name ensure `available_unused_providers` with that name are not installed.
    while (!marked_for_install_providers.empty()) {
        Id name = same_name_subqueue(spool, &marked_for_install_providers.get_queue(), &same_names.get_queue());
        if (same_names.size() <= static_cast<int>(installonly_limit)) {
            continue;
        }

        // We want to avoid reinstalling packages marked for ERASE, therefore
        // if some unused provider is also available we need to mark it ERASE as well.
        auto low = std::lower_bound(
            available_unused_providers.begin(), available_unused_providers.end(), name, name_solvable_id_name_cmp_key);
        while (low != available_unused_providers.end() && (*low)->name == name) {
            job.push_back(SOLVER_ERASE | SOLVER_SOLVABLE, spool.solvable2id(*low));
            ++low;
        }

        reresolve = true;
        for (int j = 0; j < same_names.size(); ++j) {
            Id id = same_names[j];
            Id action = SOLVER_ERASE;
            if (j < static_cast<int>(installonly_limit)) {
                action = SOLVER_INSTALL;
            }

            job.push_back(action | SOLVER_SOLVABLE, id);
        }
    }
    return reresolve;
}


libdnf5::solv::IdQueue GoalPrivate::list_results(Id type_filter1, Id type_filter2) {
    /* no transaction */
    if (!libsolv_transaction) {
        libdnf_assert_goal_resolved();

        // TODO(jmracek) else if (removalOfProtected && removalOfProtected->size()) {
        //    throw Goal::Error(_("no solution, cannot remove protected package"),
        //                                  DNF_ERROR_REMOVAL_OF_PROTECTED_PKG);
        //}

        // TODO(lukash) replace with a proper and descriptive exception
        throw RuntimeError(M_("no solution possible"));
    }

    libdnf5::solv::IdQueue result_ids;
    const int common_mode = SOLVER_TRANSACTION_SHOW_OBSOLETES | SOLVER_TRANSACTION_CHANGE_IS_REINSTALL;

    for (int i = 0; i < libsolv_transaction->steps.count; ++i) {
        Id p = libsolv_transaction->steps.elements[i];
        Id type;

        switch (type_filter1) {
            case SOLVER_TRANSACTION_OBSOLETED:
                type = transaction_type(libsolv_transaction, p, common_mode);
                break;
            default:
                type = transaction_type(
                    libsolv_transaction, p, common_mode | SOLVER_TRANSACTION_SHOW_ACTIVE | SOLVER_TRANSACTION_SHOW_ALL);
                break;
        }

        if (type == type_filter1 || (type_filter2 && type == type_filter2)) {
            result_ids.push_back(p);
        }
    }
    return result_ids;
}


libdnf5::GoalProblem GoalPrivate::resolve() {
    auto & pool = get_rpm_pool();
    libdnf5::solv::IdQueue job(staging);
    construct_job(
        *pool,
        job,
        installonly,
        allow_erasing,
        protected_packages.get(),
        protected_running_kernel,
        user_installed_packages.get(),
        exclude_from_weak.get());

    /* apply the excludes */
    //dnf_sack_recompute_considered(sack);

    // TODO make_provides_ready remove temporary Ids for One_OF => what about to lock it?
    //dnf_sack_make_provides_ready(sack);
    if (libsolv_transaction) {
        transaction_free(libsolv_transaction);
        libsolv_transaction = NULL;
    }

    init_solver(pool, libsolv_solver);

    // Remove SOLVER_WEAK and add SOLVER_BEST to all transactions to allow report skipped packages and best candidates
    // with broken dependencies
    if (run_in_strict_mode) {
        for (int i = 0; i < job.size(); i += 2) {
            job[i] &= ~SOLVER_WEAK;
            job[i] |= SOLVER_FORCEBEST;
        }
    }

    int ignore_weak_deps = install_weak_deps ? 0 : 1;
    libsolv_solver.set_flag(SOLVER_FLAG_IGNORE_RECOMMENDED, ignore_weak_deps);

    int downgrade = allow_downgrade ? 1 : 0;
    libsolv_solver.set_flag(SOLVER_FLAG_ALLOW_DOWNGRADE, downgrade);

    // Set up vendor locking modes
    int vendor_change = allow_vendor_change ? 1 : 0;
    libsolv_solver.set_flag(SOLVER_FLAG_ALLOW_VENDORCHANGE, vendor_change);
    libsolv_solver.set_flag(SOLVER_FLAG_DUP_ALLOW_VENDORCHANGE, vendor_change);

#if defined(LIBSOLV_FLAG_FOCUSNEW)
    // Ensure the solver tries to install the latest versions of dependencies, even if it results in a bigger transaction
    // Available since libsolv-0.7.30
    libsolv_solver.set_flag(SOLVER_FLAG_FOCUS_NEW, 1);
#endif

    if (libsolv_solver.solve(job)) {
        return libdnf5::GoalProblem::SOLVER_ERROR;
    }

    // either allow solutions callback or installonlies, both at the same time are not supported
    if (limit_installonly_packages(job, protected_running_kernel.id)) {
        // allow removing non-installonly packages that depend on a kernel about to be removed
        allow_uninstall_all_but_protected(*pool, job, protected_packages.get(), protected_running_kernel);
        if (libsolv_solver.solve(job)) {
            return libdnf5::GoalProblem::SOLVER_ERROR;
        }
    }

    libsolv_transaction = libsolv_solver.create_transaction();

    return protected_in_removals();
}

libdnf5::solv::IdQueue GoalPrivate::list_installs() {
    return list_results(SOLVER_TRANSACTION_INSTALL, SOLVER_TRANSACTION_OBSOLETES);
}

libdnf5::solv::IdQueue GoalPrivate::list_reinstalls() {
    return list_results(SOLVER_TRANSACTION_REINSTALL, 0);
}

libdnf5::solv::IdQueue GoalPrivate::list_upgrades() {
    return list_results(SOLVER_TRANSACTION_UPGRADE, 0);
}

libdnf5::solv::IdQueue GoalPrivate::list_downgrades() {
    return list_results(SOLVER_TRANSACTION_DOWNGRADE, 0);
}

libdnf5::solv::IdQueue GoalPrivate::list_removes() {
    return list_results(SOLVER_TRANSACTION_ERASE, 0);
}

libdnf5::solv::IdQueue GoalPrivate::list_obsoleted() {
    return list_results(SOLVER_TRANSACTION_OBSOLETED, 0);
}

void GoalPrivate::write_debugdata(const std::filesystem::path & abs_dest_dir) {
    libsolv_solver.write_debugdata(abs_dest_dir);
}

// PackageSet
// Goal::listUnneeded()
// {
//     PackageSet pset(p_impl->sack);
//     libdnf5::solv::IdQueue queue;
//     Solver *solv = p_impl->solv;
//
//     solver_get_unneeded(solv, queue.getQueue(), 0);
//     queue2pset(queue, &pset);
//     return pset;
// }
//
// PackageSet
// Goal::listSuggested()
// {
//     PackageSet pset(p_impl->sack);
//     libdnf5::solv::IdQueue queue;
//     Solver *solv = p_impl->solv;
//
//     solver_get_recommendations(solv, NULL, queue.getQueue(), 0);
//     queue2pset(queue, &pset);
//     return pset;
// }

std::vector<std::vector<std::tuple<ProblemRules, Id, Id, Id, std::string>>> GoalPrivate::get_problems() {
    auto & pool = get_rpm_pool();
    auto * installed_repo = pool->installed;

    libdnf_assert_goal_resolved();

    auto count_problems = static_cast<int>(libsolv_solver.problem_count());
    if (count_problems == 0) {
        return {};
    }
    // std::tuple<ProblemRules, Id source, Id dep, Id target, std::string>>
    std::vector<std::vector<std::tuple<ProblemRules, Id, Id, Id, std::string>>> problems;

    // libsolv counts problem from 1
    for (int i = 1; i <= count_problems; ++i) {
        auto problem_queue = libsolv_solver.findallproblemrules(i);
        std::vector<std::tuple<ProblemRules, Id, Id, Id, std::string>> problem;
        for (int j = 0; j < problem_queue.size(); ++j) {
            Id rid = problem_queue[j];
            auto descriptions_queue = libsolv_solver.allruleinfos(rid);
            if (!descriptions_queue.empty()) {
                for (int ir = 0; ir < descriptions_queue.size(); ir += 4) {
                    SolverRuleinfo type = static_cast<SolverRuleinfo>(descriptions_queue[ir]);
                    Id source = descriptions_queue[ir + 1];
                    Id target = descriptions_queue[ir + 2];
                    Id dep = descriptions_queue[ir + 3];
                    ProblemRules rule;
                    const char * solv_string = nullptr;
                    switch (type) {
                        case SOLVER_RULE_DISTUPGRADE:
                            rule = ProblemRules::RULE_DISTUPGRADE;
                            break;
                        case SOLVER_RULE_INFARCH:
                            rule = ProblemRules::RULE_INFARCH;
                            break;
                        case SOLVER_RULE_UPDATE:
                            rule = ProblemRules::RULE_UPDATE;
                            break;
                        case SOLVER_RULE_JOB:
                            rule = ProblemRules::RULE_JOB;
                            break;
                        case SOLVER_RULE_JOB_UNSUPPORTED:
                            rule = ProblemRules::RULE_JOB_UNSUPPORTED;
                            break;
                        case SOLVER_RULE_JOB_NOTHING_PROVIDES_DEP:
                            rule = ProblemRules::RULE_JOB_NOTHING_PROVIDES_DEP;
                            break;
                        case SOLVER_RULE_JOB_UNKNOWN_PACKAGE:
                            rule = ProblemRules::RULE_JOB_UNKNOWN_PACKAGE;
                            break;
                        case SOLVER_RULE_JOB_PROVIDED_BY_SYSTEM:
                            rule = ProblemRules::RULE_JOB_PROVIDED_BY_SYSTEM;
                            break;
                        case SOLVER_RULE_PKG:
                            rule = ProblemRules::RULE_PKG;
                            break;
                        case SOLVER_RULE_BEST:
                            if (source > 0) {
                                rule = ProblemRules::RULE_BEST_1;
                                break;
                            }
                            rule = ProblemRules::RULE_BEST_2;
                            break;
                        case SOLVER_RULE_PKG_NOT_INSTALLABLE: {
                            Solvable * solvable = pool.id2solvable(source);
                            if (pool_disabled_solvable(*pool, solvable)) {
                                // TODO(jmracek) RULE_PKG_NOT_INSTALLABLE_4 not handled
                                rule = ProblemRules::RULE_PKG_NOT_INSTALLABLE_1;
                                break;
                            }
                            if (solvable->arch && solvable->arch != ARCH_SRC && solvable->arch != ARCH_NOSRC &&
                                pool->id2arch && (solvable->arch > pool->lastarch || !pool->id2arch[solvable->arch])) {
                                rule = ProblemRules::RULE_PKG_NOT_INSTALLABLE_2;
                                break;
                            }
                            rule = ProblemRules::RULE_PKG_NOT_INSTALLABLE_3;
                        } break;
                        case SOLVER_RULE_PKG_NOTHING_PROVIDES_DEP:
                            rule = ProblemRules::RULE_PKG_NOTHING_PROVIDES_DEP;
                            break;
                        case SOLVER_RULE_PKG_SAME_NAME:
                            rule = ProblemRules::RULE_PKG_SAME_NAME;
                            break;
                        case SOLVER_RULE_PKG_CONFLICTS: {
                            Solvable * src_solvable = pool.id2solvable(source);
                            Solvable * tgt_solvable = pool.id2solvable(target);
                            if (src_solvable->repo == installed_repo || tgt_solvable->repo == installed_repo) {
                                rule = ProblemRules::RULE_PKG_INSTALLED_CONFLICTS;
                            } else {
                                rule = ProblemRules::RULE_PKG_CONFLICTS;
                            }
                        } break;
                        case SOLVER_RULE_PKG_OBSOLETES:
                            rule = ProblemRules::RULE_PKG_OBSOLETES;
                            break;
                        case SOLVER_RULE_PKG_INSTALLED_OBSOLETES:
                            rule = ProblemRules::RULE_PKG_INSTALLED_OBSOLETES;
                            break;
                        case SOLVER_RULE_PKG_IMPLICIT_OBSOLETES:
                            rule = ProblemRules::RULE_PKG_IMPLICIT_OBSOLETES;
                            break;
                        case SOLVER_RULE_PKG_REQUIRES: {
                            Solvable * solvable = pool.id2solvable(source);
                            if (solvable->repo == installed_repo) {
                                rule = ProblemRules::RULE_PKG_INSTALLED_REQUIRES;
                            } else {
                                rule = ProblemRules::RULE_PKG_REQUIRES;
                            }
                        } break;
                        case SOLVER_RULE_PKG_SELF_CONFLICT:
                            rule = ProblemRules::RULE_PKG_SELF_CONFLICT;
                            break;
                        case SOLVER_RULE_YUMOBS:
                            rule = ProblemRules::RULE_YUMOBS;
                            break;
                        default:
                            rule = ProblemRules::RULE_UNKNOWN;
                            solv_string = libsolv_solver.problemruleinfo2str(type, source, target, dep);
                            break;
                    }
                    problem.emplace_back(std::make_tuple(
                        rule, source, dep, target, solv_string ? std::string(solv_string) : std::string()));
                }
            }
        }
        problems.push_back(std::move(problem));
    }
    return problems;
}

libdnf5::GoalProblem GoalPrivate::protected_in_removals() {
    libdnf5::GoalProblem ret = libdnf5::GoalProblem::NO_PROBLEM;
    if ((!protected_packages || protected_packages->empty()) && protected_running_kernel.id <= 0) {
        removal_of_protected.reset();
        return ret;
    }
    auto removes = list_removes();
    auto obsoleted = list_obsoleted();
    if (removes.empty() && obsoleted.empty()) {
        removal_of_protected.reset();
        return ret;
    }

    auto & pool = get_rpm_pool();

    libdnf5::solv::SolvMap pkg_remove_list(pool->nsolvables);
    for (const auto & remove : removes) {
        pkg_remove_list.add_unsafe(remove);
    }
    // We want to allow obsoletion of protected packages, so we do not consider
    // obsoletes here, only removes. Previously, obsoletion of protected
    // packages was disallowed, but there needed to be some mechanism for
    // obsoleting/swapping a protected package, such as to obsolete `dnf` in
    // favor of `dnf5`. Obsoleting a package is much harder to do accidentally
    // than removing it.

    libdnf5::solv::SolvMap protected_pkgs(pool->nsolvables);
    if (protected_packages) {
        protected_pkgs |= *protected_packages;
    }
    if (protected_running_kernel.id > 0) {
        protected_pkgs.add_unsafe(protected_running_kernel.id);
        // Special case: consider the obsoletion of the running kernel as a
        // removal. Obsoletion of other protected packages should be allowed.
        for (const auto & obsolete : obsoleted) {
            if (obsolete == protected_running_kernel.id) {
                pkg_remove_list.add_unsafe(obsolete);
            }
        }
    }

    removal_of_protected.reset(new libdnf5::solv::SolvMap(std::move(pkg_remove_list)));
    for (auto pkg_id : *removal_of_protected) {
        if (protected_pkgs.contains(pkg_id)) {
            ret = libdnf5::GoalProblem::SOLVER_ERROR;
        } else {
            removal_of_protected->remove_unsafe(pkg_id);
        }
    }
    return ret;
}

void GoalPrivate::add_protected_packages(const libdnf5::solv::SolvMap & map) {
    if (!protected_packages) {
        protected_packages.reset(new libdnf5::solv::SolvMap(map));
    } else {
        *protected_packages |= map;
    }
}

void GoalPrivate::set_protected_packages(const libdnf5::solv::SolvMap & map) {
    protected_packages.reset(new libdnf5::solv::SolvMap(map));
}

void GoalPrivate::reset_protected_packages() {
    protected_packages.reset();
}

void GoalPrivate::set_user_installed_packages(const libdnf5::solv::IdQueue & queue) {
    user_installed_packages.reset(new libdnf5::solv::IdQueue(queue));
}

void GoalPrivate::add_transaction_user_installed(const libdnf5::solv::IdQueue & idqueue) {
    if (!transaction_user_installed) {
        auto & pool = get_rpm_pool();
        transaction_user_installed.reset(new libdnf5::solv::SolvMap(pool->nsolvables));
    }
    for (const auto & id : idqueue) {
        transaction_user_installed->add(id);
    }
}

void GoalPrivate::add_transaction_group_reason(const libdnf5::solv::IdQueue & idqueue) {
    if (!transaction_group_reason) {
        auto & pool = get_rpm_pool();
        transaction_group_reason.reset(new libdnf5::solv::SolvMap(pool->nsolvables));
    }
    for (const auto & id : idqueue) {
        transaction_group_reason->add(id);
    }
}

void GoalPrivate::add_transaction_group_reason(const libdnf5::solv::SolvMap & solvmap) {
    if (!transaction_group_reason) {
        transaction_group_reason.reset(new libdnf5::solv::SolvMap(solvmap));
    } else {
        *transaction_group_reason |= solvmap;
    }
}

void GoalPrivate::add_exclude_from_weak(const libdnf5::solv::SolvMap & solvmap) {
    if (!exclude_from_weak) {
        exclude_from_weak.reset(new libdnf5::solv::SolvMap(solvmap));
    } else {
        *exclude_from_weak |= solvmap;
    }
}

transaction::TransactionItemReason GoalPrivate::get_reason(Id id) {
    //solver_get_recommendations
    libdnf_assert_goal_resolved();

    Id info;
    int reason = libsolv_solver.describe_decision(id, &info);

    if ((reason == SOLVER_REASON_UNIT_RULE || reason == SOLVER_REASON_RESOLVE_JOB) &&
        (libsolv_solver.ruleclass(info) == SOLVER_RULE_JOB || libsolv_solver.ruleclass(info) == SOLVER_RULE_BEST)) {
        // explicitly user-installed
        if (transaction_user_installed && transaction_user_installed->contains(id)) {
            return transaction::TransactionItemReason::USER;
        }
        // explicitly group-installed
        if (transaction_group_reason && transaction_group_reason->contains(id)) {
            return transaction::TransactionItemReason::GROUP;
        }
        // for some packages (e.g. installed by provide) we cannot decide the reason, resort to USER
        return transaction::TransactionItemReason::USER;
    }
    if (reason == SOLVER_REASON_CLEANDEPS_ERASE)
        return transaction::TransactionItemReason::CLEAN;
    if (reason == SOLVER_REASON_WEAKDEP)
        return transaction::TransactionItemReason::WEAK_DEPENDENCY;
    auto cleanDepsQueue = libsolv_solver.get_cleandeps();
    for (int i = 0; i < cleanDepsQueue.size(); ++i) {
        if (cleanDepsQueue[i] == id) {
            return transaction::TransactionItemReason::CLEAN;
        }
    }
    return transaction::TransactionItemReason::DEPENDENCY;
}

libdnf5::solv::IdQueue GoalPrivate::list_obsoleted_by_package(Id id) {
    if (!libsolv_transaction) {
        throw RuntimeError(M_("no solution possible"));
    }
    libdnf5::solv::IdQueue obsoletes;
    transaction_all_obs_pkgs(libsolv_transaction, id, &obsoletes.get_queue());
    const ObsoleteCmpData obsoete_cmp_data{get_rpm_pool(), id};
    obsoletes.sort(&obsq_cmp, &obsoete_cmp_data);
    return obsoletes;
}

}  // namespace libdnf5::rpm::solv
