// 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/repo/repo_errors.hpp"
constexpr const char * REPOID_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.:";

#include "download_data.hpp"
#include "repo_cache_private.hpp"
#include "repo_downloader.hpp"
#include "rpm/package_sack_impl.hpp"
#include "solv_repo.hpp"
#include "utils/string.hpp"

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

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

#include <fcntl.h>
#include <fmt/format.h>
#include <glib.h>
#include <solv/chksum.h>
#include <solv/repo.h>
#include <solv/util.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <utime.h>

#include <cerrno>
#include <cstring>
#include <ctime>
#include <filesystem>
#include <set>

namespace libdnf5::repo {

static void is_readable_rpm(const std::string & fn) {
    if (std::filesystem::path(fn).extension() != ".rpm") {
        throw RepoRpmError(M_("Failed to load RPM \"{}\": doesn't have the \".rpm\" extension"), fn);
    }

    if (access(fn.c_str(), R_OK) != 0) {
        throw RepoRpmError(M_("Failed to access RPM \"{}\": {}"), fn, std::string(strerror(errno)));
    }
}


static int64_t mtime(const char * filename) {
    struct stat st;
    stat(filename, &st);
    return st.st_mtime;
}

class Repo::Impl {
public:
    Impl(const BaseWeakPtr & base, const std::string & id, Repo::Type type);

private:
    friend Repo;
    libdnf5::BaseWeakPtr base;
    bool use_includes{false};
    std::string repo_file_path;
    Type type;
    ConfigRepo config;

    SyncStrategy sync_strategy{SyncStrategy::TRY_CACHE};
    bool expired{false};
    std::unique_ptr<SolvRepo> solv_repo;
    std::unique_ptr<repo::DownloadData> downloader;

