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

#include "base/base_impl.hpp"
#include "repo_cache_private.hpp"
#include "repo_downloader.hpp"
#include "solv/pool.hpp"

#include "libdnf5/base/base.hpp"
#include "libdnf5/utils/bgettext/bgettext-mark-domain.h"
#include "libdnf5/utils/fs/temp.hpp"
#include "libdnf5/utils/to_underlying.hpp"

extern "C" {
#include <solv/chksum.h>
#include <solv/repo_comps.h>
#include <solv/repo_deltainfoxml.h>
#include <solv/repo_repomdxml.h>
#include <solv/repo_rpmdb.h>
#include <solv/repo_rpmmd.h>
#include <solv/repo_solv.h>
#include <solv/repo_updateinfoxml.h>
#include <solv/repo_write.h>
}


namespace libdnf5::repo {

namespace fs = libdnf5::utils::fs;


constexpr auto CHKSUM_TYPE = REPOKEY_TYPE_SHA256;
constexpr const char * CHKSUM_IDENT = "H000";


static std::array<char, SOLV_USERDATA_SOLV_TOOLVERSION_SIZE> get_padded_solv_toolversion() {
    std::array<char, SOLV_USERDATA_SOLV_TOOLVERSION_SIZE> padded_solv_toolversion{};
    std::string solv_ver_str{solv_toolversion};
    std::copy(solv_ver_str.rbegin(), solv_ver_str.rend(), padded_solv_toolversion.rbegin());

    return padded_solv_toolversion;
}

void SolvRepo::userdata_fill(SolvUserdata * userdata) {
    if (strlen(solv_toolversion) > SOLV_USERDATA_SOLV_TOOLVERSION_SIZE) {
        libdnf_throw_assertion(
            "Libsolv's solv_toolvesion is: {} long but we expect max of: {}",
            strlen(solv_toolversion),
            SOLV_USERDATA_SOLV_TOOLVERSION_SIZE);
    }

    memcpy(userdata->dnf_magic, SOLV_USERDATA_MAGIC.data(), SOLV_USERDATA_MAGIC.size());
    memcpy(userdata->dnf_version, SOLV_USERDATA_DNF_VERSION.data(), SOLV_USERDATA_DNF_VERSION.size());
    memcpy(userdata->libsolv_version, get_padded_solv_toolversion().data(), SOLV_USERDATA_SOLV_TOOLVERSION_SIZE);
    memcpy(userdata->checksum, checksum, CHKSUM_BYTES);
}

bool SolvRepo::can_use_solvfile_cache(solv::Pool & pool, fs::File & solvfile_cache) {
    auto & logger = *base->get_logger();

    if (!solvfile_cache) {
        logger.debug(("Missing solvfile cache: \"{}\""), solvfile_cache.get_path().native());
        return false;
    }

    unsigned char * dnf_solv_userdata_read;
    int dnf_solv_userdata_len_read;

    int ret_code = solv_read_userdata(solvfile_cache.get(), &dnf_solv_userdata_read, &dnf_solv_userdata_len_read);
    if (ret_code != 0) {
        logger.warning(
            ("Failed to read solv userdata: \"{}\": for: {}"), pool_errstr(*pool), solvfile_cache.get_path().native());
        return false;
    }
    std::unique_ptr<SolvUserdata, decltype(&solv_free)> solv_userdata(
        reinterpret_cast<SolvUserdata *>(dnf_solv_userdata_read), &solv_free);

    if (dnf_solv_userdata_len_read != SOLV_USERDATA_SIZE) {
        logger.warning(
            ("Solv userdata length mismatch read: \"{}\" vs expected \"{}\" for: {}"),
            dnf_solv_userdata_len_read,
            SOLV_USERDATA_SIZE,
            solvfile_cache.get_path().native());
        return false;
    }

    // check dnf solvfile magic bytes
    if (memcmp(solv_userdata->dnf_magic, SOLV_USERDATA_MAGIC.data(), SOLV_USERDATA_MAGIC.size()) != 0) {
        logger.warning(
            "Magic bytes don't match, read: \"{}\" vs. dnf solvfile magic: \"{}\" for: {}",
            solv_userdata->dnf_magic,
            SOLV_USERDATA_MAGIC.data(),
            solvfile_cache.get_path().native());
        return false;
    }

    // check dnf solvfile version
    if (memcmp(solv_userdata->dnf_version, SOLV_USERDATA_DNF_VERSION.data(), SOLV_USERDATA_DNF_VERSION.size()) != 0) {
        logger.warning(
            "Dnf solvfile version doesn't match, read: \"{}\" vs. expected dnf solvfile version: \"{}\" for: {}",
            solv_userdata->dnf_version,
            SOLV_USERDATA_DNF_VERSION.data(),
            solvfile_cache.get_path().native());
        return false;
    }

    // check libsolv solvfile version
    if (memcmp(
            solv_userdata->libsolv_version,
            get_padded_solv_toolversion().data(),
            SOLV_USERDATA_SOLV_TOOLVERSION_SIZE) != 0) {
        logger.warning(
            "Libsolv solvfile version doesn't match, read: \"{}\" vs. expected libsolv version: \"{}\" for: {}",
            solv_userdata->libsolv_version,
            solv_toolversion,
            solvfile_cache.get_path().native());
        return false;
    }

    // check solvfile checksum
    if (memcmp(solv_userdata->checksum, checksum, CHKSUM_BYTES) != 0) {
        logger.debug(
            "Solvfile's repomd checksum doesn't match, read: \"{}\" vs. expected repomd checksum: \"{}\" for: {}",
            pool_bin2hex(*pool, solv_userdata->checksum, sizeof solv_userdata->checksum),
            pool_bin2hex(*pool, checksum, sizeof checksum),
            solvfile_cache.get_path().native());
        return false;
    }

    solvfile_cache.rewind();
    return true;
}


// Computes checksum of data in opened file.
// Calls rewind(fp) before returning.
void checksum_calc(unsigned char * out, fs::File & file) {
    // based on calc_checksum_fp in libsolv's solv.c
    char buf[4096];
    auto h = solv_chksum_create(CHKSUM_TYPE);
    int l;

    file.rewind();
    solv_chksum_add(h, CHKSUM_IDENT, strlen(CHKSUM_IDENT));
    while ((l = static_cast<int>(file.read(buf, sizeof(buf)))) > 0) {
        solv_chksum_add(h, buf, l);
    }
    file.rewind();
    solv_chksum_free(h, out);
}


static const char * repodata_type_to_name(RepodataType type) {
    switch (type) {
        case RepodataType::FILELISTS:
            return RepoDownloader::MD_FILENAME_FILELISTS;
        case RepodataType::PRESTO:
            return RepoDownloader::MD_FILENAME_PRESTODELTA;
        case RepodataType::UPDATEINFO:
            return RepoDownloader::MD_FILENAME_UPDATEINFO;
        case RepodataType::COMPS:
            return RepoDownloader::MD_FILENAME_GROUP;
        case RepodataType::OTHER:
            return RepoDownloader::MD_FILENAME_OTHER;
        case RepodataType::APPSTREAM:
            libdnf_throw_assertion("No static filename for RepodataType::APPSTREAM");
            break;
    }

    libdnf_throw_assertion("Unknown RepodataType: {}", utils::to_underlying(type));
}


static int repodata_type_to_flags(RepodataType type) {
    switch (type) {
        case RepodataType::FILELISTS:
            return REPO_EXTEND_SOLVABLES | REPO_LOCALPOOL;
        case RepodataType::PRESTO:
            return REPO_EXTEND_SOLVABLES;
        case RepodataType::UPDATEINFO:
            return 0;
        case RepodataType::COMPS:
            return 0;
        case RepodataType::OTHER:
            return REPO_EXTEND_SOLVABLES | REPO_LOCALPOOL;
        case RepodataType::APPSTREAM:
            return 0;
    }

    libdnf_throw_assertion("Unknown RepodataType: {}", utils::to_underlying(type));
}


// Returns `true` when all solvables in the repository are stored continuously,
// without interleaving with solvables from other repositories.
// Complexity: Linear to the current number of solvables in the repository
bool is_one_piece(::Repo * repo) {
    for (auto i = repo->start; i < repo->end; ++i) {
        if (repo->pool->solvables[i].repo != repo) {
            return false;
        }
    }

    return true;
}


SolvRepo::SolvRepo(const libdnf5::BaseWeakPtr & base, const ConfigRepo & config, void * appdata)
    : base(base),
      config(config),
      repo(repo_create(*get_rpm_pool(base), config.get_id().c_str())),
      comps_repo(repo_create(*get_comps_pool(base), config.get_id().c_str())) {
    repo->appdata = appdata;
    comps_repo->appdata = appdata;
}


SolvRepo::~SolvRepo() {
    repo->appdata = nullptr;
    comps_repo->appdata = nullptr;
}


void SolvRepo::load_repo_main(const std::string & repomd_fn, const std::string & primary_fn) {
    auto & logger = *base->get_logger();
    auto & pool = get_rpm_pool(base);

    fs::File repomd_file(repomd_fn, "r");

    checksum_calc(checksum, repomd_file);

    int solvables_start = pool->nsolvables;
    int repodata_start = repo->nrepodata;

    if (load_solv_cache(pool, nullptr, 0)) {
        main_solvables_start = solvables_start;
        main_solvables_end = pool->nsolvables;
        main_repodata_start = repodata_start;
        main_repodata_end = repo->nrepodata;

        return;
    }

    fs::File primary_file(primary_fn, "r", true);

    logger.debug("Loading repomd and primary for repo \"{}\"", config.get_id());
    if (repo_add_repomdxml(repo, repomd_file.get(), 0) != 0) {
        throw SolvError(
            M_("Failed to load repomd for repo \"{}\" from \"{}\": {}."),
            config.get_id(),
            repomd_fn,
            std::string(pool_errstr(*pool)));
    }

    if (repo_add_rpmmd(repo, primary_file.get(), 0, 0) != 0) {
        throw SolvError(
            M_("Failed to load primary for repo \"{}\" from \"{}\": {}."),
            config.get_id(),
            primary_fn,
            std::string(pool_errstr(*pool)));
    }

    main_solvables_start = solvables_start;
    main_solvables_end = pool->nsolvables;
    main_repodata_start = repodata_start;
    main_repodata_end = repo->nrepodata;

    if (config.get_build_cache_option().get_value()) {
        write_main(true);
    }
}


void SolvRepo::load_system_repo_ext(RepodataType type) {
    std::string type_name = repodata_type_to_name(type);
    switch (type) {
        case RepodataType::COMPS: {
            // get installed groups from system state and load respective xml files
            // to the libsolv pool
            auto & system_state = base->p_impl->get_system_state();
            auto comps_dir = system_state.get_group_xml_dir();
            for (auto & group_id : system_state.get_installed_groups()) {
                auto ext_fn = comps_dir / "groups" / (group_id + ".xml");
                if (!read_group_solvable_from_xml(ext_fn)) {
                    // The group xml file either not exists or is not parseable by
                    // libsolv.
                    groups_missing_xml.push_back(std::move(group_id));
                }
            }
            for (auto & environment_id : system_state.get_installed_environments()) {
                auto ext_fn = comps_dir / "environments" / (environment_id + ".xml");
                if (!read_group_solvable_from_xml(ext_fn)) {
                    // The environment xml file either not exists or is not parseable by
                    // libsolv.
                    environments_missing_xml.push_back(std::move(environment_id));
                }
            }
            break;
        }
        case RepodataType::FILELISTS:
        case RepodataType::OTHER:
        case RepodataType::PRESTO:
        case RepodataType::UPDATEINFO:
        case RepodataType::APPSTREAM:
            throw SolvError(M_("Unsupported extended repodata type for the system repo: \"{}\"."), type_name);
    }
}

void SolvRepo::load_repo_ext(RepodataType type, const DownloadData & download_data) {
    std::string type_name = "";
    load_repo_ext(type, type_name, download_data);
}

void SolvRepo::load_repo_ext(RepodataType type, const std::string & in_type_name, const DownloadData & download_data) {
    auto & logger = *base->get_logger();
    solv::Pool & pool = type == RepodataType::COMPS ? static_cast<solv::Pool &>(get_comps_pool(base))
                                                    : static_cast<solv::Pool &>(get_rpm_pool(base));

    std::string type_name = in_type_name.empty() ? repodata_type_to_name(type) : in_type_name;

    std::string ext_fn;

    if (type == RepodataType::COMPS) {
        ext_fn = download_data.get_metadata_path(RepoDownloader::MD_FILENAME_GROUP_GZ);
        if (ext_fn.empty()) {
            ext_fn = download_data.get_metadata_path(type_name);
        }
    } else {
        ext_fn = download_data.get_metadata_path(type_name);
    }

    if (ext_fn.empty()) {
        logger.debug("No {} metadata available for repo \"{}\"", type_name, config.get_id());
        return;
    }

    int solvables_start = pool->nsolvables;

    if (load_solv_cache(pool, type_name.c_str(), repodata_type_to_flags(type))) {
        if (type == RepodataType::UPDATEINFO) {
            updateinfo_solvables_start = solvables_start;
            updateinfo_solvables_end = pool->nsolvables;
        }

        return;
    }

    fs::File ext_file(ext_fn, "r", true);
    logger.debug("Loading {} extension for repo \"{}\" from \"{}\"", type_name, config.get_id(), ext_fn);

    int res = 0;
    switch (type) {
        case RepodataType::FILELISTS:
            res = repo_add_rpmmd(repo, ext_file.get(), "FL", REPO_EXTEND_SOLVABLES);
            break;
        case RepodataType::PRESTO:
            res = repo_add_deltainfoxml(repo, ext_file.get(), 0);
            break;
        case RepodataType::UPDATEINFO:
            if ((res = repo_add_updateinfoxml(repo, ext_file.get(), 0)) == 0) {
                updateinfo_solvables_start = solvables_start;
                updateinfo_solvables_end = pool->nsolvables;
            }
            break;
        case RepodataType::COMPS:
            res = repo_add_comps(comps_repo, ext_file.get(), 0);
            break;
        case RepodataType::OTHER:
            res = repo_add_rpmmd(repo, ext_file.get(), 0, REPO_EXTEND_SOLVABLES);
            break;
        case RepodataType::APPSTREAM:
            break;
    }

    if (res != 0) {
        throw SolvError(
            M_("Failed to load {} extension for repo \"{}\" from \"{}\": {}"),
            type_name,
            config.get_id(),
            ext_fn,
            std::string(pool_errstr(*get_rpm_pool(base))));
    }

    if (config.get_build_cache_option().get_value()) {
        if (type == RepodataType::COMPS) {
            write_ext(comps_repo->nrepodata - 1, type, type_name);
        } else {
            write_ext(repo->nrepodata - 1, type, type_name);
        }
    }
}


void SolvRepo::load_system_repo(const std::string & rootdir) {
    auto & logger = *base->get_logger();
    auto & pool = get_rpm_pool(base);
    std::string real_rootdir{rootdir};

    if (rootdir.empty()) {
        base->get_config().get_installroot_option().lock("installroot locked by loading system repo");
        real_rootdir = base->get_config().get_installroot_option().get_value();
    }
    logger.debug("Loading system repo rpmdb from root \"{}\"", real_rootdir);
    pool_set_rootdir(*pool, real_rootdir.c_str());

    int solvables_start = pool->nsolvables;
    int repodata_start = repo->nrepodata;

    // TODO(egoode) investigate performance hit of RPM_ADD_WITH_CHANGELOG, possibly make this configurable
    int flagsrpm = REPO_REUSE_REPODATA | RPM_ADD_WITH_HDRID | REPO_USE_ROOTDIR | RPM_ADD_WITH_CHANGELOG;
    if (repo_add_rpmdb(repo, nullptr, flagsrpm) != 0) {
        throw SolvError(
            M_("Failed to load system repo from root \"{}\": {}"),
            real_rootdir,
            std::string(pool_errstr(*get_rpm_pool(base))));
    }

    if (!rootdir.empty()) {
        // if loading an extra repo, reset rootdir back to installroot
        pool_set_rootdir(*pool, base->get_config().get_installroot_option().get_value().c_str());
    }

    pool_set_installed(*pool, repo);

    main_solvables_start = solvables_start;
    main_solvables_end = pool->nsolvables;
    main_repodata_start = repodata_start;
    main_repodata_end = repo->nrepodata;
}


// return true if q1 is a superset of q2
// only works if there are no duplicates both in q1 and q2
// the map parameter must point to an empty map that can hold all ids
// (it is also returned empty)
static bool is_superset(
    const libdnf5::solv::IdQueue & q1, const libdnf5::solv::IdQueue & q2, libdnf5::solv::SolvMap & map) {
    int cnt = 0;
    for (int i = 0; i < q2.size(); i++) {
        map.add_unsafe(q2[i]);
    }
    for (int i = 0; i < q1.size(); i++) {
        if (map.contains_unsafe(q1[i])) {
            cnt++;
        }
    }
    for (int i = 0; i < q2.size(); i++) {
        map.remove_unsafe(q2[i]);
    }
    return cnt == q2.size();
}


void SolvRepo::rewrite_repo(libdnf5::solv::IdQueue & fileprovides) {
    auto & logger = *base->get_logger();
    auto & pool = get_rpm_pool(base);

    logger.debug("Rewriting repo \"{}\" with added file provides", config.get_id());

    if (!config.get_build_cache_option().get_value() || main_solvables_start == 0 || fileprovides.size() == 0) {
        return;
    }

    libdnf5::solv::IdQueue fileprovidesq;
    libdnf5::solv::SolvMap providedids(pool->ss.nstrings);
    Repodata * data = repo_id2repodata(repo, 1);
    if (repodata_lookup_idarray(data, SOLVID_META, REPOSITORY_ADDEDFILEPROVIDES, &fileprovidesq.get_queue())) {
        if (is_superset(fileprovidesq, fileprovides, providedids)) {
            return;
        }
    }

    repodata_set_idarray(data, SOLVID_META, REPOSITORY_ADDEDFILEPROVIDES, &fileprovides.get_queue());
    repodata_internalize(data);

    write_main(false);
}


void SolvRepo::internalize() {
    if (!needs_internalizing) {
        return;
    }
    repo_internalize(repo);
    needs_internalizing = false;
}


void SolvRepo::set_priority(int priority) {
    repo->priority = priority;
}


void SolvRepo::set_subpriority(int subpriority) {
    repo->subpriority = subpriority;
}


bool SolvRepo::load_solv_cache(solv::Pool & pool, const char * type_name, int flags) {
    auto & logger = *base->get_logger();

    auto path = solv_file_path(type_name);

    try {
        fs::File cache_file(path, "r");

        if (can_use_solvfile_cache(pool, cache_file)) {
            logger.debug("Loading solv cache file: \"{}\"", path.native());
            if (repo_add_solv(
                    type_name && std::string_view(type_name) == RepoDownloader::MD_FILENAME_GROUP ? comps_repo : repo,
                    cache_file.get(),
                    flags) != 0) {
                throw SolvError(
                    M_("Failed to load {} cache for repo \"{}\" from \"{}\": {}"),
                    type_name ? std::string(type_name) : "primary",
                    config.get_id(),
                    path.native(),
                    std::string(pool_errstr(*get_rpm_pool(base))));
            }
            return true;
        }
    } catch (const FileSystemError & e) {
        if (std::error_code(e.get_error_code(), std::system_category()).default_error_condition() ==
            std::errc::no_such_file_or_directory) {
            logger.trace("Cache file \"{}\" not found", path.native());
        } else {
            logger.warning("Error opening cache file, ignoring: {}", e.what());
        }
    }

    return false;
}


void SolvRepo::write_main(bool load_after_write) {
    auto & logger = *base->get_logger();
    auto & pool = get_rpm_pool(base);

    const char * chksum = pool_bin2hex(*pool, checksum, solv_chksum_len(CHKSUM_TYPE));

    const auto solvfile_path = solv_file_path();
    const auto solvfile_parent_dir = solvfile_path.parent_path();

    std::filesystem::create_directory(solvfile_parent_dir);

    auto cache_tmp_file = fs::TempFile(solvfile_parent_dir, solvfile_path.filename());
    auto & cache_file = cache_tmp_file.open_as_file("w+");

    logger.trace(
        "Writing primary cache for repo \"{}\" to \"{}\" (checksum: 0x{})",
        config.get_id(),
        cache_tmp_file.get_path().native(),
        chksum);

    SolvUserdata solv_userdata{};
    userdata_fill(&solv_userdata);


    Repowriter * writer = repowriter_create(repo);
    repowriter_set_userdata(writer, &solv_userdata, SOLV_USERDATA_SIZE);
    repowriter_set_solvablerange(writer, main_solvables_start, main_solvables_end);
    repowriter_set_repodatarange(writer, main_repodata_start, main_repodata_end);
    int res = repowriter_write(writer, cache_file.get());
    repowriter_free(writer);

    if (res != 0) {
        throw SolvError(
            M_("Failed to write primary cache for repo \"{}\" to \"{}\": {}"),
            config.get_id(),
            cache_tmp_file.get_path().native(),
            std::string(pool_errstr(*pool)));
    }

    cache_tmp_file.close();

    if (load_after_write && is_one_piece(repo)) {
        // this saves memory, libsolv doesn't load all the data from a solv file, it dup()s the fd,
        // keeps the file open and lazily loads some data on-demand.
        fs::File file(cache_tmp_file.get_path(), "r");

        repo_empty(repo, 1);

        // While the number of solvables (main_solvables_start/end) doesn't
        // change when we reload the repo from solv file the repodata can/do change.
        main_repodata_start = repo->nrepodata;

        int ret = repo_add_solv(repo, file.get(), 0);
        if (ret) {
            throw SolvError(
                M_("Failed to re-load primary cache for repo \"{}\" from \"{}\": {}"),
                config.get_id(),
                cache_tmp_file.get_path().native(),
                std::string(pool_errstr(*pool)));
        }

        main_repodata_end = repo->nrepodata;
    }

    std::filesystem::permissions(
        cache_tmp_file.get_path(),
        std::filesystem::perms::group_read | std::filesystem::perms::others_read,
        std::filesystem::perm_options::add);
    std::filesystem::rename(cache_tmp_file.get_path(), solvfile_path);
    cache_tmp_file.release();
}


void SolvRepo::write_ext(Id repodata_id, RepodataType type, const std::string & type_name) {
    libdnf_assert(repodata_id != 0, "0 is not a valid repodata id");

    auto & logger = *base->get_logger();
    solv::Pool & pool = type == RepodataType::COMPS ? static_cast<solv::Pool &>(get_comps_pool(base))
                                                    : static_cast<solv::Pool &>(get_rpm_pool(base));

    const auto solvfile_path = solv_file_path(type_name.c_str());
    const auto solvfile_parent_dir = solvfile_path.parent_path();

    std::filesystem::create_directory(solvfile_parent_dir);

    auto cache_tmp_file = fs::TempFile(solvfile_parent_dir, solvfile_path.filename());
    auto & cache_file = cache_tmp_file.open_as_file("w+");

    logger.trace(
        "Writing {} extension cache for repo \"{}\" to \"{}\"",
        type_name,
        config.get_id(),
        cache_tmp_file.get_path().native());


    SolvUserdata solv_userdata{};
    userdata_fill(&solv_userdata);

    Repowriter * writer;
    if (type == RepodataType::COMPS) {
        writer = repowriter_create(comps_repo);
    } else {
        writer = repowriter_create(repo);
    }
    repowriter_set_userdata(writer, &solv_userdata, SOLV_USERDATA_SIZE);
    repowriter_set_repodatarange(writer, repodata_id, repodata_id + 1);

    if (type == RepodataType::UPDATEINFO) {
        repowriter_set_solvablerange(writer, updateinfo_solvables_start, updateinfo_solvables_end);
    }

    if (type != RepodataType::COMPS && type != RepodataType::UPDATEINFO) {
        repowriter_set_flags(writer, REPOWRITER_NO_STORAGE_SOLVABLE);
    }

    int res = repowriter_write(writer, cache_file.get());
    repowriter_free(writer);

    if (res != 0) {
        throw SolvError(
            M_("Failed to write {} cache for repo \"{}\" to \"{}\": {}"),
            type_name,
            config.get_id(),
            cache_tmp_file.get_path().native(),
            std::string(pool_errstr(*pool)));
    }

    cache_tmp_file.close();

    if (is_one_piece(repo) && type != RepodataType::UPDATEINFO && type != RepodataType::COMPS) {
        // this saves memory, libsolv doesn't load all the data from a solv file, it dup()s the fd,
        // keeps the file open and lazily loads some data on-demand.
        fs::File file(cache_tmp_file.get_path(), "r");

        Repodata * data = repo_id2repodata(repo, repodata_id);

        repodata_extend_block(data, repo->start, repo->end - repo->start);
        data->state = REPODATA_LOADING;
        res = repo_add_solv(repo, file.get(), repodata_type_to_flags(type) | REPO_USE_LOADING);
        if (res) {
            throw SolvError(
                M_("Failed to re-load {} cache for repo \"{}\" from \"{}\": {}"),
                type_name,
                config.get_id(),
                cache_tmp_file.get_path().native(),
                std::string(pool_errstr(*pool)));
        }
        data->state = REPODATA_AVAILABLE;
    }

    std::filesystem::permissions(
        cache_tmp_file.get_path(),
        std::filesystem::perms::group_read | std::filesystem::perms::others_read,
        std::filesystem::perm_options::add);
    std::filesystem::rename(cache_tmp_file.get_path(), solvfile_path);
    cache_tmp_file.release();
}


std::string SolvRepo::solv_file_name(const char * type) {
    if (type != nullptr) {
        return fmt::format("{}-{}.solvx", config.get_id(), type);
    } else {
        return config.get_id() + ".solv";
    }
}


std::filesystem::path SolvRepo::solv_file_path(const char * type) {
    return std::filesystem::path(config.get_cachedir()) / CACHE_SOLV_FILES_DIR / solv_file_name(type);
}

bool SolvRepo::read_group_solvable_from_xml(const std::string & path) {
    auto & logger = *base->get_logger();
    bool read_success = true;

    fs::File ext_file;
    try {
        ext_file = fs::File(path, "r", true);
    } catch (FileSystemError & e) {
        logger.warning("Cannot load group extension for {} repo from \"{}\": {}", config.get_id(), path, e.what());
        read_success = false;
    }

    if (read_success) {
        logger.debug("Loading group extension for {} repo from \"{}\"", config.get_id(), path);
        read_success = repo_add_comps(comps_repo, ext_file.get(), 0) == 0;
        if (!read_success) {
            logger.debug("Loading group extension for {} repo from \"{}\" failed.", config.get_id(), path);
        }
    }

    return read_success;
}

void SolvRepo::create_group_solvable(const std::string & groupid, const libdnf5::system::GroupState & state) {
    solv::Pool & pool = static_cast<solv::Pool &>(get_comps_pool(base));
    libdnf_assert(
        comps_repo == (*pool)->installed, "SolvRepo::create_group_solvable() call enabled only for @System repo.");

    // create a new solvable for the group
    auto group_solvable_id = repo_add_solvable(comps_repo);
    Solvable * group_solvable = pool.id2solvable(group_solvable_id);

    // Information about group contained in the system state is very limited.
    // We have only repoid and list of installed packages.

    // Set id with proper prefix
    group_solvable->name = pool.str2id(("group:" + groupid).c_str(), 1);
    // Set noarch and empty evr
    group_solvable->arch = ARCH_NOARCH;
    group_solvable->evr = ID_EMPTY;
    // Make the new group provide it's own id
    group_solvable->dep_provides = repo_addid_dep(
        comps_repo, group_solvable->dep_provides, pool.rel2id(group_solvable->name, group_solvable->evr, REL_EQ, 1), 0);

    // Add group packages
    for (const auto & pkg_name : state.packages) {
        auto pkg_id = pool.str2id(pkg_name.c_str(), 1);
        Id type = SOLVABLE_RECOMMENDS;
        repo_add_idarray(comps_repo, group_solvable_id, type, pkg_id);
    }

    Repodata * data = repo_last_repodata(comps_repo);

    // Mark the repo as user-visible
    repodata_set_void(data, group_solvable_id, SOLVABLE_ISVISIBLE);

    repodata_internalize(data);
}

void SolvRepo::create_environment_solvable(
    const std::string & environmentid, const libdnf5::system::EnvironmentState & state) {
    solv::Pool & pool = static_cast<solv::Pool &>(get_comps_pool(base));
    libdnf_assert(
        comps_repo == (*pool)->installed,
        "SolvRepo::create_environment_solvable() call enabled only for @System repo.");

    // create a new solvable for the environment
    auto environment_solvable_id = repo_add_solvable(comps_repo);
    Solvable * environment_solvable = pool.id2solvable(environment_solvable_id);

    // Information about environment contained in the system state is very limited.
    // We have only repoid and list of installed groups.

    // Set id with proper prefix
    environment_solvable->name = pool.str2id(("environment:" + environmentid).c_str(), 1);
    // Set noarch and empty evr
    environment_solvable->arch = ARCH_NOARCH;
    environment_solvable->evr = ID_EMPTY;
    // Make the new environment provide it's own id
    environment_solvable->dep_provides = repo_addid_dep(
        comps_repo,
        environment_solvable->dep_provides,
        pool.rel2id(environment_solvable->name, environment_solvable->evr, REL_EQ, 1),
        0);

    // Add groups in the environment
    for (const auto & grp_name : state.groups) {
        auto grp_id = pool.str2id(grp_name.c_str(), 1);
        repo_add_idarray(comps_repo, environment_solvable_id, SOLVABLE_REQUIRES, grp_id);
    }

    Repodata * data = repo_last_repodata(comps_repo);

    // Mark the repo as user-visible
    repodata_set_void(data, environment_solvable_id, SOLVABLE_ISVISIBLE);

    repodata_internalize(data);
}

}  // namespace libdnf5::repo
