/*
Copyright Contributors to the libdnf project.

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 "repo_downloader.hpp"

#include "utils/fs/utils.hpp"
#include "utils/string.hpp"

#include "libdnf5/conf/const.hpp"
#include "libdnf5/repo/repo.hpp"
#include "libdnf5/repo/repo_errors.hpp"
#include "libdnf5/utils/bgettext/bgettext-mark-domain.h"

#include <fmt/format.h>
#include <libdnf5/utils/bgettext/bgettext-lib.h>
#include <librepo/librepo.h>
#include <solv/chksum.h>
#include <solv/util.h>
#include <sys/stat.h>

#include <filesystem>
#include <fstream>
#include <random>

#define RECOGNIZED_CHKSUMS {"sha512", "sha256"}

namespace libdnf5::repo {

static void str_vector_to_char_array(const std::vector<std::string> & vec, const char * arr[]) {
    for (size_t i = 0; i < vec.size(); ++i) {
        arr[i] = vec[i].c_str();
    }
    arr[vec.size()] = nullptr;
}

static LrYumRepo * get_yum_repo(LibrepoResult & result) {
    LrYumRepo * yum_repo;
    result.get_info(LRR_YUM_REPO, &yum_repo);
    return yum_repo;
}

static LrYumRepoMd * get_yum_repomd(LibrepoResult & result) {
    LrYumRepoMd * yum_repomd;
    result.get_info(LRR_YUM_REPOMD, &yum_repomd);
    return yum_repomd;
}

int RepoDownloader::end_cb_full_download(void * data, LrTransferStatus status, const char * msg) {
    libdnf_assert(data != nullptr, "data in callback must be set");

    auto download_callback_data = static_cast<CallbackData *>(data);
    auto cb_status = static_cast<DownloadCallbacks::TransferStatus>(status);
    if (cb_status == DownloadCallbacks::TransferStatus::SUCCESSFUL && download_callback_data) {
        // move all downloaded object from tmpdir to destdir
        for (auto & dir :
             std::filesystem::directory_iterator(download_callback_data->temp_download_target->get_path())) {
            auto tmp_item = dir.path();

            auto target_item = download_callback_data->destination / tmp_item.filename();
            std::filesystem::remove_all(target_item);

            utils::fs::move_recursive(tmp_item, target_item);
        }

        if (download_callback_data->load_repo) {
            download_callback_data->load_repo(download_callback_data->repo.get(), false);
        }
    }
    if (auto * download_callbacks = download_callback_data->repo->get_base()->get_download_callbacks()) {
        return download_callbacks->end(download_callback_data->user_cb_data, cb_status, msg);
    }
    return 0;
}

int RepoDownloader::end_cb_sync_check(void * data, LrTransferStatus status, const char * msg) {
    libdnf_assert(data != nullptr, "data in callback must be set");

    auto cbd = static_cast<CallbackData *>(data);
    auto cb_status = static_cast<DownloadCallbacks::TransferStatus>(status);
    if (cb_status == DownloadCallbacks::TransferStatus::SUCCESSFUL && cbd) {
        cbd->is_in_sync = false;
        auto & repo = *cbd->repo.get();
        auto & config = cbd->repo->get_config();
        auto & download_data = cbd->repo->get_download_data();
        if (!config.get_metalink_option().empty() && !config.get_metalink_option().get_value().empty()) {
            LrMetalink * metalink;
            download_data.handle->get_info(LRI_METALINK, &metalink);
            cbd->is_in_sync = RepoDownloader::is_metalink_in_sync(repo, metalink);
        } else {
            std::filesystem::path repomd;
            if (cbd->lr_target->repo) {
                repomd = cbd->lr_target->repo->repomd;
            }
            cbd->is_in_sync = RepoDownloader::is_repomd_in_sync(repo, repomd);
            if (!cbd->is_in_sync) {
                // We have already downloaded the new repomd, when we will go download the
                // rest of the metadata do just an update.
                if (std::filesystem::exists(repomd)) {
                    download_data.handle->set_opt(LRO_UPDATE, 1);
                }
            }
        }

        if (cbd->load_repo && cbd->is_in_sync) {
            auto logger = repo.get_base()->get_logger();
            logger->debug(
                "Using cache for repo \"{}\". It is expired, but matches the original.", repo.get_config().get_id());
            cbd->load_repo(cbd->repo.get(), true);
        }
    }
    if (auto * download_callbacks = cbd->repo->get_base()->get_download_callbacks()) {
        return download_callbacks->end(cbd->user_cb_data, cb_status, msg);
    }
    return 0;
}

int RepoDownloader::progress_cb(void * data, double total_to_download, double downloaded) {
    if (!data) {
        return 0;
    }
    auto download_callback_data = static_cast<CallbackData *>(data);
    if (auto * download_callbacks = download_callback_data->repo->get_base()->get_download_callbacks()) {
        // "total_to_download" and "downloaded" from librepo are related to the currently downloaded file.
        // We add the size of previously downloaded files.
        // ignore zero progress events at the beginning of the download, so we don't start with 100% progress
        if (total_to_download != 0 || download_callback_data->prev_total_to_download != 0) {
            if (total_to_download != download_callback_data->prev_total_to_download) {
                download_callback_data->prev_total_to_download = total_to_download;
                download_callback_data->sum_prev_downloaded += download_callback_data->prev_downloaded;
            }
            download_callback_data->prev_downloaded = downloaded;
        }
        total_to_download += download_callback_data->sum_prev_downloaded;
        downloaded += download_callback_data->sum_prev_downloaded;

        return download_callbacks->progress(download_callback_data->user_cb_data, total_to_download, downloaded);
    }
    return 0;
}

void RepoDownloader::fastest_mirror_cb(void * data, LrFastestMirrorStages stage, void * ptr) {
    if (!data) {
        return;
    }
    auto download_callback_data = static_cast<CallbackData *>(data);
    if (auto * download_callbacks = download_callback_data->repo->get_base()->get_download_callbacks()) {
        const char * msg;
        std::string msg_string;
        if (ptr) {
            switch (stage) {
                case LR_FMSTAGE_CACHELOADING:
                case LR_FMSTAGE_CACHELOADINGSTATUS:
                case LR_FMSTAGE_STATUS:
                    msg = static_cast<const char *>(ptr);
                    break;
                case LR_FMSTAGE_DETECTION:
                    msg_string = std::to_string(*(static_cast<long *>(ptr)));
                    msg = msg_string.c_str();
                    break;
                default:
                    msg = nullptr;
            }
        } else {
            msg = nullptr;
        }
        download_callbacks->fastest_mirror(
            download_callback_data->user_cb_data, static_cast<DownloadCallbacks::FastestMirrorStage>(stage), msg);
    }
}

int RepoDownloader::mirror_failure_cb(void * data, const char * msg, const char * url) {
    if (!data) {
        return 0;
    }
    auto download_callback_data = static_cast<CallbackData *>(data);
    if (auto * download_callbacks = download_callback_data->repo->get_base()->get_download_callbacks()) {
        // In this callback type we don't have the `metadata` type, pass NULL instead. The dnf5 callback accounts for this.
        return download_callbacks->mirror_failure(download_callback_data->user_cb_data, msg, url, NULL);
    }
    return 0;
};

int RepoDownloader::mirror_failure_cb(
    void * data, const char * msg, const char * url, [[maybe_unused]] const char * metadata) {
    if (!data) {
        return 0;
    }

    auto download_callback_data = static_cast<CallbackData *>(data);
    if (auto * download_callbacks = download_callback_data->repo->get_base()->get_download_callbacks()) {
        return download_callbacks->mirror_failure(download_callback_data->user_cb_data, msg, url, metadata);
    }
    return 0;
};


LibrepoError::LibrepoError(std::unique_ptr<GError> && lr_error)
    : Error(M_("Librepo error: {}"), std::string(lr_error->message)),
      code(lr_error->code) {}


void RepoDownloader::add(
    libdnf5::repo::Repo & repo, const std::string & destdir, std::function<repo_loading_func> load_repo) try {
    auto & cbd = callback_data.emplace_back();
    cbd.repo = repo.get_weak_ptr();
    cbd.destination = destdir;
    cbd.load_repo = load_repo;
    std::filesystem::create_directories(cbd.destination);
    cbd.temp_download_target = libdnf5::utils::fs::TempDir{cbd.destination, "tmpdir"};

    auto & download_data = cbd.repo->get_download_data();
    auto * download_callbacks = cbd.repo->get_base()->get_download_callbacks();
    auto & config = cbd.repo->get_config();
    if (download_callbacks) {
        cbd.user_cb_data = download_callbacks->add_new_download(
            download_data.user_data,
            !config.get_name_option().get_value().empty()
                ? config.get_name_option().get_value().c_str()
                : (!config.get_id().empty() ? config.get_id().c_str() : "unknown"),
            -1);
        cbd.prev_total_to_download = 0;
        cbd.prev_downloaded = 0;
        cbd.sum_prev_downloaded = 0;
    }

    download_data.handle = init_remote_handle(*cbd.repo, cbd.temp_download_target->get_path().c_str(), true);
    download_data.handle->set_opt(LRO_FASTESTMIRRORCB, static_cast<LrFastestMirrorCb>(fastest_mirror_cb));
    add_countme_flag(download_data, *download_data.handle);

    // We don't set the cbdata here yet because the callback_data vector
    // can grow as repos are added which could change the addresses of each
    // cbdata.
    cbd.lr_target = std::unique_ptr<LrMetadataTarget>(lr_metadatatarget_new2(
        download_data.handle->get(),
        NULL,
        static_cast<LrProgressCb>(progress_cb),
        static_cast<LrMirrorFailureCb>(mirror_failure_cb),
        NULL,
        config.get_repo_gpgcheck_option().get_value() ? download_data.pgp.get_keyring_dir().c_str() : NULL,
        0));
} catch (const std::runtime_error & e) {
    libdnf5::throw_with_nested(RepoDownloadError(M_("Failed to set up metadata download")));
}

std::tuple<std::unordered_map<Repo *, std::vector<std::string>>, bool>
RepoDownloader::download_repos_descriptions() try {
    if (callback_data.empty()) {
        return {};
    }

    GSList * list{nullptr};
    for (auto & cbd : callback_data) {
        // If there is currently no metadata it cannot be up to date
        if (cbd.repo->get_download_data().get_metadata_path(RepoDownloader::MD_FILENAME_PRIMARY).empty()) {
            continue;
        }

        // Download only metalink or repomd
        auto & download_data = cbd.repo->get_download_data();
        auto & config = cbd.repo->get_config();
        if (!config.get_metalink_option().empty() && !config.get_metalink_option().get_value().empty()) {
            download_data.handle->set_opt(LRO_FETCHMIRRORS, 1L);
        } else {
            const char * dlist[] = LR_YUM_REPOMDONLY;
            download_data.handle->set_opt(LRO_YUMDLIST, dlist);
        }

        cbd.lr_target->endcb = end_cb_sync_check;
        cbd.lr_target->cbdata = &cbd;
        download_data.handle->set_opt(LRO_FASTESTMIRRORDATA, &cbd);

        list = g_slist_prepend(list, cbd.lr_target.get());
    }
    std::unique_ptr<GSList, decltype(&g_slist_free)> list_holder(list, &g_slist_free);

    GError * err{nullptr};
    if (!lr_download_metadata(list, &err)) {
        throw LibrepoError(std::unique_ptr<GError>(err));
    }

    std::unordered_map<Repo *, std::vector<std::string>> per_repo_errors;
    bool all_in_sync = true;
    for (auto & cbd : callback_data) {
        std::vector<std::string> errors;
        for (GList * err = cbd.lr_target->err; err; err = g_list_next(err)) {
            gchar * err_msg = (gchar *)err->data;
            errors.emplace_back(err_msg);
        }

        per_repo_errors[cbd.repo.get()] = errors;
        all_in_sync &= cbd.is_in_sync;
    }

    return {per_repo_errors, all_in_sync};
} catch (const std::runtime_error & e) {
    libdnf5::throw_with_nested(RepoDownloadError(M_("Failed to download metadata")));
}

std::unordered_map<Repo *, std::vector<std::string>> RepoDownloader::download() try {
    if (callback_data.empty()) {
        return {};
    }

    //TODO(amatej): If we make this API public should we take cacheonly option into account?
    //              PackageDownloader respects it.
    //              For repos it is currently handled in update_and_load_repos so we don't
    //              even attempt to download remote repos with cacheonly enabled.

    GSList * list{nullptr};
    for (auto & cbd : callback_data) {
        // Don't download repositories that are already in sync
        if (cbd.is_in_sync) {
            continue;
        }
        auto & download_data = cbd.repo->get_download_data();
        // Configure handle to download all the needed metadata
        download_data.handle->set_opt(LRO_FETCHMIRRORS, 0L);
        configure_handle_dlist(*download_data.handle, download_data.get_optional_metadata());

        cbd.lr_target->endcb = end_cb_full_download;
        cbd.lr_target->cbdata = &cbd;
        download_data.handle->set_opt(LRO_FASTESTMIRRORDATA, &cbd);

        list = g_slist_prepend(list, cbd.lr_target.get());
    }
    std::unique_ptr<GSList, decltype(&g_slist_free)> list_holder(list, &g_slist_free);

    GError * err{nullptr};
    if (!lr_download_metadata(list, &err)) {
        throw LibrepoError(std::unique_ptr<GError>(err));
    }

    std::unordered_map<Repo *, std::vector<std::string>> per_repo_errors;
    for (auto & cbd : callback_data) {
        std::vector<std::string> errors;
        for (GList * err = cbd.lr_target->err; err; err = g_list_next(err)) {
            gchar * err_msg = (gchar *)err->data;
            errors.emplace_back(err_msg);
        }

        per_repo_errors[cbd.repo.get()] = errors;
    }

    return per_repo_errors;
} catch (const std::runtime_error & e) {
    libdnf5::throw_with_nested(RepoDownloadError(M_("Failed to download metadata")));
}

// Use metalink to check whether our metadata are still current.
bool RepoDownloader::is_metalink_in_sync(Repo & repo, LrMetalink * metalink) try {
    auto & download_data = repo.get_download_data();
    auto & logger = *(download_data.base)->get_logger();
    auto & config = download_data.config;

    if (!metalink) {
        logger.trace("Sync check: repo \"{}\" skipped, no metalink", config.get_id());
        return false;
    }

    // check all recognized hashes
    auto chksum_free = [](Chksum * ptr) { solv_chksum_free(ptr, nullptr); };
    struct hashInfo {
        const LrMetalinkHash * lr_metalink_hash;
        std::unique_ptr<Chksum, decltype(chksum_free)> chksum;
    };
    std::vector<hashInfo> hashes;
    for (auto hash = metalink->hashes; hash; hash = hash->next) {
        auto lr_metalink_hash = static_cast<const LrMetalinkHash *>(hash->data);
        for (auto algorithm : RECOGNIZED_CHKSUMS) {
            if (strcmp(lr_metalink_hash->type, algorithm) == 0)
                hashes.push_back({lr_metalink_hash, {nullptr, chksum_free}});
        }
    }
    if (hashes.empty()) {
        logger.trace("Sync check: repo \"{}\" skipped, no usable hash", config.get_id());
        return false;
    }

    for (auto & hash : hashes) {
        auto chk_type = solv_chksum_str2type(hash.lr_metalink_hash->type);
        hash.chksum.reset(solv_chksum_create(chk_type));
    }

    std::ifstream repomd(download_data.repomd_filename, std::ifstream::binary);
    char buf[4096];
    int readed;
    while ((readed = static_cast<int>(repomd.readsome(buf, sizeof(buf)))) > 0) {
        for (auto & hash : hashes)
            solv_chksum_add(hash.chksum.get(), buf, readed);
    }

    for (auto & hash : hashes) {
        int chksumLen;
        auto chksum = solv_chksum_get(hash.chksum.get(), &chksumLen);
        std::vector<char> chksumHex(static_cast<std::size_t>(chksumLen * 2 + 1), '\0');
        solv_bin2hex(chksum, chksumLen, chksumHex.data());
        if (strcmp(chksumHex.data(), hash.lr_metalink_hash->value) != 0) {
            logger.trace(
                "Sync check: failed for repo \"{}\", {} checksum mismatch",
                config.get_id(),
                hash.lr_metalink_hash->type);
            return false;
        }
    }

    logger.debug("Sync check: repo \"{}\" in sync, metalink checksums match", config.get_id());
    return true;
} catch (const std::runtime_error & e) {
    libdnf5::throw_with_nested(RepoDownloadError(
        M_("Error checking if metalink \"{}\" is in sync for repository \"{}\""),
        repo.get_download_data().get_source_info().second,
        repo.get_download_data().config.get_id()));
}

// Use repomd to check whether our metadata are still current.
bool RepoDownloader::is_repomd_in_sync(Repo & repo, std::filesystem::path repomd) try {
    auto & download_data = repo.get_download_data();
    auto & logger = *(download_data.base)->get_logger();
    auto & config = download_data.config;

    auto same = utils::fs::have_files_same_content_noexcept(download_data.repomd_filename.c_str(), repomd.c_str());
    if (same)
        logger.debug("Sync check: repo \"{}\" in sync, repomd matches", config.get_id());
    else
        logger.trace("Sync check: failed for repo \"{}\", repomd mismatch", config.get_id());
    return same;
} catch (const std::runtime_error & e) {
    auto src = repo.get_download_data().get_source_info();
    libdnf5::throw_with_nested(RepoDownloadError(
        M_("Error checking if repomd ({}: \"{}\") is in sync for repository \"{}\""),
        src.first,
        src.second,
        repo.get_config().get_id()));
}

void RepoDownloader::load_local(DownloadData & download_data) try {
    auto & config = download_data.config;
    LibrepoHandle h(RepoDownloader::init_local_handle(download_data));

    if (config.get_repo_gpgcheck_option().get_value()) {
        auto pubringdir = download_data.pgp.get_keyring_dir();
        h.set_opt(LRO_GNUPGHOMEDIR, pubringdir.c_str());
    }
    auto result = h.perform();

    download_data.repomd_filename = libdnf5::utils::string::c_to_str(get_yum_repo(result)->repomd);

    // copy the mirrors out of the handle (handle_get_info() allocates new
    // space and passes ownership, so we copy twice in this case, as we want to
    // store a vector of strings)
    char ** lr_mirrors;
    h.get_info(LRI_MIRRORS, &lr_mirrors);
    if (lr_mirrors) {
        for (auto mirror = lr_mirrors; *mirror; ++mirror) {
            download_data.mirrors.emplace_back(*mirror);
        }
    }
    g_strfreev(lr_mirrors);

    download_data.revision = libdnf5::utils::string::c_to_str(get_yum_repomd(result)->revision);

    GError * err_p{nullptr};
    // TODO(lukash) return time_t instead of converting to signed int
    download_data.max_timestamp = static_cast<int>(lr_yum_repomd_get_highest_timestamp(get_yum_repomd(result), &err_p));
    if (err_p != nullptr) {
        throw libdnf5::repo::LibrepoError(std::unique_ptr<GError>(err_p));
    }

    for (auto elem = get_yum_repomd(result)->content_tags; elem; elem = g_slist_next(elem)) {
        if (elem->data) {
            download_data.content_tags.emplace_back(static_cast<const char *>(elem->data));
        }
    }

    for (auto elem = get_yum_repomd(result)->distro_tags; elem; elem = g_slist_next(elem)) {
        if (elem->data) {
            auto distro_tag = static_cast<LrYumDistroTag *>(elem->data);
            if (distro_tag->tag) {
                download_data.distro_tags.emplace_back(distro_tag->cpeid, distro_tag->tag);
            }
        }
    }

    for (auto elem = get_yum_repomd(result)->records; elem; elem = g_slist_next(elem)) {
        if (elem->data) {
            auto rec = static_cast<LrYumRepoMdRecord *>(elem->data);
            download_data.metadata_locations.emplace_back(rec->type, rec->location_href);
        }
    }

    for (auto * elem = get_yum_repo(result)->paths; elem; elem = g_slist_next(elem)) {
        if (elem->data) {
            auto yumrepopath = static_cast<LrYumRepoPath *>(elem->data);
            download_data.metadata_paths[yumrepopath->type] = yumrepopath->path;
        }
    }
} catch (const std::runtime_error & e) {
    libdnf5::throw_with_nested(
        RepoDownloadError(M_("Error loading local metadata for repository \"{}\""), download_data.config.get_id()));
}

/// Returns a librepo handle, set as per the repo options.
/// Note that destdir is None, and the handle is cached.
// TODO(jrohel) The librepo handle callbacks are not set. If librepo itself downloads an extra file
//              (eg metalink) we won't know about it.
LibrepoHandle & RepoDownloader::get_cached_handle(Repo & repo) {
    auto & donwload_data = repo.get_download_data();
    if (!donwload_data.handle) {
        donwload_data.handle = RepoDownloader::init_remote_handle(repo, nullptr, true);
    }
    apply_http_headers(donwload_data, *donwload_data.handle);
    return *donwload_data.handle;
}

LibrepoHandle RepoDownloader::init_local_handle(const DownloadData & download_data) {
    LibrepoHandle h;

    common_handle_setup(h, download_data);

    std::string cachedir = download_data.config.get_cachedir();
    h.set_opt(LRO_DESTDIR, cachedir.c_str());
    const char * urls[] = {cachedir.c_str(), nullptr};
    h.set_opt(LRO_URLS, urls);
    h.set_opt(LRO_LOCAL, 1L);

    return h;
}

LibrepoHandle RepoDownloader::init_remote_handle(Repo & repo, const char * destdir, bool mirror_setup) {
    auto & download_data = repo.get_download_data();
    LibrepoHandle h;
    h.init_remote(download_data.config);

    RepoDownloader::common_handle_setup(h, download_data);

    apply_http_headers(download_data, h);

    h.set_opt(LRO_DESTDIR, destdir);

    auto & config = download_data.config;

    enum class Source { NONE, METALINK, MIRRORLIST } source{Source::NONE};
    std::string tmp;
    if (!config.get_metalink_option().empty() && !(tmp = config.get_metalink_option().get_value()).empty()) {
        source = Source::METALINK;
    } else if (!config.get_mirrorlist_option().empty() && !(tmp = config.get_mirrorlist_option().get_value()).empty()) {
        source = Source::MIRRORLIST;
    }
    if (source != Source::NONE) {
        if (mirror_setup) {
            if (source == Source::METALINK) {
                h.set_opt(LRO_METALINKURL, tmp.c_str());
            } else {
                h.set_opt(LRO_MIRRORLISTURL, tmp.c_str());
                // YUM-DNF compatibility hack. YUM guessed by content of keyword "metalink" if
                // mirrorlist is really mirrorlist or metalink)
                if (tmp.find("metalink") != tmp.npos)
                    h.set_opt(LRO_METALINKURL, tmp.c_str());
            }

            h.set_opt(LRO_FASTESTMIRROR, config.get_fastestmirror_option().get_value() ? 1L : 0L);

            auto fastest_mirror_cache_dir = config.get_basecachedir_option().get_value();
            if (fastest_mirror_cache_dir.back() != '/') {
                fastest_mirror_cache_dir.push_back('/');
            }
            fastest_mirror_cache_dir += "fastestmirror.cache";
            h.set_opt(LRO_FASTESTMIRRORCACHE, fastest_mirror_cache_dir.c_str());
        } else {
            // use already resolved mirror list
            std::vector<const char *> c_mirrors(download_data.mirrors.size() + 1, nullptr);
            str_vector_to_char_array(download_data.mirrors, c_mirrors.data());
            h.set_opt(LRO_URLS, c_mirrors.data());
        }
    } else if (!config.get_baseurl_option().get_value().empty()) {
        std::vector<const char *> urls(config.get_baseurl_option().get_value().size() + 1, nullptr);
        str_vector_to_char_array(config.get_baseurl_option().get_value(), urls.data());
        h.set_opt(LRO_URLS, urls.data());
    } else {
        throw RepoDownloadError(
            M_("No valid source (baseurl, mirrorlist or metalink) found for repository \"{}\""), config.get_id());
    }

    return h;
}

void RepoDownloader::configure_handle_dlist(LibrepoHandle & h, std::set<std::string> && optional_metadata) {
    if (optional_metadata.extract(libdnf5::METADATA_TYPE_ALL)) {
        h.set_opt(LRO_YUMDLIST, LR_RPMMD_FULL);
    } else {
        std::vector<const char *> dlist;
        dlist.push_back(MD_FILENAME_PRIMARY);
#ifdef WITH_MODULEMD
        dlist.push_back(MD_FILENAME_MODULES);
#endif
        if (optional_metadata.extract(libdnf5::METADATA_TYPE_FILELISTS)) {
            dlist.push_back(MD_FILENAME_FILELISTS);
        }
        if (optional_metadata.extract(libdnf5::METADATA_TYPE_OTHER)) {
            dlist.push_back(MD_FILENAME_OTHER);
        }
        if (optional_metadata.extract(libdnf5::METADATA_TYPE_PRESTO)) {
            dlist.push_back(MD_FILENAME_PRESTODELTA);
        }
        if (optional_metadata.extract(libdnf5::METADATA_TYPE_COMPS)) {
            dlist.push_back(MD_FILENAME_GROUP_GZ);
        }
        if (optional_metadata.extract(libdnf5::METADATA_TYPE_UPDATEINFO)) {
            dlist.push_back(MD_FILENAME_UPDATEINFO);
        }

        // download the rest metadata added by 3rd parties
        for (auto & item : optional_metadata) {
            // the appstream metadata is a "virtual" type, the list
            // of the types is read from the repomd file
            if (item.compare(libdnf5::METADATA_TYPE_APPSTREAM) != 0)
                dlist.push_back(item.c_str());
        }
        if (optional_metadata.extract(libdnf5::METADATA_TYPE_APPSTREAM)) {
            // ideally, the repomd.xml file should be read and every type matching is_appstream_metadata_type()
            // would be added from it, but the content is not known at this point and when it is known, then
            // it's too late, thus declare some "expected" types to be downloaded here
            dlist.push_back("appstream");
            dlist.push_back("appstream-icons");
            dlist.push_back("appstream-icons-48x48");
            dlist.push_back("appstream-icons-48x48@2");
            dlist.push_back("appstream-icons-64x64");
            dlist.push_back("appstream-icons-64x64@2");
            dlist.push_back("appstream-icons-128x128");
            dlist.push_back("appstream-icons-128x128@2");
            // consult the prefixes with the is_appstream_metadata_type()
            dlist.push_back("appdata");
            dlist.push_back("appdata-icons");
            dlist.push_back("appdata-icons-48x48");
            dlist.push_back("appdata-icons-48x48@2");
            dlist.push_back("appdata-icons-64x64");
            dlist.push_back("appdata-icons-64x64@2");
            dlist.push_back("appdata-icons-128x128");
            dlist.push_back("appdata-icons-128x128@2");
        }

        dlist.push_back(nullptr);
        h.set_opt(LRO_YUMDLIST, dlist.data());
    }
}

void RepoDownloader::common_handle_setup(LibrepoHandle & h, const DownloadData & download_data) {
    configure_handle_dlist(h, download_data.get_optional_metadata());
    h.set_opt(LRO_PRESERVETIME, static_cast<long>(download_data.preserve_remote_time));
    h.set_opt(LRO_REPOTYPE, LR_YUMREPO);
    h.set_opt(LRO_INTERRUPTIBLE, 1L);
    h.set_opt(LRO_GPGCHECK, download_data.config.get_repo_gpgcheck_option().get_value());
    h.set_opt(LRO_MAXMIRRORTRIES, static_cast<long>(download_data.max_mirror_tries));
    h.set_opt(LRO_MAXPARALLELDOWNLOADS, download_data.config.get_max_parallel_downloads_option().get_value());

    LrUrlVars * repomd_substs = nullptr;
    repomd_substs = lr_urlvars_set(repomd_substs, MD_FILENAME_GROUP_GZ, MD_FILENAME_GROUP);
    h.set_opt(LRO_YUMSLIST, repomd_substs);

    LrUrlVars * substs = nullptr;
    // Deprecated, we have direct access to base, therefore we don't need to set explicitly substitutions
    for (const auto & item : download_data.substitutions) {
        substs = lr_urlvars_set(substs, item.first.c_str(), item.second.c_str());
    }
    for (const auto & item : download_data.base->get_vars()->get_variables()) {
        substs = lr_urlvars_set(substs, item.first.c_str(), item.second.value.c_str());
    }
    h.set_opt(LRO_VARSUB, substs);

#ifdef LRO_SUPPORTS_CACHEDIR
    // If zchunk is enabled, set librepo cache dir
    if (download_data.config.get_main_config().get_zchunk_option().get_value()) {
        h.set_opt(LRO_CACHEDIR, download_data.config.get_basecachedir_option().get_value().c_str());
    }
#endif
}

void RepoDownloader::apply_http_headers(DownloadData & download_data, LibrepoHandle & handle) {
    if (download_data.http_headers.empty()) {
        handle.set_opt(LRO_HTTPHEADER, nullptr);
        return;
    }

    std::unique_ptr<char * [], std::function<void(char **)>> lr_headers {
        nullptr, [](char ** ptr) {
            for (auto item = ptr; *item != nullptr; ++item) {
                delete[] *item;
            }
            delete[] ptr;
        }
    };

    lr_headers.reset(new char * [download_data.http_headers.size() + 1] {});

    for (size_t i = 0; i < download_data.http_headers.size(); ++i) {
        const auto & header = download_data.http_headers[i];
        lr_headers[i] = new char[header.size() + 1];
        strcpy(lr_headers[i], header.c_str());
    }

    handle.set_opt(LRO_HTTPHEADER, lr_headers.get());
}

// COUNTME CONSTANTS
//
// width of the sliding time window (in seconds)
const int COUNTME_WINDOW = 7 * 24 * 60 * 60;  // 1 week
// starting point of the sliding time window relative to the UNIX epoch
// allows for aligning the window with a specific weekday
const int COUNTME_OFFSET = 345600;  // Monday (1970-01-05 00:00:00 UTC)
// estimated number of metalink requests sent over the window
// used to generate the probability distribution of counting events
const int COUNTME_BUDGET = 4;  // metadata_expire defaults to 2 days
// cookie file name
const std::string COUNTME_COOKIE = "countme";
// cookie file format version
const int COUNTME_VERSION = 0;
// longevity buckets that we report in the flag
// example: {A, B, C} defines 4 buckets [0, A), [A, B), [B, C), [C, infinity)
// where each letter represents a window step (starting from 0)
const std::array<const int, 3> COUNTME_BUCKETS = {{2, 5, 25}};

/// The countme flag will be added once (and only once) in every position of
/// a sliding time window (COUNTME_WINDOW) that starts at COUNTME_OFFSET and
/// moves along the time axis, by one length at a time, in such a way that
/// the current point in time always stays within:
///
/// UNIX epoch                    now
/// |                             |
/// |---*-----|-----|-----|-----[-*---]---> time
///     |                       ~~~~~~~
///     COUNTME_OFFSET          COUNTME_WINDOW
///
/// This is to align the time window with an absolute point in time rather
/// than the last counting event (which could facilitate tracking across
/// multiple such events).
///
/// In the below comments, the window's current position will be referred to
/// as "this window" for brevity.
void RepoDownloader::add_countme_flag(DownloadData & download_data, LibrepoHandle & handle) {
    auto & logger = *(download_data.base)->get_logger();
    auto & config = download_data.config;

    // Bail out if not counting or not running as root (since the persistdir is
    // only root-writable)
    if (!config.get_countme_option().get_value() || getuid() != 0)
        return;

    // Bail out if not a remote handle
    long local;
    handle.get_info(LRI_LOCAL, &local);
    if (local)
        return;

    // Bail out if no metalink or mirrorlist is defined
    auto & metalink = config.get_metalink_option();
    auto & mirrorlist = config.get_mirrorlist_option();
    if ((metalink.empty() || metalink.get_value().empty()) && (mirrorlist.empty() || mirrorlist.get_value().empty()))
        return;

    if (!std::filesystem::is_directory(config.get_persistdir())) {
        std::filesystem::create_directories(config.get_persistdir());
    }

    // Load the cookie
    std::filesystem::path file_path(config.get_persistdir());
    file_path /= COUNTME_COOKIE;

    int ver = COUNTME_VERSION;    // file format version (for future use)
    time_t epoch = 0;             // position of first observed window
    time_t win = COUNTME_OFFSET;  // position of last counted window
    int budget = -1;              // budget for this window (-1 = generate)
    // TODO(lukash) ideally replace with utils::fs::File (via adding scanf() support?),
    // once we are able to test this (using CI stack tests)
    std::ifstream(file_path) >> ver >> epoch >> win >> budget;

    // Bail out if the window has not advanced since
    time_t now = time(nullptr);
    time_t delta = now - win;
    if (delta < COUNTME_WINDOW) {
        logger.trace("countme: no event for repo \"{}\": window already counted", config.get_id());
        return;
    }

    // Evenly distribute the probability of the counting event over the first N
    // requests in this window (where N = COUNTME_BUDGET), by defining a random
    // "budget" of ordinary requests that we first have to spend.  This ensures
    // that no particular request is special and thus no privacy loss is
    // incurred by adding the flag within N requests.
    if (budget < 0) {
        std::random_device rd;
        std::default_random_engine gen(rd());
        std::uniform_int_distribution<int> dist(1, COUNTME_BUDGET);
        budget = dist(gen);
    }
    budget--;
    if (!budget) {
        // Budget exhausted, counting!

        // Compute the position of this window
        win = now - (delta % COUNTME_WINDOW);

        // Compute the epoch from this system's epoch or, if unknown, declare
        // this window as the epoch (unless stored in the cookie previously).
        time_t sysepoch = get_system_epoch();
        if (sysepoch)
            epoch = sysepoch - ((sysepoch - COUNTME_OFFSET) % COUNTME_WINDOW);
        if (!epoch)
            epoch = win;

        // Window step (0 at epoch)
        int64_t step = (win - epoch) / COUNTME_WINDOW;

        // Compute the bucket we are in
        uint32_t i;
        for (i = 0; i < COUNTME_BUCKETS.size(); ++i)
            if (step < COUNTME_BUCKETS[i])
                break;
        uint32_t bucket = i + 1;  // Buckets are numbered from 1

        // Set the flag
        std::string flag = "countme=" + std::to_string(bucket);
        handle.set_opt(LRO_ONETIMEFLAG, flag.c_str());
        logger.trace("countme: event triggered for repo \"{}\": bucket {}", config.get_id(), bucket);

        // Request a new budget
        budget = -1;
    } else {
        logger.trace("countme: no event for repo \"{}\": budget to spend: {}", config.get_id(), budget);
    }

    // Save the cookie
    utils::fs::File(file_path, "w").write(fmt::format("{} {} {} {}", COUNTME_VERSION, epoch, win, budget));
}


/* Returns this system's installation time ("epoch") as a UNIX timestamp.
 *
 * Uses the machine-id(5) file's mtime as a good-enough source of truth.  This
 * file is typically tied to the system's installation or first boot where it's
 * populated by an installer tool or init system, respectively, and is never
 * changed afterwards.
 *
 * Some systems, such as containers that don't run an init system, may have the
 * file missing, empty or uninitialized, in which case this function returns 0.
 */
time_t RepoDownloader::get_system_epoch() {
    std::string filename = "/etc/machine-id";
    std::string id;
    struct stat st;

    if (stat(filename.c_str(), &st) != 0 || !st.st_size)
        return 0;
    std::ifstream(filename) >> id;
    if (id == "uninitialized")
        return 0;

    return st.st_mtime;
}


}  // namespace libdnf5::repo
