// Copyright Contributors to the DNF5 project.
// Copyright Contributors to the libdnf project.
// SPDX-License-Identifier: GPL-2.0-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 General Public License as published by
// the Free Software Foundation, either version 2 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with libdnf.  If not, see <https://www.gnu.org/licenses/>.

#include "setopt.hpp"

#include "shared.hpp"

#include <libdnf5/conf/const.hpp>
#include <libdnf5/repo/repo_config_override.hpp>
#include <libdnf5/utils/bgettext/bgettext-mark-domain.h>

#include <filesystem>

namespace dnf5 {

using namespace libdnf5;

void ConfigManagerSetOptCommand::set_argument_parser() {
    auto & ctx = get_context();
    auto & parser = ctx.get_argument_parser();

    auto & cmd = *get_argument_parser_command();
    cmd.set_description("Set configuration and repositories options");

    auto opts_vals =
        parser.add_new_positional_arg("optvals", cli::ArgumentParser::PositionalArg::AT_LEAST_ONE, nullptr, nullptr);
    opts_vals->set_description("List of options with values. Format: \"[REPO_ID.]option=value\"");
    opts_vals->set_parse_hook_func([this](
                                       [[maybe_unused]] cli::ArgumentParser::PositionalArg * arg,
                                       int argc,
                                       const char * const argv[]) {
        for (int i = 0; i < argc; ++i) {
            auto value = argv[i];
            const auto * const val = strchr(value + 1, '=');
            if (!val) {
                throw cli::ArgumentParserError(
                    M_("{}: Badly formatted argument value \"{}\""), std::string{"optval"}, std::string{value});
            }
            std::string key{value, val};
            std::string key_value{val + 1};
            auto dot_pos = key.rfind('.');
            if (dot_pos != std::string::npos) {
                if (dot_pos == key.size() - 1) {
                    throw cli::ArgumentParserError(
                        M_("{}: Badly formatted argument value: Last key character cannot be '.': {}"),
                        std::string{"optval"},
                        std::string{value});
                }

                // Save the repository option for later processing (solving glob pattern, writing to file).
                auto repo_id = key.substr(0, dot_pos);
                if (repo_id.empty()) {
                    throw cli::ArgumentParserError(
                        M_("{}: Empty repository id is not allowed: {}"), std::string{"optval"}, std::string{value});
                }
                auto repo_key = key.substr(dot_pos + 1);

                // Test if the repository option is known and can be set.
                try {
                    tmp_repo_conf.opt_binds().at(repo_key).new_string(Option::Priority::COMMANDLINE, key_value);
                } catch (const Error & ex) {
                    throw ConfigManagerError(
                        M_("Cannot set repository option \"{}\": {}"), std::string{value}, std::string{ex.what()});
                }

                const auto [it, inserted] = in_repos_setopts[repo_id].insert({repo_key, key_value});
                if (!inserted) {
                    if (it->second != key_value) {
                        throw ConfigManagerError(
                            M_("Sets the \"{}\" option of the repository \"{}\" again with a different value: \"{}\" "
                               "!= \"{}\""),
                            repo_key,
                            repo_id,
                            it->second,
                            key_value);
                    }
                }
            } else {
                // Test if the global option is known and can be set.
                try {
                    tmp_config.opt_binds().at(key).new_string(Option::Priority::COMMANDLINE, key_value);
                } catch (const Error & ex) {
                    throw ConfigManagerError(
                        M_("Cannot set option: \"{}\": {}"), std::string{value}, std::string(ex.what()));
                }

                // Save the global option for later writing to a file.
                const auto [it, inserted] = main_setopts.insert({key, key_value});
                if (!inserted) {
                    if (it->second != key_value) {
                        throw ConfigManagerError(
                            M_("Sets the \"{}\" option again with a different value: \"{}\" != \"{}\""),
                            key,
                            it->second,
                            key_value);
                    }
                }
            }
        }
        return true;
    });
    cmd.register_positional_arg(opts_vals);

    auto create_missing_dirs_opt = parser.add_new_named_arg("create-missing-dir");
    create_missing_dirs_opt->set_long_name("create-missing-dir");
    create_missing_dirs_opt->set_description("Allow to create missing directories");
    create_missing_dirs_opt->set_has_value(false);
    create_missing_dirs_opt->set_parse_hook_func([this](cli::ArgumentParser::NamedArg *, const char *, const char *) {
        create_missing_dirs = true;
        return true;
    });
    cmd.register_named_arg(create_missing_dirs_opt);
}


void ConfigManagerSetOptCommand::configure() {
    auto & ctx = get_context();
    const auto & config = ctx.get_base().get_config();

    auto repo_ids = load_existing_repo_ids(ctx);
    for (auto & [in_repo_id, repo_setopts] : in_repos_setopts) {
        auto filtered_repo_ids = filter_repo_ids(in_repo_id, repo_ids);
        if (filtered_repo_ids.empty()) {
            throw ConfigManagerError(M_("No matching repository to modify: {}"), in_repo_id);
        }
        for (const auto & repo_id : filtered_repo_ids) {
            for (const auto & [key, value] : repo_setopts) {
                // Save the repository option for later writing to a file.
                const auto [it, inserted] = matching_repos_setopts[repo_id].insert({key, value});
                if (!inserted) {
                    if (it->second != value) {
                        throw ConfigManagerError(
                            M_("Sets the \"{}\" option of the repository \"{}\" again with a different value: \"{}\" "
                               "!= \"{}\""),
                            key,
                            repo_id,
                            it->second,
                            value);
                    }
                }
            }
        }
    }

    // Write new and modify existing options in the main configuration file.
    if (!main_setopts.empty()) {
        ConfigParser parser;

        const auto & cfg_filepath = get_config_file_path(config);
        resolve_missing_dir(cfg_filepath.parent_path(), create_missing_dirs);

        const bool exists = std::filesystem::exists(cfg_filepath);
        if (exists) {
            parser.read(cfg_filepath);
        }

        modify_config(parser, "main", main_setopts);
        parser.write(cfg_filepath, false);
        if (!exists) {
            set_file_permissions(cfg_filepath);
        }
    }

    // Write new and modify existing options in the repositories overrides configuration file.
    if (!matching_repos_setopts.empty()) {
        libdnf5::repo::RepoConfigOverride repo_config_override(ctx.get_base());
        auto override_dir = repo_config_override.get_override_file_path().parent_path();
        resolve_missing_dir(override_dir, create_missing_dirs);
        repo_config_override.save(matching_repos_setopts);
    }
}

}  // namespace dnf5
