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

#include "repo_downloader.hpp"
#include "temp_files_memory.hpp"
#include "utils/fs/utils.hpp"
#include "utils/url.hpp"

#include "libdnf5/base/base.hpp"
#include "libdnf5/common/exception.hpp"
#include "libdnf5/repo/download_callbacks.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 <algorithm>
#include <filesystem>


namespace std {

template <>
struct default_delete<LrPackageTarget> {
    void operator()(LrPackageTarget * ptr) noexcept { lr_packagetarget_free(ptr); }
};

}  // namespace std


namespace libdnf5::repo {

class PackageTarget {
public:
    PackageTarget(const libdnf5::rpm::Package & package, const std::string & destination, void * user_data)
        : package(package),
          destination(destination),
          user_data(user_data) {}

    ~PackageTarget() {
        if (need_call_end_callback) {
            if (auto * download_callbacks = package.get_base()->get_download_callbacks()) {
                download_callbacks->end(user_cb_data, DownloadCallbacks::TransferStatus::ERROR, "Interrupted");
            }
        }
    }

    libdnf5::rpm::Package package;
    std::string destination;
    void * user_data;
    void * user_cb_data{nullptr};
    bool need_call_end_callback{false};
    bool transfer_failed{false};
};

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

    auto * package_target = static_cast<PackageTarget *>(data);
    auto cb_status = static_cast<DownloadCallbacks::TransferStatus>(status);
    if (cb_status == DownloadCallbacks::TransferStatus::ERROR) {
        package_target->transfer_failed = true;
    }
    if (auto * download_callbacks = package_target->package.get_base()->get_download_callbacks()) {
        libdnf_assert(package_target->need_call_end_callback == true, "unexpected end_callback call");
        package_target->need_call_end_callback = false;
        return download_callbacks->end(package_target->user_cb_data, cb_status, msg);
    }
    return 0;
}

static int progress_callback(void * data, double total_to_download, double downloaded) {
    libdnf_assert(data != nullptr, "data in callback must be set");

    auto * package_target = static_cast<PackageTarget *>(data);
    if (auto * download_callbacks = package_target->package.get_base()->get_download_callbacks()) {
        return download_callbacks->progress(package_target->user_cb_data, total_to_download, downloaded);
    }
    return 0;
}

static int mirror_failure_callback(void * data, const char * msg, const char * url) {
    libdnf_assert(data != nullptr, "data in callback must be set");

    auto * package_target = static_cast<PackageTarget *>(data);
    if (auto * download_callbacks = package_target->package.get_base()->get_download_callbacks()) {
        return download_callbacks->mirror_failure(package_target->user_cb_data, msg, url, nullptr);
    }
    return 0;
}


class PackageDownloader::Impl {
public:
    Impl(const BaseWeakPtr & base) : base(base), fail_fast(true), resume(true) {}

private:
    friend PackageDownloader;

    BaseWeakPtr base;

    std::vector<PackageTarget> targets;
    std::optional<bool> keep_packages;
    bool fail_fast;
    bool resume;
};


PackageDownloader::PackageDownloader(const BaseWeakPtr & base) : p_impl(std::make_unique<Impl>(base)) {}
PackageDownloader::PackageDownloader(Base & base) : p_impl(std::make_unique<Impl>(base.get_weak_ptr())) {}
PackageDownloader::~PackageDownloader() = default;


void PackageDownloader::add(const libdnf5::rpm::Package & package, void * user_data) {
    add(package, package.get_repo()->get_packages_download_dir(), user_data);
}


void PackageDownloader::add(const libdnf5::rpm::Package & package, const std::string & destination, void * user_data) {
    p_impl->targets.emplace_back(package, destination, user_data);
}


