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

#include "dnf5/context.hpp"
#include "library.hpp"

#include <fmt/format.h>
#include <libdnf5/common/exception.hpp>

#include <filesystem>

namespace dnf5 {

// Support for Plugin in the shared library.
class PluginLibrary : public Plugin {
public:
    // Loads a shared library, finds symbols, and instantiates the plugin.
    explicit PluginLibrary(Context & context, const std::string & library_path);

    ~PluginLibrary();

private:
    using TGetApiVersionFunc = decltype(&dnf5_plugin_get_api_version);
    using TGetNameFunc = decltype(&dnf5_plugin_get_name);
    using TGetVersionFunc = decltype(&dnf5_plugin_get_version);
    using TNewInstanceFunc = decltype(&dnf5_plugin_new_instance);
    using TDeleteInstanceFunc = decltype(&dnf5_plugin_delete_instance);
    using TGetLastException = decltype(&dnf5_plugin_get_last_exception);
    TGetApiVersionFunc get_api_version{nullptr};
    TGetNameFunc get_name{nullptr};
    TGetVersionFunc get_version{nullptr};
    TNewInstanceFunc new_instance{nullptr};
    TDeleteInstanceFunc delete_instance{nullptr};
    TGetLastException get_last_exception{nullptr};
    utils::Library library;
};

PluginLibrary::PluginLibrary(Context & context, const std::string & library_path) : library(library_path) {
    get_api_version = reinterpret_cast<TGetApiVersionFunc>(library.get_address("dnf5_plugin_get_api_version"));
    get_name = reinterpret_cast<TGetNameFunc>(library.get_address("dnf5_plugin_get_name"));

    const auto & application_plugin_api_version = dnf5::get_plugin_api_version();
    const auto & plugin_api_version = get_api_version();
    if (plugin_api_version.major != application_plugin_api_version.major) {
        auto msg = fmt::format(
            "Unsupported plugin API combination. API version provided by plugin \"{}\" (\"{}\") is \"{}.{}\"."
            " API version in dnf5 is \"{}.{}\".",
            get_name(),
            library_path,
            plugin_api_version.major,
            plugin_api_version.minor,
            application_plugin_api_version.major,
            application_plugin_api_version.minor);
        throw std::runtime_error(msg);
    }

    get_version = reinterpret_cast<TGetVersionFunc>(library.get_address("dnf5_plugin_get_version"));
    new_instance = reinterpret_cast<TNewInstanceFunc>(library.get_address("dnf5_plugin_new_instance"));
    delete_instance = reinterpret_cast<TDeleteInstanceFunc>(library.get_address("dnf5_plugin_delete_instance"));

    try {
        get_last_exception = reinterpret_cast<TGetLastException>(library.get_address("dnf5_plugin_get_last_exception"));
    } catch (const std::runtime_error &) {
        // The original plugin API did not have the "dnf5_plugin_get_last_exception" function.
        // To maintain compatibility with older plugins, the "dnf5_plugin_get_last_exception" function is optional.
    }

    iplugin_instance = new_instance(dnf5::get_application_version(), context);
    if (!iplugin_instance) {
        auto & logger = *context.get_base().get_logger();
        std::runtime_error plugin_exception("Failed to create a dnf plugin instance");
        if (get_last_exception) {
            if (auto * last_exception = get_last_exception()) {
                try {
                    if (*last_exception) {
                        std::rethrow_exception(*last_exception);
                    }
                } catch (const std::exception & ex) {
                    *last_exception = nullptr;  // We no longer need to save the exception in the plugin.
                    logger.error("dnf5_plugin_new_instance: {}", ex.what());
                    libdnf5::throw_with_nested(std::move(plugin_exception));
                } catch (...) {
                    *last_exception = nullptr;  // We no longer need to save the exception in the plugin.
                    libdnf5::throw_with_nested(std::move(plugin_exception));
                }
            }
        }
        throw std::move(plugin_exception);
    }
}

PluginLibrary::~PluginLibrary() {
    finish();
    delete_instance(iplugin_instance);
    iplugin_instance = nullptr;
}

Plugins::~Plugins() {
    finish();
    for (auto plugin = plugins.rbegin(), stop = plugins.rend(); plugin != stop; ++plugin) {
        (*plugin).reset();
    }
}

void Plugins::register_plugin(std::unique_ptr<Plugin> && plugin) {
    auto logger = context->get_base().get_logger();
    auto * iplugin = plugin->get_iplugin();
    plugins.emplace_back(std::move(plugin));
    auto name = iplugin->get_name();
    auto version = iplugin->get_version();
    logger->info("Added dnf5 plugin \"{}\" version \"{}.{}.{}\"", name, version.major, version.minor, version.micro);
}

void Plugins::load_plugin(const std::string & file_path) {
    auto logger = context->get_base().get_logger();
    logger->debug("Loading plugin file=\"{}\"", file_path);
    auto plugin = std::make_unique<PluginLibrary>(*context, file_path);
    auto * iplugin = plugin->get_iplugin();
    plugins.emplace_back(std::move(plugin));
    auto name = iplugin->get_name();
    auto version = iplugin->get_version();
    logger->info(
        "Loaded dnf5 plugin \"{}\" (\"{}\") version \"{}.{}.{}\"",
        name,
        file_path,
        version.major,
        version.minor,
        version.micro);
}

void Plugins::load_plugins(const std::string & dir_path) {
    auto logger = context->get_base().get_logger();

    std::vector<std::filesystem::path> lib_paths;
    std::error_code ec;
    for (const auto & p : std::filesystem::directory_iterator(dir_path, ec)) {
        if ((p.is_regular_file() || p.is_symlink()) && p.path().extension() == ".so") {
            lib_paths.emplace_back(p.path());
        }
    }
    if (ec) {
        logger->warning("Cannot read dnf5 plugins directory \"{}\": {}", dir_path, ec.message());
        return;
    }
    std::sort(lib_paths.begin(), lib_paths.end());

    for (const auto & path : lib_paths) {
        try {
            load_plugin(path);
        } catch (const std::exception & ex) {
            logger->error("Cannot load dnf5 plugin \"{}\": {}", path.string(), ex.what());
            libdnf5::throw_with_nested(std::runtime_error("Cannot load dnf5 plugin: " + path.string()));
        }
    }
}

bool Plugins::init() {
    for (auto & plugin : plugins) {
        if (plugin->get_enabled()) {
            plugin->init();
        }
    }
    return true;
}

void Plugins::finish() noexcept {
    for (auto plugin = plugins.rbegin(), stop = plugins.rend(); plugin != stop; ++plugin) {
        if ((*plugin)->get_enabled()) {
            (*plugin)->finish();
        }
    }
}

}  // namespace dnf5