    WeakPtrGuard<Repo, false> data_guard;
};

Repo::Impl::Impl(const BaseWeakPtr & base, const std::string & id, Repo::Type type)
    : base(base),
      type(type),
      config(base->get_config(), id),
      downloader(new DownloadData(base, config, type)) {}

Repo::Repo(const BaseWeakPtr & base, const std::string & id, Repo::Type type)
    : p_impl(std::make_unique<Impl>(base, id, type)) {
    if (type == Type::AVAILABLE) {
        auto idx = verify_id(id);
        if (idx != std::string::npos) {
            throw RepoError(M_("Invalid repository id \"{}\": unexpected character '{}'"), id, id[idx]);
        }
    }
    auto & cacheonly = base->get_config().get_cacheonly_option().get_value();
    if (cacheonly != "none") {
        p_impl->sync_strategy = SyncStrategy::ONLY_CACHE;
    }
}

Repo::Repo(Base & base, const std::string & id, Repo::Type type) : Repo(base.get_weak_ptr(), id, type) {}

Repo::~Repo() = default;

Repo::Type Repo::get_type() const noexcept {
    return p_impl->type;
}

void Repo::set_callbacks(std::unique_ptr<RepoCallbacks> && callbacks) {
    p_impl->downloader->set_callbacks(std::move(callbacks));
}

std::unique_ptr<libdnf5::repo::RepoCallbacks> & Repo::get_callbacks() {
    return p_impl->downloader->callbacks;
}

void Repo::set_user_data(void * user_data) noexcept {
    p_impl->downloader->set_user_data(user_data);
}

void * Repo::get_user_data() const noexcept {
    return p_impl->downloader->get_user_data();
}

std::string::size_type Repo::verify_id(const std::string & repo_id) {
    return repo_id.find_first_not_of(REPOID_CHARS);
}

void Repo::verify() const {
    if (p_impl->config.get_baseurl_option().empty() &&
        (p_impl->config.get_metalink_option().empty() || p_impl->config.get_metalink_option().get_value().empty()) &&
        (p_impl->config.get_mirrorlist_option().empty() ||
         p_impl->config.get_mirrorlist_option().get_value().empty())) {
        throw RepoError(
            M_("Repository \"{}\" has no source (baseurl, mirrorlist or metalink) set."), p_impl->config.get_id());
    }

    const auto & type = (p_impl->config).get_type_option().get_value();
    const char * supported_repo_types[]{"rpm-md", "rpm", "repomd", "rpmmd", "yum", "YUM"};
    if (!type.empty()) {
        for (auto supported : supported_repo_types) {
            if (type == supported) {
                return;
            }
        }
        throw RepoError(M_("Repository \"{}\" has unsupported type \"{}\", skipping."), p_impl->config.get_id(), type);
    }
}

ConfigRepo & Repo::get_config() noexcept {
    return p_impl->config;
}

const ConfigRepo & Repo::get_config() const noexcept {
    return p_impl->config;
}

std::string Repo::get_id() const noexcept {
    return p_impl->config.get_id();
}

void Repo::enable() {
    p_impl->config.get_enabled_option().set(Option::Priority::RUNTIME, true);
}

void Repo::disable() {
    p_impl->config.get_enabled_option().set(Option::Priority::RUNTIME, false);
}

bool Repo::is_enabled() const {
    return p_impl->config.get_enabled_option().get_value();
}

bool Repo::is_local() const {
    if ((!p_impl->config.get_metalink_option().empty() && !p_impl->config.get_metalink_option().get_value().empty()) ||
        (!p_impl->config.get_mirrorlist_option().empty() &&
         !p_impl->config.get_mirrorlist_option().get_value().empty())) {
        return false;
    }
    if (!p_impl->config.get_baseurl_option().get_value().empty() &&
        p_impl->config.get_baseurl_option().get_value()[0].compare(0, 7, "file://") == 0) {
        return true;
    }
    return false;
}

void Repo::read_metadata_cache() {
    p_impl->downloader->reset_loaded();
    RepoDownloader::load_local(*p_impl->downloader);
}

std::vector<std::pair<std::string, std::string>> Repo::get_appstream_metadata() const {
    return get_download_data().get_appstream_metadata();
}

bool Repo::is_in_sync() {
    RepoDownloader repo_downloader{};
    repo_downloader.add(*this, std::filesystem::temp_directory_path(), NULL);
    // If there is an error there should be only the one map key (this repo) in the returned
    // unordered map but iterate through all keys to be safe.
    const auto & [errors, in_sync] = repo_downloader.download_repos_descriptions();
    for (auto & [repo, errs] : errors) {
        if (!errs.empty()) {
            auto src = repo->get_download_data().get_source_info();
            throw libdnf5::repo::RepoDownloadError(
                M_("Failed to download metadata ({}: \"{}\") for repository \"{}\": {}"),
                src.first,
                src.second,
                repo->get_id(),
                libdnf5::utils::string::join(errs, ", "));
        }
    }
    return in_sync;
}


void Repo::download_metadata(const std::string & destdir) {
    RepoDownloader repo_downloader{};
    repo_downloader.add(*this, destdir, NULL);
    // If there is an error there should be only the one map key (this repo) in the returned
    // unordered map but iterate through all keys to be safe.
    for (auto & [repo, errs] : repo_downloader.download()) {
        if (!errs.empty()) {
            auto src = repo->get_download_data().get_source_info();
            throw libdnf5::repo::RepoDownloadError(
                M_("Failed to download metadata ({}: \"{}\") for repository \"{}\": {}"),
                src.first,
                src.second,
                repo->get_id(),
                libdnf5::utils::string::join(errs, ", "));
        }
    }
}

void Repo::load() {
    // Each repository can only be loaded once. This one has already loaded, so just return instantly
    if (is_loaded()) {
        return;
    }

    make_solv_repo();

    if (p_impl->type == Type::AVAILABLE) {
        load_available_repo();
    } else if (p_impl->type == Type::SYSTEM) {
        load_system_repo();
    }

    p_impl->solv_repo->set_needs_internalizing();
    p_impl->base->get_rpm_package_sack()->p_impl->invalidate_provides();
}

void Repo::add_libsolv_testcase(const std::string & path) {
    make_solv_repo();

    libdnf5::utils::fs::File testcase_file(path, "r", true);
    testcase_add_testtags(p_impl->solv_repo->repo, testcase_file.get(), 0);

    p_impl->solv_repo->set_needs_internalizing();
    p_impl->base->get_rpm_package_sack()->p_impl->invalidate_provides();
}

bool Repo::get_use_includes() const {
    return p_impl->use_includes;
}

void Repo::set_use_includes(bool enabled) {
    p_impl->use_includes = enabled;
}

int Repo::get_cost() const {
    return p_impl->config.get_cost_option().get_value();
}

void Repo::set_cost(int value, Option::Priority priority) {
    auto & conf_cost = p_impl->config.get_cost_option();
    conf_cost.set(priority, value);
    if (p_impl->solv_repo) {
        p_impl->solv_repo->set_subpriority(-conf_cost.get_value());
    }
}

int Repo::get_priority() const {
    return p_impl->config.get_priority_option().get_value();
}

void Repo::set_priority(int value, Option::Priority priority) {
    auto & conf_priority = p_impl->config.get_priority_option();
    conf_priority.set(priority, value);
    if (p_impl->solv_repo) {
        p_impl->solv_repo->set_priority(-conf_priority.get_value());
    }
}

int64_t Repo::get_age() const {
    return time(nullptr) - mtime(p_impl->downloader->get_metadata_path(RepoDownloader::MD_FILENAME_PRIMARY).c_str());
}

void Repo::expire() {
    p_impl->expired = true;
}

bool Repo::is_expired() const {
    if (p_impl->expired)
        // explicitly requested expired state
        return true;
    if (p_impl->config.get_metadata_expire_option().get_value() == -1)
        return false;
    return get_age() >= p_impl->config.get_metadata_expire_option().get_value();
}

int Repo::get_expires_in() const {
    return p_impl->config.get_metadata_expire_option().get_value() - static_cast<int>(get_age());
}

std::string Repo::get_metadata_path(const std::string & metadata_type) {
    return p_impl->downloader->get_metadata_path(metadata_type);
}

void Repo::set_max_mirror_tries(int max_mirror_tries) {
    p_impl->downloader->max_mirror_tries = max_mirror_tries;
}

int64_t Repo::get_timestamp() const {
    auto path = p_impl->downloader->get_metadata_path(RepoDownloader::MD_FILENAME_PRIMARY);
    if (!path.empty()) {
        return mtime(path.c_str());
    }
    return -1;
}

int Repo::get_max_timestamp() {
    return p_impl->downloader->max_timestamp;
}

void Repo::set_preserve_remote_time(bool preserve_remote_time) {
    p_impl->downloader->preserve_remote_time = preserve_remote_time;
}

bool Repo::get_preserve_remote_time() const {
    return p_impl->downloader->preserve_remote_time;
}

const std::vector<std::string> & Repo::get_content_tags() {
    return p_impl->downloader->content_tags;
}

const std::vector<std::pair<std::string, std::string>> & Repo::get_distro_tags() {
    return p_impl->downloader->distro_tags;
}

const std::vector<std::pair<std::string, std::string>> Repo::get_metadata_locations() const {
    return p_impl->downloader->metadata_locations;
}

std::string Repo::get_cachedir() const {
    return p_impl->config.get_cachedir();
}

std::string Repo::get_persistdir() const {
    return p_impl->config.get_persistdir();
}

const std::string & Repo::get_revision() const {
    return p_impl->downloader->revision;
}

void Repo::set_repo_file_path(const std::string & path) {
    p_impl->repo_file_path = path;
}

const std::string & Repo::get_repo_file_path() const noexcept {
    return p_impl->repo_file_path;
}

void Repo::set_sync_strategy(SyncStrategy strategy) {
    p_impl->sync_strategy = strategy;
}

Repo::SyncStrategy Repo::get_sync_strategy() const noexcept {
    return p_impl->sync_strategy;
}

//void Repo::download_url(const char * url, int fd) {
//    downloader->download_url(url, fd);
//}

void Repo::set_http_headers(const std::vector<std::string> & headers) {
    p_impl->downloader->http_headers = headers;
}

std::vector<std::string> Repo::get_http_headers() const {
    return p_impl->downloader->http_headers;
}

std::vector<std::string> Repo::get_mirrors() const {
    return p_impl->downloader->mirrors;
}

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


void Repo::make_solv_repo() {
    if (!p_impl->solv_repo) {
        p_impl->solv_repo.reset(new SolvRepo(p_impl->base, p_impl->config, this));

        // TODO(lukash) move the below to SolvRepo? Requires sharing Type
        if (p_impl->type == Type::SYSTEM) {
            pool_set_installed(*get_rpm_pool(p_impl->base), p_impl->solv_repo->repo);
            pool_set_installed(*get_comps_pool(p_impl->base), p_impl->solv_repo->comps_repo);
        }

        p_impl->solv_repo->set_priority(-get_priority());
        p_impl->solv_repo->set_subpriority(-get_cost());
    }
}


void Repo::load_available_repo() {
    auto primary_fn = p_impl->downloader->get_metadata_path(RepoDownloader::MD_FILENAME_PRIMARY);
    if (primary_fn.empty()) {
        throw RepoError(M_("Failed to load repository: \"primary\" data not present or in unsupported format"));
    }

    p_impl->solv_repo->load_repo_main(p_impl->downloader->repomd_filename, primary_fn);

    auto optional_metadata = p_impl->config.get_main_config().get_optional_metadata_types_option().get_value();
    const bool all_metadata = optional_metadata.contains(libdnf5::METADATA_TYPE_ALL);

    if (all_metadata || optional_metadata.contains(libdnf5::METADATA_TYPE_APPSTREAM)) {
        auto appstream_metadata = p_impl->downloader->get_appstream_metadata();
        for (auto & item : appstream_metadata) {
            p_impl->solv_repo->load_repo_ext(RepodataType::APPSTREAM, item.first, *p_impl->downloader.get());
        }
    }

    if (all_metadata || optional_metadata.contains(libdnf5::METADATA_TYPE_FILELISTS)) {
        p_impl->solv_repo->load_repo_ext(RepodataType::FILELISTS, *p_impl->downloader.get());
    }

    if (all_metadata || optional_metadata.contains(libdnf5::METADATA_TYPE_OTHER)) {
        p_impl->solv_repo->load_repo_ext(RepodataType::OTHER, *p_impl->downloader.get());
    }

    if (all_metadata || optional_metadata.contains(libdnf5::METADATA_TYPE_PRESTO)) {
        p_impl->solv_repo->load_repo_ext(RepodataType::PRESTO, *p_impl->downloader.get());
    }

    if (all_metadata || optional_metadata.contains(libdnf5::METADATA_TYPE_UPDATEINFO)) {
        p_impl->solv_repo->load_repo_ext(RepodataType::UPDATEINFO, *p_impl->downloader.get());
    }

    if (all_metadata || optional_metadata.contains(libdnf5::METADATA_TYPE_COMPS)) {
        p_impl->solv_repo->load_repo_ext(RepodataType::COMPS, *p_impl->downloader.get());
    }

    // Load module metadata
#ifdef WITH_MODULEMD
    auto & logger = *p_impl->base->get_logger();

    std::string ext_fn = p_impl->downloader->get_metadata_path(RepoDownloader::MD_FILENAME_MODULES);

    if (ext_fn.empty()) {
        logger.debug(
            "No {} metadata available for repo {}", RepoDownloader::MD_FILENAME_MODULES, p_impl->config.get_id());
        return;
    }

    logger.debug(
        "Loading {} extension for repo {} from \"{}\"",
        RepoDownloader::MD_FILENAME_MODULES,
        p_impl->config.get_id(),
        ext_fn);
    auto yaml_content = libdnf5::utils::fs::File(ext_fn, "r", true).read();
    p_impl->base->get_module_sack()->add(yaml_content, p_impl->config.get_id());
#endif
}


// TODO(jkolarik): currently all metadata are loaded for system repo, maybe we want to have more control about that
void Repo::load_system_repo() {
    p_impl->solv_repo->load_system_repo();
    p_impl->solv_repo->load_system_repo_ext(RepodataType::COMPS);
}

void Repo::add_xml_comps(const std::string & path) {
    if (std::filesystem::path(path).extension() != ".xml") {
        throw RepoCompsError(M_("Failed to load comps \"{}\": doesn't have the \".xml\" extension"), path);
    }

    make_solv_repo();

    if (!get_solv_repo().read_group_solvable_from_xml(path)) {
        throw RepoCompsError(
            M_("Failed to load xml Comps \"{}\": {}"), path, std::string(pool_errstr(p_impl->solv_repo->repo->pool)));
    }
    p_impl->base->get_rpm_package_sack()->p_impl->invalidate_provides();
}

rpm::Package Repo::add_rpm_package(const std::string & path, bool with_hdrid) {
    is_readable_rpm(path);

    make_solv_repo();

    int flags = REPO_REUSE_REPODATA | REPO_NO_INTERNALIZE;
    if (with_hdrid) {
        flags |= RPM_ADD_WITH_HDRID | RPM_ADD_WITH_SHA256SUM;
    }

    Id new_id = repo_add_rpm(p_impl->solv_repo->repo, path.c_str(), flags);
    if (new_id == 0) {
        throw RepoRpmError(
            M_("Failed to load RPM \"{}\": {}"), path, std::string(pool_errstr(p_impl->solv_repo->repo->pool)));
    }

    p_impl->solv_repo->set_needs_internalizing();
    p_impl->base->get_rpm_package_sack()->p_impl->invalidate_provides();

    return rpm::Package(p_impl->base, rpm::PackageId(new_id));
}


void Repo::internalize() {
    if (p_impl->solv_repo) {
        p_impl->solv_repo->internalize();
    }
}


bool Repo::clone_root_metadata() {
    auto & logger = *p_impl->base->get_logger();

    auto repo_cachedir = p_impl->config.get_cachedir();
    auto base_cachedir = p_impl->config.get_basecachedir_option().get_value();
    auto system_cachedir = p_impl->config.get_main_config().get_system_cachedir_option().get_value();

    // return fast if the system_cachedir does not exist or is equivalent to base_cachedir
    std::error_code ec;
    if (!std::filesystem::exists(system_cachedir, ec) ||
        std::filesystem::equivalent(base_cachedir, system_cachedir, ec)) {
        return false;
    }

    auto base_path_pos = repo_cachedir.find(base_cachedir);
    libdnf_assert(
        base_path_pos != std::string::npos,
        "Repo cachedir \"{}\" doesn't contain base cachedir \"{}\"",
        repo_cachedir,
        base_cachedir);

    auto root_repo_cachedir = repo_cachedir;
    root_repo_cachedir.replace(base_path_pos, base_cachedir.size(), system_cachedir);

    auto root_repodata_cachedir = std::filesystem::path(root_repo_cachedir) / CACHE_METADATA_DIR;
    try {
        if (!std::filesystem::exists(root_repodata_cachedir)) {
            return false;
        }
    } catch (const std::filesystem::filesystem_error & e) {
        logger.debug("Error when checking root repodata at \"{}\" : \"{}\"", root_repodata_cachedir.c_str(), e.what());
        return false;
    }

    auto repo_cache = RepoCache(p_impl->base, repo_cachedir);
    repo_cache.remove_metadata();
    repo_cache.remove_solv_files();

    auto repodata_cachedir = std::filesystem::path(repo_cachedir) / CACHE_METADATA_DIR;
    auto root_repodata_metalink = std::filesystem::path(root_repo_cachedir) / CACHE_METALINK_FILE;
    auto root_repodata_mirrorlist = std::filesystem::path(root_repo_cachedir) / CACHE_MIRRORLIST_FILE;
    try {
        std::filesystem::create_directories(repodata_cachedir);
        std::filesystem::copy(root_repodata_cachedir, repodata_cachedir);
        if (std::filesystem::exists(root_repodata_metalink)) {
            std::filesystem::copy(root_repodata_metalink, repo_cachedir);
        }
        if (std::filesystem::exists(root_repodata_mirrorlist)) {
            std::filesystem::copy(root_repodata_mirrorlist, repo_cachedir);
        }
    } catch (const std::filesystem::filesystem_error & e) {
        logger.debug(
            "Error when cloning root repodata from \"{}\" to \"{}\" : \"{}\"",
            root_repodata_cachedir.c_str(),
            repodata_cachedir.c_str(),
            e.what());
        repo_cache.remove_metadata();
        return false;
    }

    auto root_solv_cachedir = std::filesystem::path(root_repo_cachedir) / CACHE_SOLV_FILES_DIR;
    auto solv_cachedir = std::filesystem::path(repo_cachedir) / CACHE_SOLV_FILES_DIR;
    try {
        if (std::filesystem::exists(root_solv_cachedir)) {
            std::filesystem::create_directories(solv_cachedir);
            std::filesystem::copy(root_solv_cachedir, solv_cachedir);
        }
    } catch (const std::filesystem::filesystem_error & e) {
        logger.debug(
            "Error when cloning root solv data from \"{}\" to \"{}\" : \"{}\"",
            root_solv_cachedir.c_str(),
            solv_cachedir.c_str(),
            e.what());
        repo_cache.remove_solv_files();
    }

    return true;
}


void Repo::recompute_expired() {
    if (p_impl->expired) {
        return;
    }

    if (RepoCache(p_impl->base, get_cachedir()).is_attribute(RepoCache::ATTRIBUTE_EXPIRED)) {
        p_impl->expired = true;
        return;
    }

    if (p_impl->config.get_metadata_expire_option().get_value() == -1) {
        return;
    }

    if (p_impl->config.get_main_config().get_check_config_file_age_option().get_value() &&
        !p_impl->repo_file_path.empty() &&
        mtime(p_impl->repo_file_path.c_str()) >
            mtime(p_impl->downloader->get_metadata_path(RepoDownloader::MD_FILENAME_PRIMARY).c_str())) {
        p_impl->expired = true;
    } else {
        p_impl->expired = get_age() >= p_impl->config.get_metadata_expire_option().get_value();
    }
}

std::string Repo::type_to_string(Type type) {
    switch (type) {
        case Type::AVAILABLE:
            return "available";
        case Type::COMMANDLINE:
            return "commandline";
        case Type::SYSTEM:
            return "system";
    }
    return "";
}

libdnf5::repo::RepoWeakPtr Repo::get_weak_ptr() {
    return {this, &p_impl->data_guard};
}

DownloadData & Repo::get_download_data() const {
    return *p_impl->downloader;
}

bool Repo::is_loaded() const {
    return p_impl->solv_repo.get();
}

SolvRepo & Repo::get_solv_repo() const {
    libdnf_user_assert(p_impl->solv_repo, "repo must be loaded to access solv_repo");
    return *p_impl->solv_repo;
}

void Repo::mark_fresh() {
    p_impl->expired = false;
}

std::filesystem::path Repo::get_packages_download_dir() const {
    auto destdir_option = p_impl->base->get_config().get_destdir_option();
    if (destdir_option.empty()) {
        return std::filesystem::path(get_cachedir()) / "packages";
    } else {
        return destdir_option.get_value();
    }
}

}  // namespace libdnf5::repo