void PackageDownloader::download() try {
    // Reset failure state from any previous download() call so retries
    // (repeated download() calls) report fresh per-package results.
    for (auto & target : p_impl->targets) {
        target.transfer_failed = false;
    }
    if (p_impl->targets.empty()) {
        return;
    }

    auto & config = p_impl->base->get_config();
    auto use_cache_only = config.get_cacheonly_option().get_value() == "all";
    GError * err{nullptr};

    std::vector<std::unique_ptr<LrPackageTarget>> lr_targets;
    lr_targets.reserve(p_impl->targets.size());
    std::vector<PackageTarget *> local_targets;
    for (auto & pkg_target : p_impl->targets) {
        if (use_cache_only && !pkg_target.package.is_available_locally()) {
            throw RepoCacheonlyError(
                M_("Cannot download the \"{0}\" package, cacheonly option is activated."),
                pkg_target.package.get_nevra());
        }

        std::filesystem::create_directories(pkg_target.destination);

        if (auto * download_callbacks = pkg_target.package.get_base()->get_download_callbacks()) {
            pkg_target.user_cb_data = download_callbacks->add_new_download(
                pkg_target.user_data,
                pkg_target.package.get_full_nevra().c_str(),
                static_cast<double>(pkg_target.package.get_download_size()));
            pkg_target.need_call_end_callback = true;
        }

        if (pkg_target.package.is_available_locally()) {
            // First only gather all local package targets, we cannot handle them right away because
            // the download callbacks are not fully initialized (they don't contain all required downloads).
            // This could cause problems for example when the first added pkg_target is local and we called
            // the `end` callback right here the callbacks might think that they are finished.
            local_targets.push_back(&pkg_target);
            continue;
        }

        // Disable fastest_mirror callbacks for package downloading for now
        // TODO(amatej): There is a problem in the API, libdnf5::repo::DownloadCallbacks::fastest_mirror interface
        //               expects user_cb_data created by libdnf5::repo::DownloadCallbacks::add_new_download, these
        //               are specific for each package download.
        //               However librepo API allows setting fastest mirror callback data only per librepo handle and
        //               we reuse one handle for all packages from given repository.
        //               To resolve this we either have to create separate handle for each package (could be expensive)
        //               or enhance LrPackageTarget with fastestmirror callback data (breaks librepo ABI).
        auto & cached_handle = RepoDownloader::get_cached_handle(*pkg_target.package.get_repo());
        cached_handle.set_opt(LRO_FASTESTMIRRORCB, NULL);
        cached_handle.set_opt(LRO_FASTESTMIRRORDATA, NULL);

        std::string encoded_location = pkg_target.package.get_location();
        if (encoded_location.find("://") == std::string::npos) {
            // TODO(mblaha): Here we rely on the fact that createrepo_c does not
            // URL encode the package location. If we introduce URL encoding in
            // createrepo_c in the future, we should also introduce a respective
            // repository config option to let libdnf5 know whether the location
            // is encoded to set the proper preserve_already_encoded flag value.
            encoded_location = libdnf5::utils::url::url_path_encode(encoded_location, false);
        }
        auto * lr_target = lr_packagetarget_new_v3(
            cached_handle.get(),
            encoded_location.c_str(),
            pkg_target.destination.c_str(),
            static_cast<LrChecksumType>(pkg_target.package.get_checksum().get_type()),
            pkg_target.package.get_checksum().get_checksum().c_str(),
            static_cast<int64_t>(pkg_target.package.get_download_size()),
            pkg_target.package.get_baseurl().empty() ? nullptr : pkg_target.package.get_baseurl().c_str(),
            p_impl->resume,
            progress_callback,
            &pkg_target,
            end_callback,
            mirror_failure_callback,
            0,
            0,
            &err);

        if (!lr_target) {
            throw LibrepoError(std::unique_ptr<GError>(err));
        }

        lr_targets.emplace_back(lr_target);
    }

    for (auto * local_pkg_target : local_targets) {
        // Copy local packages to their destination directories, reflinking when
        // the filesystem supports copy-on-write.
        std::filesystem::path source = local_pkg_target->package.get_package_path();
        std::filesystem::path destination = local_pkg_target->destination / source.filename();
        std::error_code ec;
        const bool same_file = std::filesystem::equivalent(source, destination, ec);
        if (!same_file) {
            utils::fs::reflink_or_copy(source, destination, ec);
        }
        if (ec) {
            local_pkg_target->transfer_failed = true;
        }
        if (auto * download_callbacks = local_pkg_target->package.get_base()->get_download_callbacks()) {
            std::string msg;
            DownloadCallbacks::TransferStatus status;
            if (ec) {
                status = DownloadCallbacks::TransferStatus::ERROR;
                msg = ec.message();
            } else {
                status = DownloadCallbacks::TransferStatus::ALREADYEXISTS;
                if (same_file) {
                    msg = _("Already downloaded");
                } else {
                    msg = fmt::format("Copied from {}", source.string());
                }
            }
            local_pkg_target->need_call_end_callback = false;
            download_callbacks->end(local_pkg_target->user_cb_data, status, msg.c_str());
        }
    }

    // Adding items to the end of GSList is slow. We go from the back and add items to the beginning.
    GSList * list{nullptr};
    for (auto it = lr_targets.rbegin(); it != lr_targets.rend(); ++it) {
        list = g_slist_prepend(list, it->get());
    }
    std::unique_ptr<GSList, decltype(&g_slist_free)> list_holder(list, &g_slist_free);

    LrPackageDownloadFlag flags = static_cast<LrPackageDownloadFlag>(0);
    if (p_impl->fail_fast) {
        flags = static_cast<LrPackageDownloadFlag>(flags | LR_PACKAGEDOWNLOAD_FAILFAST);
    }

    // Store file paths of packages we don't want to keep cached.
    auto removal_configured = !config.get_keepcache_option().get_value();
    auto removal_enforced = p_impl->keep_packages.has_value() && !p_impl->keep_packages.value();
    auto keep_enforced = p_impl->keep_packages.has_value() && p_impl->keep_packages.value();
    if (removal_enforced || (!keep_enforced && removal_configured)) {
        std::vector<std::string> package_paths;
        std::transform(
            p_impl->targets.begin(),
            p_impl->targets.end(),
            std::back_inserter(package_paths),
            [](const PackageTarget & target) {
                return std::filesystem::canonical(std::filesystem::path(target.destination)) /
                       std::filesystem::path(target.package.get_location()).filename();
            });

        auto & cachedir = config.get_cachedir_option().get_value();
        TempFilesMemory temp_files_memory(p_impl->base, cachedir);
        temp_files_memory.add_files(package_paths);
    }

    if (!lr_download_packages(list, flags, &err)) {
        throw LibrepoError(std::unique_ptr<GError>(err));
    }
} catch (const RepoCacheonlyError & e) {
    throw;
} catch (const std::runtime_error & e) {
    libdnf5::throw_with_nested(PackageDownloadError(M_("Failed to download packages")));
}

std::vector<libdnf5::rpm::Package> PackageDownloader::get_failed_packages() const {
    std::vector<libdnf5::rpm::Package> failed;
    for (const auto & target : p_impl->targets) {
        if (target.transfer_failed) {
            failed.push_back(target.package);
        }
    }
    return failed;
}

void PackageDownloader::set_fail_fast(bool value) {
    p_impl->fail_fast = value;
}

void PackageDownloader::set_resume(bool value) {
    p_impl->resume = value;
}

void PackageDownloader::force_keep_packages(bool value) {
    p_impl->keep_packages = value;
}

}  // namespace libdnf5::repo
