// Copyright Contributors to the DNF5 project
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "libdnf5/repo/repo_config_override.hpp"

#include "libdnf5/base/base.hpp"
#include "libdnf5/common/exception.hpp"
#include "libdnf5/conf/config_main.hpp"
#include "libdnf5/conf/config_parser.hpp"
#include "libdnf5/conf/const.hpp"
#include "libdnf5/repo/config_repo.hpp"
#include "libdnf5/repo/repo_errors.hpp"
#include "libdnf5/utils/bgettext/bgettext-mark-domain.h"

#include <cerrno>
#include <filesystem>
#include <map>
#include <set>
#include <string>
#include <system_error>

namespace fs = std::filesystem;

namespace libdnf5::repo {

class RepoConfigOverride::Impl {
public:
    explicit Impl(const BaseWeakPtr & base) : base(base) {}

private:
    friend RepoConfigOverride;

    BaseWeakPtr base;
};

RepoConfigOverride::RepoConfigOverride(libdnf5::Base & base) : p_impl(new Impl(base.get_weak_ptr())) {}
RepoConfigOverride::~RepoConfigOverride() = default;

RepoConfigOverride::RepoConfigOverride(const RepoConfigOverride & src) = default;
RepoConfigOverride & RepoConfigOverride::operator=(const RepoConfigOverride & src) = default;

RepoConfigOverride::RepoConfigOverride(RepoConfigOverride && src) noexcept = default;
RepoConfigOverride & RepoConfigOverride::operator=(RepoConfigOverride && src) noexcept = default;

fs::path RepoConfigOverride::get_override_file_path() const {
    const auto & config = p_impl->base->get_config();
    fs::path repos_override_dir_path{REPOS_OVERRIDE_DIR};
    const bool use_installroot_config{!config.get_use_host_config_option().get_value()};
    if (use_installroot_config) {
        const fs::path installroot_path{config.get_installroot_option().get_value()};
        repos_override_dir_path = installroot_path / repos_override_dir_path.relative_path();
    }
    return repos_override_dir_path / USER_REPOS_OVERRIDE_FILENAME;
}

void RepoConfigOverride::save(
    const std::map<std::string, std::map<std::string, std::string>> & overrides,
    const std::map<std::string, std::set<std::string>> & removals) {
    // Validate that the options being set are known repository options with
    // syntactically valid values.
    ConfigMain tmp_config;
    ConfigRepo tmp_repo_conf{tmp_config, "temporary_to_check_repository_options"};
    for (const auto & [repo_id, options] : overrides) {
        for (const auto & [key, value] : options) {
            try {
                tmp_repo_conf.opt_binds().at(key).new_string(Option::Priority::RUNTIME, value);
            } catch (const Error & e) {
                throw RepoConfigOverrideError(
                    M_("Cannot set option \"{}\" for repository \"{}\": {}"), key, repo_id, std::string(e.what()));
            }
        }
    }

    const auto override_file_path = get_override_file_path();
    const auto repos_override_dir_path = override_file_path.parent_path();

    if (!fs::exists(repos_override_dir_path)) {
        try {
            fs::create_directories(repos_override_dir_path);
        } catch (const fs::filesystem_error & e) {
            throw FileSystemError(
                e.code().value(),
                repos_override_dir_path,
                M_("Cannot create directory \"{}\": {}"),
                repos_override_dir_path.string(),
                e.code().message());
        }
    }

    ConfigParser parser;
    const bool file_exists = fs::exists(override_file_path);
    if (file_exists) {
        parser.read(override_file_path);
    }

    parser.get_header() =
        "# Generated by libdnf5.\n# Do not modify this file manually, use dnf5 config-manager instead.\n";

    bool changed = false;

    for (const auto & [repo_id, options] : overrides) {
        if (!parser.has_section(repo_id)) {
            parser.add_section(repo_id);
            changed = true;
        }
        for (const auto & [key, value] : options) {
            if (!parser.has_option(repo_id, key) || parser.get_value(repo_id, key) != value) {
                parser.set_value(repo_id, key, value, "");
                changed = true;
            }
        }
    }

    for (const auto & [repo_id, keys] : removals) {
        if (parser.has_section(repo_id)) {
            for (const auto & key : keys) {
                changed |= parser.remove_option(repo_id, key);
            }
            if (parser.get_data().at(repo_id).empty()) {
                parser.remove_section(repo_id);
                changed = true;
            }
        }
    }

    if (!changed) {
        return;
    }

    try {
        parser.write(override_file_path, false);
    } catch (const std::system_error & e) {
        throw FileSystemError(
            e.code().value(),
            override_file_path,
            M_("Cannot write to file \"{}\": {}"),
            override_file_path.string(),
            e.code().message());
    } catch (const std::exception & e) {
        throw FileSystemError(
            errno ? errno : EIO,
            override_file_path,
            M_("Cannot write to file \"{}\": {}"),
            override_file_path.string(),
            std::string(e.what()));
    }

    if (!file_exists) {
        try {
            fs::permissions(
                override_file_path,
                fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::others_read);
        } catch (const fs::filesystem_error & e) {
            throw FileSystemError(
                e.code().value(),
                override_file_path,
                M_("Cannot set permissions on file \"{}\": {}"),
                override_file_path.string(),
                e.code().message());
        }
    }
}

}  // namespace libdnf5::repo
