// 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 "libdnf5/module/module_sack.hpp"

#include "base/solver_problems_internal.hpp"
#include "module/module_goal_private.hpp"
#include "module/module_metadata.hpp"
#include "module/module_sack_impl.hpp"
#include "solv/solv_map.hpp"

#include "libdnf5/base/base.hpp"
#include "libdnf5/base/base_weak.hpp"
#include "libdnf5/common/exception.hpp"
#include "libdnf5/module/module_errors.hpp"
#include "libdnf5/module/module_item.hpp"
#include "libdnf5/module/module_query.hpp"
#include "libdnf5/module/module_sack_weak.hpp"
#include "libdnf5/module/nsvcap.hpp"
#include "libdnf5/utils/bgettext/bgettext-mark-domain.h"
#include "libdnf5/utils/fs/file.hpp"

#include <modulemd-2.0/modulemd.h>

extern "C" {
#include <solv/pool.h>
#include <solv/repo.h>
}

#include "../rpm/package_sack_impl.hpp"

#include "libdnf5/repo/repo_weak.hpp"
#include "libdnf5/rpm/package_query.hpp"

#include <memory>
#include <string>

namespace libdnf5::module {


static const std::string EMPTY_RESULT;


ModuleSack::ModuleSack(const BaseWeakPtr & base) : p_impl(new Impl(*this, base)) {}
ModuleSack::~ModuleSack() {}


const std::vector<std::unique_ptr<ModuleItem>> & ModuleSack::get_modules() {
    return p_impl->get_modules();
}


std::vector<ModuleItem *> ModuleSack::get_active_modules() {
    std::vector<ModuleItem *> modules;
    if (p_impl->get_modules().empty()) {
        return modules;
    }
    if (!active_modules_resolved) {
        resolve_active_module_items();
    }
    for (auto id_module_pair : p_impl->active_modules) {
        modules.push_back(id_module_pair.second);
    }
    return modules;
}


void ModuleSack::add(const std::string & file_content, const std::string & repo_id) {
    ModuleMetadata md(get_base());
    try {
        md.add_metadata_from_string(file_content, 0);
        // The second call to add_metadata_from_string is to load all metadata in to `p_impl->module_metadata` and use
        // them later to get all defaults.
        p_impl->module_metadata.add_metadata_from_string(file_content, 0);
    } catch (const ModuleResolveError & e) {
        throw ModuleResolveError(
            M_("Failed to load module metadata for repository \"{}\": {}"), repo_id, std::string(e.what()));
    }

    Repo * repo;
    auto repo_pair = p_impl->repositories.find(repo_id);
    if (repo_pair == p_impl->repositories.end()) {
        repo = repo_create(p_impl->pool, repo_id.c_str());
        p_impl->repositories[repo_id] = int(repo->repoid);
    } else {
        repo = pool_id2repo(p_impl->pool, Id(repo_pair->second));
    }
    auto items = md.get_all_module_items(get_weak_ptr(), repo_id);
    // Store module items with static context
    for (auto const & module_item_ptr : items.first) {
        std::unique_ptr<ModuleItem> module_item(module_item_ptr);
        module_item->create_solvable_and_dependencies();
        p_impl->modules.push_back(std::move(module_item));
    }
    // Store module items without static context
    for (auto const & module_item_ptr : items.second) {
        std::unique_ptr<ModuleItem> module_item(module_item_ptr);
        p_impl->modules_without_static_context.push_back(std::move(module_item));
    }
}


void ModuleSack::Impl::add_modules_without_static_context() {
    if (modules_without_static_context.empty()) {
        return;
    }

    // Create a map based on modules with static context. For each "name:stream", map requires_strings to ModuleItems.
    std::map<std::string, std::map<std::string, std::vector<ModuleItem *>>> static_context_map;
    for (auto const & module_item : modules) {
        auto requires_string = module_item->get_module_dependencies_string();
        static_context_map[module_item->get_name_stream()][requires_string].push_back(module_item.get());
    }

    // For each module with dynamic context, check whether its requires_string matches requires_string of any
    // static-context module with the same "name:stream" (i.e. if it's in the static_context_map). If so, assign it
    // the same static context.
    for (auto & module_item : modules_without_static_context) {
        auto requires_string = module_item->get_module_dependencies_string();

        auto stream_iterator = static_context_map.find(module_item->get_name_stream());
        if (stream_iterator != static_context_map.end()) {
            auto context_iterator = stream_iterator->second.find(requires_string);
            if (context_iterator != stream_iterator->second.end()) {
                module_item->set_computed_static_context(context_iterator->second[0]->get_context());
                module_item->create_solvable_and_dependencies();
                modules.push_back(std::move(module_item));
                continue;
            }
        }
        // If the requires_string didn't match anything in the static_context_map, set the new static context to
        // the requires_string (or "NoRequires" if empty). This means all dynamic-context modules with the same
        // "name:stream" and the same dependencies will have the same static context.
        if (requires_string.empty()) {
            requires_string.append("NoRequires");
        }
        module_item->set_computed_static_context(requires_string);
        module_item->create_solvable_and_dependencies();
        modules.push_back(std::move(module_item));
    }
    modules_without_static_context.clear();
}


ModuleSackWeakPtr ModuleSack::get_weak_ptr() {
    return ModuleSackWeakPtr(this, &data_guard);
}


BaseWeakPtr ModuleSack::get_base() const {
    return p_impl->base;
}


const std::string & ModuleSack::get_default_stream(const std::string & name) const {
    p_impl->module_defaults = p_impl->module_metadata.get_default_streams();
    auto it = p_impl->module_defaults.find(name);
    if (it == p_impl->module_defaults.end()) {
        return EMPTY_RESULT;
    }
    return it->second;
}


std::vector<std::string> ModuleSack::get_default_profiles(std::string module_name, std::string module_stream) {
    return p_impl->module_metadata.get_default_profiles(module_name, module_stream);
}


std::tuple<
    std::vector<std::string>,
    std::vector<std::string>,
    std::vector<std::string>,
    std::vector<std::string>,
    rpm::ReldepList>
ModuleSack::Impl::collect_data_for_modular_filtering() {
    // TODO(jmracek) Add support of demodularized RPMs
    // auto demodularizedNames = getDemodularizedRpms(modulePackageContainer, allPackages);

    std::vector<std::string> include_NEVRAs;
    std::vector<std::string> exclude_NEVRAs;
    std::vector<std::string> names;
    std::vector<std::string> src_names;
    libdnf5::rpm::ReldepList reldep_name_list(base);
    for (const auto & module : get_modules()) {
        auto artifacts = module->get_artifacts();
        if (module->is_active()) {
            // TODO(jmracek) Add support of demodularized RPMs
            // std::string package_ID{module->get_name_stream()};
            // package_ID.append(".");
            // package_ID.append(module->get_arch());
            //auto it = demodularizedNames.find(package_ID);
            //if (it == demodularizedNames.end()) {
            if (true) {
                for (const auto & rpm : artifacts) {
                    auto nevras = rpm::Nevra::parse(rpm, {rpm::Nevra::Form::NEVRA});
                    if (nevras.empty()) {
                        // TODO(jmracek) Unparsable NEVRA - What to do?
                        continue;
                    }
                    auto & nevra = *nevras.begin();
                    auto arch = nevra.get_arch();
                    if (arch == "src" || arch == "nosrc") {
                        src_names.push_back(nevra.get_name());
                    } else {
                        names.push_back(nevra.get_name());
                        reldep_name_list.add_reldep(nevra.get_name());
                    }
                }
            }
            // } else {
            //     for (const auto &rpm : artifacts) {
            //         auto nevras = rpm::Nevra::parse(rpm, {rpm::Nevra::Form::NEVRA});
            //         if (nevras.empty()) {
            //             // TODO(jmracek) Unparsable NEVRA - What to do?
            //             continue;rpm/package_query.hpp
            //         }
            //         auto & nevra = *nevras.begin();
            //         bool found = false;
            //         for ( auto & demodularized : it->second) {
            //             if (nevra.get_name() == demodularized) {
            //                 found = true;
            //                 break;
            //             }
            //         }
            //         if (found) {
            //             continue;
            //         }
            //         auto arch = nevra.get_arch();
            //         // source packages do not provide anything and must not cause excluding binary packages
            //         if (arch == "src" || arch == "nosrc") {
            //             src_names.push_back(nevra.get_name());
            //         } else {
            //             names.push_back(nevra.get_name());
            //             reldep_name_list.add_reldep(nevra.get_name());
            //         }
            //     }rpm/package_query.hpp
            // }
            copy(std::begin(artifacts), std::end(artifacts), std::back_inserter(include_NEVRAs));
        } else {
            copy(std::begin(artifacts), std::end(artifacts), std::back_inserter(exclude_NEVRAs));
        }
    }

    return std::make_tuple(
        std::move(include_NEVRAs),
        std::move(exclude_NEVRAs),
        std::move(names),
        std::move(src_names),
        std::move(reldep_name_list));
}

void ModuleSack::Impl::module_filtering() {
    if (get_modules().empty()) {
        return;
    }

    // Try to automatically detect platform id
    if (!platform_detected) {
        auto platform_id = detect_platform_name_and_stream();
        if (platform_id) {
            ModuleItem::create_platform_solvable(module_sack->get_weak_ptr(), platform_id->first, platform_id->second);
            platform_detected = true;
        }
    }

    auto [include_NEVRAs, exclude_NEVRAs, names, src_names, reldep_name_list] = collect_data_for_modular_filtering();

    // Packages from system, commandline, and hotfix repositories are not targets for modular filtering
    libdnf5::rpm::PackageQuery target_packages(base);

    // TODO(replace) "@System", "@commandline" by defined variables like in dnf4
    std::vector<std::string> keep_repo_ids = {"@System", "@commandline"};

    libdnf5::repo::RepoQuery hotfix_repos(base);
    hotfix_repos.filter_enabled(true);
    hotfix_repos.filter(
        [](const libdnf5::repo::RepoWeakPtr & repo) {
            return repo->get_config().get_module_hotfixes_option().get_value();
        },
        true,
        libdnf5::sack::QueryCmp::EQ);
    for (auto & repo : hotfix_repos) {
        keep_repo_ids.push_back(repo->get_id());
    }

    target_packages.filter_repo_id(keep_repo_ids, libdnf5::sack::QueryCmp::NEQ);

    libdnf5::rpm::PackageQuery include_query(base);
    libdnf5::rpm::PackageQuery exclude_query(target_packages);
    libdnf5::rpm::PackageQuery exclude_provides_query(target_packages);
    libdnf5::rpm::PackageQuery exclude_names_query(target_packages);
    libdnf5::rpm::PackageQuery exclude_src_names_query(target_packages);

    include_query.filter_nevra(include_NEVRAs);

    // All packages from not active modules must be filtered out by modular filtering except packages from active
    // modules
    exclude_query.filter_nevra(exclude_NEVRAs);
    exclude_query.difference(include_query);

    // Exclude packages by their Provides. Provides are used to disable obsoletes. Remove included modular packages to
    // not exclude modular packages from active modules
    exclude_provides_query.filter_provides(reldep_name_list);
    exclude_provides_query.difference(include_query);

    // Search for source packages with same names as included source artifacts. Handling of source packages differently
    // prevent filtering out of binary packages that has the same name as source package but binary package is not
    // in module (it prevents creation of broken dependenciers in the distribution)
    exclude_src_names_query.filter_name(src_names);
    exclude_src_names_query.filter_arch(std::vector<std::string>{"src", "nosrc"});

    // Required to filtrate out source packages and packages with incompatible architectures.
    exclude_names_query.filter_name(names);

    // Performance optimization => merging with exclude_src_names_query will prevent additional removal of included
    // packages. Remove included modular packages to not exclude modular packages from active modules
    exclude_names_query.update(exclude_src_names_query);
    exclude_names_query.difference(include_query);

    base->get_rpm_package_sack()->p_impl->set_module_excludes(exclude_query);
    base->get_rpm_package_sack()->p_impl->add_module_excludes(exclude_provides_query);
    base->get_rpm_package_sack()->p_impl->add_module_excludes(exclude_names_query);

    // TODO(jmracek) Store also includes or data more structuralized - module not actave packages,
    // filtered out not modular packages or so on

    // dnf_sack_set_module_includes(sack, includeQuery.getResultPset());*/
}


void ModuleSack::Impl::make_provides_ready() {
    if (provides_ready) {
        return;
    }

    // Temporarily replaces the considered map with an empty one. Ignores "excludes" during calculation provides.
    Map * considered = pool->considered;
    pool->considered = nullptr;

    // Internalize repositories
    for (auto repo_pair : repositories) {
        repo_internalize(pool_id2repo(pool, repo_pair.second));
    }

    pool_createwhatprovides(pool);
    provides_ready = true;

    // Sets the original considered map.
    pool->considered = considered;
}


void ModuleSack::Impl::recompute_considered_in_pool() {
    if (considered_uptodate) {
        return;
    }

    // TODO(pkratoch): This can be optimized: pool->considered can be a nullptr if there are no excludes, so, we can
    // check it at the beginning and either set the pool->considered to nullptr or not initialize it in the first
    // place (if it already was a nullptr).
    if (!pool->considered) {
        pool->considered = static_cast<Map *>(g_malloc0(sizeof(Map)));
        map_init(pool->considered, pool->nsolvables);
    } else {
        map_grow(pool->considered, pool->nsolvables);
    }
    map_setall(pool->considered);

    if (excludes) {
        map_subtract(pool->considered, &excludes->get_map());
    }

    considered_uptodate = true;
}


void ModuleSack::Impl::set_active_modules(ModuleGoalPrivate & goal) {
    active_modules.clear();

    std::set<std::string> solvable_names;
    for (auto id : goal.list_installs()) {
        Solvable * s = pool_id2solvable(pool, id);
        const char * name = pool_id2str(pool, s->name);
        solvable_names.emplace(name);
    }
    for (const auto & module_item : modules) {
        std::string solvable_name = module_item->get_name_stream_staticcontext();
        if (solvable_names.contains(solvable_name)) {
            active_modules[module_item->get_id().id] = module_item.get();
        }
    }
}


// TODO(pkratoch): Could be done more efficiently, e.g. dnf4 used PackageSets
void ModuleSack::Impl::enable_dependent_modules() {
    std::map<std::string, std::vector<std::string>> modules_to_enable;

    // Go through newly enabled active module items and store their dependencies in modules_to_enable
    for (const auto & active_module : active_modules) {
        const auto & state = module_db->get_runtime_module_state(active_module.second->get_name());
        if (state.original.status != ModuleStatus::ENABLED && state.changed.status == ModuleStatus::ENABLED) {
            for (const auto & dependency : active_module.second->get_module_dependencies()) {
                try {
                    if (module_db->get_status(dependency.get_module_name()) == ModuleStatus::AVAILABLE) {
                        modules_to_enable.emplace(dependency.get_module_name(), dependency.get_streams());
                    }
                } catch (NoModuleError &) {
                    ;
                }
            }
        }
    }

    // While there are some modules to enable, search for corresponding active module items and enable them + process their dependencies
    bool new_modules_to_enable = true;
    while (new_modules_to_enable && !modules_to_enable.empty()) {
        new_modules_to_enable = false;
        for (const auto & active_module : active_modules) {
            const auto & module_name = active_module.second->get_name();
            const auto & module_stream = active_module.second->get_stream();
            const auto & module_to_enable = modules_to_enable.find(module_name);
            // If this module_item is in the modules_to_enable, enable it + process its dependencies
            if (module_to_enable != modules_to_enable.end() &&
                (module_to_enable->second.empty() ||
                 std::find(module_to_enable->second.begin(), module_to_enable->second.end(), module_stream) !=
                     module_to_enable->second.end())) {
                modules_to_enable.erase(module_to_enable);
                module_db->change_status(module_name, ModuleStatus::ENABLED);
                module_db->change_stream(module_name, module_stream);
                for (const auto & dependency : active_module.second->get_module_dependencies()) {
                    try {
                        if (module_db->get_status(dependency.get_module_name()) == ModuleStatus::AVAILABLE) {
                            new_modules_to_enable = true;
                            modules_to_enable.emplace(dependency.get_module_name(), dependency.get_streams());
                        }
                    } catch (NoModuleError &) {
                        ;
                    }
                }
            }
        }
    }
    libdnf_assert(
        modules_to_enable.empty(), "Some enabled modules or their dependencies were not found among active modules.");
}


std::pair<std::vector<std::vector<std::tuple<ProblemRules, Id, Id, Id, std::string>>>, GoalProblem>
ModuleSack::Impl::module_solve(std::vector<ModuleItem *> & module_items) {
    std::vector<std::vector<std::tuple<ProblemRules, Id, Id, Id, std::string>>> problems;
    if (module_items.empty()) {
        active_modules.clear();
        return std::make_pair(problems, GoalProblem::NO_PROBLEM);
    }

    recompute_considered_in_pool();
    make_provides_ready();

    // Require both enabled and default module streams + require latest versions
    ModuleGoalPrivate goal_strict(base->get_module_sack()->get_weak_ptr());
    // Require only enabled module streams + require latest versions
    ModuleGoalPrivate goal_best(base->get_module_sack()->get_weak_ptr());
    // Require only enabled module streams
    ModuleGoalPrivate goal(base->get_module_sack()->get_weak_ptr());
    // No strict requirements
    ModuleGoalPrivate goal_weak(base->get_module_sack()->get_weak_ptr());

    for (const auto & module_item : module_items) {
        // Create "module(name:stream)" provide reldep
        const Id reldep_id = pool_str2id(pool, fmt::format("module({})", module_item->get_name_stream()).c_str(), 1);
        goal_strict.add_provide_install(reldep_id, 0, 1);
        goal_weak.add_provide_install(reldep_id, 1, 0);
        if (module_db->get_status(module_item->get_name()) == ModuleStatus::ENABLED) {
            goal_best.add_provide_install(reldep_id, 0, 1);
            goal.add_provide_install(reldep_id, 0, 0);
        } else {
            goal_best.add_provide_install(reldep_id, 1, 1);
            goal.add_provide_install(reldep_id, 1, 0);
        }
    }

    // There are queues of modules to enable that must be added to goals, because they better match user requirements
    // than just "module(name:stream)" provides. (E.g. user might have requested specific context or version.)
    for (auto & queue : modules_to_enable) {
        goal_strict.add_install(queue, 0, 1);
        goal_weak.add_install(queue, 1, 0);
        goal_best.add_install(queue, 0, 1);
        goal.add_install(queue, 0, 0);
    }

    auto ret = goal_strict.resolve();

    // Store modules debug solver data
    if (base->get_config().get_debug_solver_option().get_value()) {
        auto debug_dir = std::filesystem::path(base->get_config().get_debugdir_option().get_value());
        auto modules_debug_dir = std::filesystem::absolute(debug_dir / "modules");
        std::filesystem::create_directories(modules_debug_dir);

        goal_strict.write_debugdata(modules_debug_dir);
    }

    if (ret == libdnf5::GoalProblem::NO_PROBLEM) {
        set_active_modules(goal_strict);
        return make_pair(problems, GoalProblem::NO_PROBLEM);
    }

    problems = goal_strict.get_problems();

    ret = goal_best.resolve();

    if (ret == libdnf5::GoalProblem::NO_PROBLEM) {
        set_active_modules(goal_best);
        return make_pair(problems, GoalProblem::MODULE_SOLVER_ERROR_DEFAULTS);
    }

    ret = goal.resolve();

    if (ret == libdnf5::GoalProblem::NO_PROBLEM) {
        set_active_modules(goal);
        return make_pair(problems, GoalProblem::MODULE_SOLVER_ERROR_LATEST);
    }

    // Conflicting modules has to be removed otherwise it could result than one of them will be active
    for (auto conflicting_module_id : goal.list_conflicting()) {
        excludes->add(conflicting_module_id);
    }

    ret = goal_weak.resolve();

    if (ret == libdnf5::GoalProblem::NO_PROBLEM) {
        set_active_modules(goal_weak);
        return make_pair(problems, GoalProblem::MODULE_SOLVER_ERROR);
    }

    auto logger = base->get_logger();
    logger->critical("Modularity filtering totally broken\n");

    active_modules.clear();
    return make_pair(problems, GoalProblem::MODULE_SOLVER_ERROR);
}


/// @brief Parse platform id from string value.
/// @param platform_id Platform id string.
/// @return Pair where first item is the platform module name and second is the platform stream.
static std::pair<std::string, std::string> parse_platform_id_from_string(const std::string & platform_id) {
    auto index = platform_id.find(':');
    if (index == std::string::npos) {
        return {};
    } else {
        return make_pair(platform_id.substr(0, index), platform_id.substr(index + 1));
    }
}


/// @brief Custom exception to recognize an error in platform id string format.
class PlatformIdFormatError : public libdnf5::Error {
    using Error::Error;
    const char * get_domain_name() const noexcept override { return "libdnf5::module"; }
    const char * get_name() const noexcept override { return "PlatformIdFormatError"; }
};


/// @brief Parse platform id from string value inside the 'os-release' file.
/// @param os_release_path Full path to the 'os-release' file.
/// @return Pair where first item is the platform module name and second is the platform stream.
/// @throws PlatformIdFormatError when platform id in the file has incorrect format.
/// @throws libdnf5::FileSystemError when there is any error during reading a file.
static std::pair<std::string, std::string> parse_platform_id_from_file(const std::string & os_release_path) {
    libdnf5::utils::fs::File file{os_release_path, "r"};
    std::string line;
    while (file.read_line(line)) {
        if (line.find("PLATFORM_ID") != std::string::npos) {
            auto eq_position = line.find('=');
            if (eq_position == std::string::npos) {
                throw PlatformIdFormatError(M_("Missing '='"));
            }
            auto start_position = line.find('"', eq_position + 1);
            if (start_position == std::string::npos) {
                throw PlatformIdFormatError(M_("Missing '\"' in the value"));
            }
            auto colon_position = line.find(':', eq_position + 1);
            if (colon_position == std::string::npos) {
                throw PlatformIdFormatError(M_("Missing ':' in the value"));
            }
            auto end_position = line.find('"', colon_position + 1);
            if (end_position == std::string::npos) {
                throw PlatformIdFormatError(M_("Missing '\"' in the value"));
            }

            return make_pair(
                line.substr(start_position + 1, colon_position - start_position - 1),
                line.substr(colon_position + 1, end_position - colon_position - 1));
        }
    }
    return {};
}


/// @brief Parse value inside parentheses of the provide string value.
/// Example of a specific usecase:
/// - when the \a provide_name is 'base-module'.
/// - and the whole provides string is 'base-module(platform:f38)'.
/// - then the parsed value is 'platform:f38'.
/// @param packages Package set where the provides strings are searched for.
/// @param provide_name Name of the provide to be parsed.
/// @return Set of all parsed values found in \a packages from provides having the \a provide_name name.
static std::set<std::string> get_strings_from_provide(
    const libdnf5::rpm::PackageSet & packages, const char * provide_name) {
    std::set<std::string> strings;

    auto provide_name_length = strlen(provide_name);
    for (auto const & package : packages) {
        auto const & provides = package.get_provides();
        auto found_provide = std::find_if(provides.begin(), provides.end(), [&](const libdnf5::rpm::Reldep & dep) {
            return !strncmp(dep.get_name(), provide_name, strlen(provide_name));
        });
        if (found_provide != provides.end()) {
            auto found_provide_name = (*found_provide).get_name();
            auto found_provide_name_length = strlen(found_provide_name);
            if (found_provide_name[provide_name_length] == '(' &&
                found_provide_name[found_provide_name_length - 1] == ')') {
                strings.emplace(
                    found_provide_name + provide_name_length + 1, found_provide_name_length - provide_name_length - 2);
            }
        }
    }

    return strings;
}


std::optional<std::pair<std::string, std::string>> ModuleSack::Impl::detect_platform_name_and_stream() const {
    // try to detect platform id from configuration
    auto & config_platform_option = base->get_config().get_module_platform_id_option();
    if (!config_platform_option.empty()) {
        auto & config_platform = config_platform_option.get_value();
        auto parsed_platform_id = parse_platform_id_from_string(config_platform);
        if (!parsed_platform_id.first.empty() && !parsed_platform_id.second.empty()) {
            return parsed_platform_id;
        } else {
            base->get_logger()->debug("Invalid format of platform ID in configuration: {}", config_platform);
        }
    }

    libdnf5::rpm::PackageQuery base_query(base);
    base_query.filter_provides("system-release");
    base_query.filter_latest_evr();

    // try to detect platform id from available packages
    libdnf5::rpm::PackageQuery available_query(base_query);
    available_query.filter_available();
    auto available_provides = get_strings_from_provide(available_query, "base-module");
    if (available_provides.size() == 1) {
        auto & query_platform_id = *available_provides.begin();
        auto parsed_platform_id = parse_platform_id_from_string(query_platform_id);
        if (!parsed_platform_id.first.empty() && !parsed_platform_id.second.empty()) {
            return parsed_platform_id;
        } else {
            base->get_logger()->debug(
                "Invalid format of module platform queried from available packages: {}", query_platform_id);
        }
    } else {
        base->get_logger()->debug("Multiple module platforms provided by available packages");
    }

    // try to detect platform id from installed packages
    libdnf5::rpm::PackageQuery installed_query(base_query);
    installed_query.filter_installed();
    auto installed_provides = get_strings_from_provide(installed_query, "base-module");
    if (installed_provides.size() == 1) {
        auto & query_platform_id = *installed_provides.begin();
        auto parsed_platform_id = parse_platform_id_from_string(query_platform_id);
        if (!parsed_platform_id.first.empty() && !parsed_platform_id.second.empty()) {
            return parsed_platform_id;
        } else {
            base->get_logger()->debug(
                "Invalid format of module platform queried from installed packages: {}", query_platform_id);
        }
    } else {
        base->get_logger()->debug("Multiple module platforms provided by installed packages");
    }

    // try to detect platform id from release files
    // TODO(jkolarik): Create constants for paths
    std::vector<std::string> os_release_paths{"etc/os-release", "usr/lib/os-release"};
    auto & installroot = base->get_config().get_installroot_option().get_value();
    for (auto & os_release_path : os_release_paths) {
        std::string full_path = std::filesystem::canonical(installroot) / os_release_path;
        std::pair<std::string, std::string> parsed_platform_id;

        try {
            parsed_platform_id = parse_platform_id_from_file(full_path);
        } catch (const PlatformIdFormatError & id_except) {
            base->get_logger()->debug(
                "Invalid format of PLATFORM_ID in '{}': {}", full_path, std::string(id_except.what()));
        } catch (const FileSystemError & fs_except) {
            base->get_logger()->debug(
                "Detection of module platform in '{}' failed: {}", full_path, std::string(fs_except.what()));
        }

        if (!parsed_platform_id.first.empty() && !parsed_platform_id.second.empty()) {
            return parsed_platform_id;
        }
    }

    base->get_logger()->debug("No valid Platform ID detected");
    return std::nullopt;
}


std::pair<base::SolverProblems, GoalProblem> ModuleSack::resolve_active_module_items() {
    p_impl->considered_uptodate = false;
    p_impl->excludes.reset(new libdnf5::solv::SolvMap(p_impl->pool->nsolvables));
    p_impl->module_db->initialize();

    ModuleStatus status;
    std::vector<ModuleItem *> module_items_to_solve;
    // Use only enabled or default modules for transaction
    for (const auto & module_item : get_modules()) {
        const auto & module_name = module_item->get_name();
        status = p_impl->module_db->get_status(module_name);
        if (status == ModuleStatus::DISABLED) {
            p_impl->excludes->add(module_item->get_id().id);
        } else if (
            status == ModuleStatus::ENABLED &&
            p_impl->module_db->get_enabled_stream(module_name) == module_item->get_stream()) {
            module_items_to_solve.push_back(module_item.get());
        } else if (status == ModuleStatus::AVAILABLE && get_default_stream(module_name) == module_item->get_stream()) {
            module_items_to_solve.push_back(module_item.get());
        }
    }

    auto problems = p_impl->module_solve(module_items_to_solve);
    active_modules_resolved = true;
    return std::make_pair(
        base::SolverProblems(base::process_module_solver_problems(p_impl->pool, problems.first)), problems.second);
}


static ModuleQuery module_spec_to_query(BaseWeakPtr & base, const std::string & module_spec) {
    for (auto & nsvcap : Nsvcap::parse(module_spec)) {
        ModuleQuery nsvcap_query(base, false);
        nsvcap_query.filter_nsvca(nsvcap, libdnf5::sack::QueryCmp::GLOB);
        if (!nsvcap_query.empty()) {
            return nsvcap_query;
        }
    }
    throw NoModuleError(M_("No such module: {}"), module_spec);
}


bool ModuleSack::Impl::enable(const std::string & name, const std::string & stream, bool count) {
    module_db->initialize();
    bool changed = false;
    changed |= module_db->change_stream(name, stream, count);
    changed |= module_db->change_status(name, ModuleStatus::ENABLED);
    if (changed) {
        module_db->clear_profiles(name);
    }
    return changed;
}


// module dict { name : { stream : [solvable id] } }
static std::unordered_map<std::string, std::unordered_map<std::string, libdnf5::solv::IdQueue>> create_module_dict(
    const ModuleQuery & module_query) {
    std::unordered_map<std::string, std::unordered_map<std::string, libdnf5::solv::IdQueue>> module_dict;
    for (const auto & module_item : module_query.list()) {
        module_dict[module_item.get_name()][module_item.get_stream()].push_back(module_item.get_id().id);
    }
    return module_dict;
}


std::set<std::string> ModuleSack::Impl::prune_module_dict(
    std::unordered_map<std::string, std::unordered_map<std::string, libdnf5::solv::IdQueue>> & module_dict) {
    // Vector of module names with multiple streams to enable
    std::set<std::string> multiple_stream_modules;

    for (auto & module_dict_iter : module_dict) {
        auto name = module_dict_iter.first;
        auto & stream_dict = module_dict_iter.second;
        auto module_status = module_db->get_status(name);

        // Multiple streams match the requested spec
        if (stream_dict.size() > 1) {
            // Get stream that is either enabled (for ENABLED module), or default (otherwise)
            std::string enabled_or_default_stream;
            if (module_status == ModuleStatus::ENABLED) {
                enabled_or_default_stream = module_db->get_enabled_stream(name);
            } else {
                enabled_or_default_stream = module_sack->get_default_stream(name);
            }

            // Module doesn't have any enabled nor default stream
            if (enabled_or_default_stream.empty()) {
                for (const auto & stream_pair : stream_dict) {
                    multiple_stream_modules.insert(fmt::format("{}:{}", name, stream_pair.first));
                }
                continue;
            }

            // The enabled or default stream is not one of the possible streams
            if (stream_dict.find(enabled_or_default_stream) == stream_dict.end()) {
                for (const auto & stream_pair : stream_dict) {
                    multiple_stream_modules.insert(fmt::format("{}:{}", name, stream_pair.first));
                }
                continue;
            }

            // Remove all streams except for the enabled_or_default_stream
            for (auto iter = stream_dict.begin(); iter != stream_dict.end();) {
                if (iter->first != enabled_or_default_stream) {
                    stream_dict.erase(iter++);
                } else {
                    ++iter;
                }
            }
        }
    }
    return multiple_stream_modules;
}


std::pair<bool, std::set<std::string>> ModuleSack::Impl::enable(const std::string & module_spec, bool count) {
    module_db->initialize();

    // For the given module_spec, create a dict { name : {stream : [solvable id] }
    auto module_dict = create_module_dict(module_spec_to_query(base, module_spec));
    // Keep only enabled or default streams if possible
    auto multiple_stream_modules = prune_module_dict(module_dict);

    // If there are any modules with multiple streams to be enabled, return immediately the set of these
    // module:stream strings.
    if (!multiple_stream_modules.empty()) {
        return std::make_pair(false, multiple_stream_modules);
    }

    bool changed = false;
    libdnf5::solv::IdQueue queue;
    for (const auto & module_dict_iter : module_dict) {
        std::string name = module_dict_iter.first;
        for (const auto & stream_dict_iter : module_dict_iter.second) {
            // Enable this stream
            changed |= enable(name, stream_dict_iter.first, count);
            // Create a queue of ids for the stream to be enabled, because it better matches user requirements
            // than just "module(name:stream)" provides. (E.g. user might have requested specific context or version.)
            queue += stream_dict_iter.second;
        }
    }
    modules_to_enable.push_back(queue);

    return std::make_pair(changed, multiple_stream_modules);
}


bool ModuleSack::Impl::disable(const std::string & module_spec, bool count) {
    module_db->initialize();

    bool changed = false;
    for (const auto & module_item : module_spec_to_query(base, module_spec)) {
        const auto & name = module_item.get_name();
        if (module_db->change_status(name, ModuleStatus::DISABLED)) {
            module_db->change_stream(name, "", count);
            module_db->clear_profiles(name);
            changed = true;
        }
    }

    return changed;
}


bool ModuleSack::Impl::reset(const std::string & module_spec, bool count) {
    module_db->initialize();

    bool changed = false;
    for (const auto & module_item : module_spec_to_query(base, module_spec)) {
        const auto & name = module_item.get_name();
        if (module_db->change_status(name, ModuleStatus::AVAILABLE)) {
            module_db->change_stream(name, "", count);
            module_db->clear_profiles(name);
            changed = true;
        }
    }

    return changed;
}


InvalidModuleStatus::InvalidModuleStatus(const std::string & status)
    : libdnf5::Error(M_("Invalid module status: {}"), status) {}


std::string module_status_to_string(ModuleStatus status) {
    switch (status) {
        case ModuleStatus::AVAILABLE:
            return "Available";
        case ModuleStatus::ENABLED:
            return "Enabled";
        case ModuleStatus::DISABLED:
            return "Disabled";
    }
    return "";
}


ModuleStatus module_status_from_string(const std::string & status) {
    if (status == "Available") {
        return ModuleStatus::AVAILABLE;
    } else if (status == "Enabled") {
        return ModuleStatus::ENABLED;
    } else if (status == "Disabled") {
        return ModuleStatus::DISABLED;
    }

    throw InvalidModuleStatus(status);
}

}  // namespace libdnf5::module
