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

#include "output/argument_parser.hpp"
#include "utils/string.hpp"

#include <fmt/format.h>
#include <libdnf5/common/exception.hpp>
#include <libdnf5/utils/bgettext/bgettext-lib.h>
#include <libdnf5/utils/bgettext/bgettext-mark-domain.h>

#include <cstring>
#include <iomanip>
#include <iostream>
#include <regex>
#include <set>
#include <utility>

namespace libdnf5::cli {

namespace {

std::size_t replace_all(std::string & inout, std::string_view what, std::string_view with) {
    std::size_t count = 0;
    for (std::string::size_type pos = 0; inout.npos != (pos = inout.find(what.data(), pos, what.length()));
         pos += with.length(), ++count) {
        inout.replace(pos, what.length(), with.data(), with.length());
    }
    return count;
}

// Function to replace occurrences of ${<index>} with strings from the array
std::string format_string(const std::string & format, int argc, const char * const argv[]) {
    const std::regex placeholder(R"(\$\{(\d+)\})");

    std::string formatted;

    // Search for all occurrences of ${<index>}
    auto begin = format.cbegin();
    auto end = format.cend();
    std::smatch match;
    while (std::regex_search(begin, end, match, placeholder)) {
        formatted.append(begin, match[0].first);  // Append text before the placeholder
        int index;
        try {
            index = std::stoi(match[1]);
        } catch (const std::invalid_argument & ex) {
            throw Error(
                M_("Misconfigured alias \"{}\": cannot convert \"{}\" to a number"),
                std::string(argv[0]),
                match[0].str());
        } catch (const std::out_of_range & ex) {
            index = -1;
        }
        if (index < 0 || index >= argc) {
            throw Error(
                M_("Misconfigured alias \"{}\": index \"{}\" out of range"), std::string(argv[0]), match[0].str());
        }
        formatted += argv[index];  // Replace placeholder with corresponding string
        begin = match[0].second;   // Continue after the last match
    }
    formatted.append(begin, end);  // Append the remaining text

    return formatted;
}

std::pair<BgettextMessage, std::string> get_conflict_arg_msg(const ArgumentParser::Argument * conflict_arg) {
    if (const auto * named_arg = dynamic_cast<const ArgumentParser::NamedArg *>(conflict_arg)) {
        std::string conflict;
        if (!named_arg->get_long_name().empty()) {
            conflict = "--" + named_arg->get_long_name();
        }
        if (!named_arg->get_long_name().empty() && named_arg->get_short_name() != '\0') {
            conflict += "/";
        }
        if (named_arg->get_short_name() != '\0') {
            conflict = std::string("-") + named_arg->get_short_name();
        }
        return {M_("\"{}\" not allowed together with named argument \"{}\""), conflict};
    } else if (dynamic_cast<const ArgumentParser::Command *>(conflict_arg)) {
        return {M_("\"{}\" not allowed in command \"{}\""), conflict_arg->get_id()};
    } else {
        return {M_("\"{}\" not allowed together with positional argument \"{}\""), conflict_arg->get_id()};
    }
}

}  // namespace

class ArgumentParser::ArgumentParserImpl {
public:
    void assert_root_command() { libdnf_assert(root_command != nullptr, "Root command is not set"); }

    std::vector<std::unique_ptr<Command>> cmds;
    std::vector<std::unique_ptr<NamedArg>> named_args;
    std::vector<std::unique_ptr<PositionalArg>> pos_args;
    std::vector<std::unique_ptr<Group>> groups;
    std::vector<std::unique_ptr<std::vector<Argument *>>> conflict_args_groups;
    std::vector<std::unique_ptr<libdnf5::Option>> values_init;
    std::vector<std::unique_ptr<std::vector<std::unique_ptr<libdnf5::Option>>>> values;
    Command * root_command{nullptr};
    Command * selected_command{nullptr};
    bool inherit_named_args{false};
    const char * const * complete_arg_ptr{nullptr};
    bool complete_add_description{true};  // Indicates whether to add a description to the suggested arguments
};


// =============== ArgumentParser::Group methods ===============

ArgumentParser::Group::~Group() = default;

void ArgumentParser::Group::set_header(std::string header) noexcept {
    this->header = std::move(header);
}

const std::string & ArgumentParser::Group::get_id() const noexcept {
    return id;
}

const std::string & ArgumentParser::Group::get_header() const noexcept {
    return header;
}

void ArgumentParser::Group::register_argument(Argument * arg) {
    for (auto * item : arguments) {
        if (item->id == arg->id) {
            throw ArgumentParserGroupArgumentIdRegisteredError(
                M_("Argument id \"{}\" already registered in group \"{}\""), arg->id, id);
        }
    }
    arguments.push_back(arg);
}

const std::vector<ArgumentParser::Argument *> & ArgumentParser::Group::get_arguments() const noexcept {
    return arguments;
}

ArgumentParser::Group::Group(const std::string & id) : id(id) {}


// =============== ArgumentParser::Argument methods ===============

ArgumentParser::Argument::~Argument() = default;

void ArgumentParser::Argument::set_long_description(std::string descr) noexcept {
    long_description = std::move(descr);
}

void ArgumentParser::Argument::set_description(std::string descr) noexcept {
    description = std::move(descr);
}

void ArgumentParser::Argument::set_conflict_arguments(std::vector<Argument *> * args) noexcept {
    conflict_args = args;
}

void ArgumentParser::Argument::add_conflict_argument(ArgumentParser::Argument & conflict_arg) {
    auto add_conflict = [](Argument & arg, Argument & conflict_arg) {
        if (arg.conflict_args) {
            auto it = std::find(arg.conflict_args->begin(), arg.conflict_args->end(), &conflict_arg);
            if (it == arg.conflict_args->end()) {
                arg.conflict_args->push_back(&conflict_arg);
            }
        } else {
            // Creates a new group of conflict arguments if it does not exist
            arg.conflict_args = arg.owner.add_conflict_args_group(std::make_unique<std::vector<Argument *>>());
            arg.conflict_args->push_back(&conflict_arg);
        }
    };

    // Adds forward conflict
    add_conflict(*this, conflict_arg);

    // Adds reverse (back) conflict
    add_conflict(conflict_arg, *this);
}

void ArgumentParser::Argument::add_conflict_arguments_from_another(Argument & src_arg) {
    // Return immediately if there are no conflicting arguments in the source argument or it is a self-assignment.
    if (!src_arg.conflict_args || &src_arg == this) {
        return;
    }

    for (std::size_t idx = 0; idx < src_arg.conflict_args->size(); ++idx) {
        auto * conflict_arg = (*src_arg.conflict_args)[idx];

        // Don't add a conflict to the argument itself and to the source argument.
        if (conflict_arg == this || conflict_arg == &src_arg) {
            continue;
        }

        add_conflict_argument(*conflict_arg);
    }
}

const std::string & ArgumentParser::Argument::get_id() const noexcept {
    return id;
}

const std::string & ArgumentParser::Argument::get_long_description() const {
    return long_description;
}

const std::string & ArgumentParser::Argument::get_description() const {
    return description;
}

std::vector<ArgumentParser::Argument *> * ArgumentParser::Argument::get_conflict_arguments() noexcept {
    return conflict_args;
}

int ArgumentParser::Argument::get_parse_count() const noexcept {
    return parse_count;
}

void ArgumentParser::Argument::reset_parse_count() noexcept {
    parse_count = 0;
}

ArgumentParser::Argument * ArgumentParser::Argument::get_conflict_argument() const noexcept {
    if (conflict_args) {
        for (auto * arg : *conflict_args) {
            if (arg != this && arg->get_parse_count() > 0) {
                return arg;
            }
        }
    }
    return nullptr;
}

void ArgumentParser::Argument::set_complete(bool complete) noexcept {
    this->complete = complete;
}

bool ArgumentParser::Argument::get_complete() const noexcept {
    return complete;
}

void ArgumentParser::Argument::help() const noexcept {}

ArgumentParser & ArgumentParser::Argument::get_argument_parser() const noexcept {
    return owner;
}

void ArgumentParser::Argument::set_user_data(ArgumentParserUserData * user_data) noexcept {
    this->user_data = user_data;
}

ArgumentParserUserData * ArgumentParser::Argument::get_user_data() const noexcept {
    return user_data;
}

ArgumentParser::Argument::Argument(ArgumentParser & owner, std::string id) : owner(owner) {
    if (id.find('.') != id.npos) {
        throw ArgumentParserArgumentInvalidIdError(M_("Invalid character '.' in argument id \"{}\""), id);
    }
    this->id = std::move(id);
}


// =============== ArgumentParser::PositionalArg methods ===============

ArgumentParser::PositionalArg::~PositionalArg() = default;

int ArgumentParser::PositionalArg::get_nvals() const noexcept {
    return nvals;
}

void ArgumentParser::PositionalArg::set_store_value(bool enable) {
    libdnf_assert(!enable || values, "set_store_value(true) was called but storage array \"values\" is not set");

    store_value = enable;
}

bool ArgumentParser::PositionalArg::get_store_value() const noexcept {
    return store_value;
}

std::vector<std::unique_ptr<libdnf5::Option>> * ArgumentParser::PositionalArg::get_linked_values() noexcept {
    return values;
}

void ArgumentParser::PositionalArg::set_parse_hook_func(ParseHookFunc && func) {
    parse_hook = std::move(func);
}

const ArgumentParser::PositionalArg::ParseHookFunc & ArgumentParser::PositionalArg::get_parse_hook_func()
    const noexcept {
    return parse_hook;
}

void ArgumentParser::PositionalArg::set_complete_hook_func(CompleteHookFunc && func) {
    complete_hook = std::move(func);
}

const ArgumentParser::PositionalArg::CompleteHookFunc & ArgumentParser::PositionalArg::get_complete_hook_func()
    const noexcept {
    return complete_hook;
}

void ArgumentParser::PositionalArg::set_nrepeats(int nrepeats) {
    this->nrepeats = nrepeats;
}

int ArgumentParser::PositionalArg::get_nrepeats() const noexcept {
    return nrepeats;
}

ArgumentParser::PositionalArg::PositionalArg(
    ArgumentParser & owner, const std::string & id, std::vector<std::unique_ptr<libdnf5::Option>> * values)
    : Argument(owner, id),
      init_value(nullptr),
      values(values) {
    libdnf_assert(values && !values->empty(), "\"values\" constructor parameter cannot be nullptr or an empty vector");

    nvals = static_cast<int>(values->size());
}

ArgumentParser::PositionalArg::PositionalArg(
    ArgumentParser & owner,
    const std::string & id,
    int nvals,
    libdnf5::Option * init_value,
    std::vector<std::unique_ptr<libdnf5::Option>> * values)
    : Argument(owner, id),
      nvals(nvals),
      init_value(init_value),
      values(values),
      store_value(values) {
    libdnf_assert(!values || init_value, "\"init_value\" constructor parameter cannot be nullptr if \"value\" is set");
}

int ArgumentParser::PositionalArg::parse(
    const char * option, int argc, const char * const argv[], bool stop_at_named_args) {
    if (const auto * arg = get_conflict_argument()) {
        auto [fmt_message, conflict_option] = get_conflict_arg_msg(arg);
        throw ArgumentParserConflictingArgumentsError(fmt_message, std::string(option), conflict_option);
    }
    if (owner.p_impl->complete_arg_ptr) {
        int usable_argc = 1;
        while (usable_argc < argc && (!stop_at_named_args || *argv[usable_argc] != '-')) {
            ++usable_argc;
        }
        auto count = static_cast<size_t>(nvals > 0 ? nvals : (nvals == OPTIONAL ? 1 : usable_argc));
        for (size_t i = 0; i < count; ++i) {
            if (owner.p_impl->complete_arg_ptr == argv + i) {
                if (get_complete() && complete_hook) {
                    auto result = complete_hook(argv[i]);
                    if (result.size() == 1) {
                        if (result[0] != option) {
                            std::cout << result[0] << std::endl;
                        }
                    } else {
                        for (const auto & line : result) {
                            std::cout << line << std::endl;
                        }
                    }
                }
                return static_cast<int>(count);
            }
        }
    }
    if (argc < nvals) {
        throw ArgumentParserPositionalArgFewValuesError(M_("Too few values for positional argument \"{}\""), this->id);
    }
    for (int i = 1; i < nvals; ++i) {
        if (stop_at_named_args && *argv[i] == '-') {
            throw ArgumentParserPositionalArgFewValuesError(
                M_("Too few values for positional argument \"{}\""), this->id);
        }
    }
    int usable_argc = 1;
    if (nvals <= 0) {
        while (usable_argc < argc && (!stop_at_named_args || *argv[usable_argc] != '-')) {
            ++usable_argc;
        }
    }
    auto count = static_cast<size_t>(nvals > 0 ? nvals : (nvals == OPTIONAL ? 1 : usable_argc));
    if (store_value) {
        for (size_t i = 0; i < count; ++i) {
            if (values->size() <= i) {
                values->push_back(std::unique_ptr<libdnf5::Option>((*init_value).clone()));
            }
            (*values)[i]->set(libdnf5::Option::Priority::COMMANDLINE, argv[i]);
        }
    }
    ++parse_count;
    if (parse_hook) {
        parse_hook(this, static_cast<int>(count), argv);
    }
    return static_cast<int>(count);
}


// =============== ArgumentParser::NamedArg methods ===============

ArgumentParser::NamedArg::~NamedArg() = default;

void ArgumentParser::NamedArg::set_long_name(std::string long_name) noexcept {
    this->long_name = std::move(long_name);
}

void ArgumentParser::NamedArg::set_short_name(char short_name) {
    this->short_name = short_name;
}

void ArgumentParser::NamedArg::set_has_value(bool has_value) {
    this->has_value = has_value;
}

void ArgumentParser::NamedArg::link_value(libdnf5::Option * value) {
    this->value = value;
}

void ArgumentParser::NamedArg::set_const_value(std::string const_value) noexcept {
    const_val = std::move(const_value);
}

const std::string & ArgumentParser::NamedArg::get_long_name() const noexcept {
    return long_name;
}

char ArgumentParser::NamedArg::get_short_name() const noexcept {
    return short_name;
}

bool ArgumentParser::NamedArg::get_has_value() const noexcept {
    return has_value;
}

const std::string & ArgumentParser::NamedArg::get_const_value() const noexcept {
    return const_val;
}

libdnf5::Option * ArgumentParser::NamedArg::get_linked_value() noexcept {
    return value;
}

const libdnf5::Option * ArgumentParser::NamedArg::get_linked_value() const noexcept {
    return value;
}

void ArgumentParser::NamedArg::set_store_value(bool enable) noexcept {
    store_value = enable;
}

bool ArgumentParser::NamedArg::get_store_value() const noexcept {
    return store_value;
}

void ArgumentParser::NamedArg::set_parse_hook_func(ParseHookFunc && func) {
    parse_hook = std::move(func);
}

const ArgumentParser::NamedArg::ParseHookFunc & ArgumentParser::NamedArg::get_parse_hook_func() const noexcept {
    return parse_hook;
}

void ArgumentParser::NamedArg::set_arg_value_help(std::string text) {
    arg_value_help = std::move(text);
}

const std::string & ArgumentParser::NamedArg::get_arg_value_help() const noexcept {
    return arg_value_help;
}

libdnf5::cli::ArgumentParser::NamedArg * ArgumentParser::NamedArg::add_alias(
    const std::string & id,
    const std::string & long_name,
    char short_name,
    libdnf5::cli::ArgumentParser::Group * group) {
    auto * alias = get_argument_parser().add_new_named_arg(id);
    alias->set_long_name(long_name);
    alias->set_short_name(short_name);

    // Set description
    std::string descr;
    if (get_short_name() != '\0') {
        descr = std::string("'-") + get_short_name() + "'";
        if (!get_long_name().empty()) {
            descr += ", ";
        }
    }
    if (!get_long_name().empty()) {
        descr += "'--" + get_long_name() + "'";
    }
    alias->set_description(fmt::format("Alias for {}", descr));

    // Copy from source argument
    alias->set_has_value(get_has_value());
    alias->link_value(get_linked_value());
    alias->set_store_value(get_store_value());
    alias->set_const_value(get_const_value());
    alias->set_arg_value_help(get_arg_value_help());
    alias->set_parse_hook_func(libdnf5::cli::ArgumentParser::NamedArg::ParseHookFunc(get_parse_hook_func()));

    // Do not offer aliases in completion
    alias->set_complete(false);

    if (group) {
        group->register_argument(alias);
    }

    alias->add_conflict_arguments_from_another(*this);

    return alias;
}

void ArgumentParser::NamedArg::attach_named_arg(const std::string & id_path, const std::string & value) {
    attached_named_args.push_back({id_path, value});
}

ArgumentParser::NamedArg::NamedArg(ArgumentParser & owner, const std::string & id) : Argument(owner, id) {}

int ArgumentParser::NamedArg::parse_long(const char * option, int argc, const char * const argv[]) {
    if (const auto * arg = get_conflict_argument()) {
        auto [fmt_message, conflict_option] = get_conflict_arg_msg(arg);
        throw ArgumentParserConflictingArgumentsError(fmt_message, "--" + std::string(option), conflict_option);
    }
    const char * arg_value;
    int consumed_args;
    const auto * assign_ptr = strchr(option, '=');
    if (has_value) {
        if (assign_ptr) {
            arg_value = assign_ptr + 1;
            consumed_args = 1;
        } else {
            if (argc < 2) {
                throw ArgumentParserNamedArgMissingValueError(
                    M_("Missing value for named argument \"--{}\""), std::string(option));
            }
            arg_value = argv[1];
            consumed_args = 2;
        }
    } else {
        if (assign_ptr) {
            throw ArgumentParserNamedArgValueNotExpectedError(
                M_("Unexpected value for named argument \"--{}\""), std::string(option));
        }
        arg_value = const_val.c_str();
        consumed_args = 1;
    }
    if (store_value && value) {
        value->set(libdnf5::Option::Priority::COMMANDLINE, arg_value);
    }
    ++parse_count;
    if (parse_hook) {
        parse_hook(this, option, arg_value);
    }

    // Invoke the attached named arguments
    for (auto & target_named_arg : attached_named_args) {
        auto & target_arg = owner.get_named_arg(target_named_arg.id_path, false);
        std::string long_name = assign_ptr ? std::string(option, assign_ptr) : option;
        if (target_arg.get_has_value()) {
            std::string target_arg_val = target_named_arg.value;
            replace_all(target_arg_val, "${}", arg_value);
            long_name += "=" + target_arg_val;
        }
        const char * const args[1] = {long_name.c_str()};
        target_arg.parse_long(long_name.c_str(), 1, args);
    }

    return consumed_args;
}

int ArgumentParser::NamedArg::parse_short(const char * option, int argc, const char * const argv[]) {
    if (const auto * arg = get_conflict_argument()) {
        auto [fmt_message, conflict_option] = get_conflict_arg_msg(arg);
        throw ArgumentParserConflictingArgumentsError(fmt_message, '-' + std::string(option), conflict_option);
    }
    const char * arg_value;
    int consumed_args;
    if (has_value) {
        if (option[1] != '\0') {
            arg_value = option + 1;
            consumed_args = 1;
        } else {
            if (argc < 2) {
                throw ArgumentParserNamedArgMissingValueError(M_("Missing value for named argument \"-{}\""), *option);
            }
            arg_value = argv[1];
            consumed_args = 2;
        }
    } else {
        arg_value = const_val.c_str();
        consumed_args =
            option[1] == '\0' ? 1 : 0;  // consume only if we are the last option in group, example of 3 options: -cvf
    }
    if (store_value && value) {
        value->set(libdnf5::Option::Priority::COMMANDLINE, arg_value);
    }
    ++parse_count;
    if (parse_hook) {
        parse_hook(this, option, arg_value);
    }

    // Invoke the attached named arguments
    for (auto & target_named_arg : attached_named_args) {
        auto & target_arg = owner.get_named_arg(target_named_arg.id_path, false);
        const char * args[2];
        int args_count = 1;
        std::string target_arg_val;
        if (target_arg.get_has_value()) {
            target_arg_val = target_named_arg.value;
            replace_all(target_arg_val, "${}", arg_value);
            args[args_count++] = target_arg_val.c_str();
        }
        char short_name[] = {option[0], '\0'};
        args[0] = short_name;
        target_arg.parse_short(short_name, args_count, args);
    }

    return consumed_args;
}


// =============== ArgumentParser::Command methods ===============

template <class Arg>
static Arg * find_arg(const std::vector<Arg *> & args, const std::string & id) {
    for (auto * item : args) {
        if (item->get_id() == id) {
            return item;
        }
    }
    return nullptr;
}

static std::string get_named_arg_names(const ArgumentParser::NamedArg * arg) {
    std::string arg_names;
    if (arg->get_short_name() != '\0') {
        arg_names = std::string("-") + arg->get_short_name();
        if (arg->get_has_value()) {
            arg_names += arg->get_arg_value_help().empty() ? " VALUE" : ' ' + arg->get_arg_value_help();
        }
        if (!arg->get_long_name().empty()) {
            arg_names += ", ";
        }
    }
    if (!arg->get_long_name().empty()) {
        arg_names += "--" + arg->get_long_name();
        if (arg->get_has_value()) {
            arg_names += arg->get_arg_value_help().empty() ? "=VALUE" : '=' + arg->get_arg_value_help();
        }
    }
    return arg_names;
}

ArgumentParser::Command::~Command() = default;

void ArgumentParser::Command::help() const noexcept {
    auto & cmds = get_commands();
    auto & named_args = get_named_args();
    auto & pos_args = get_positional_args();
    auto & groups = get_groups();

    libdnf5::cli::output::Usage usage_output;

    // generate usage
    std::string usage;
    for (const auto * cmd = this; cmd; cmd = cmd->parent) {
        std::string tmp = cmd->get_id();
        if (!cmd->get_named_args().empty()) {
            tmp += cmd->parent ? " [OPTIONS]" : " [GLOBAL OPTIONS]";
        }
        if (!cmd->get_positional_args().empty()) {
            tmp += " [ARGUMENTS]";
        }
        usage = tmp + ' ' + usage;
    }
    if (!cmds.empty()) {
        usage += "<COMMAND> ...";
    }

    // print usage
    auto * usage_header = usage_output.add_header(_("Usage:"));
    for (auto & line : libdnf5::utils::string::split(usage, "\n")) {
        usage_output.add_line(line, usage_header);
    }

    // print description
    if (!long_description.empty()) {
        usage_output.add_newline();
        auto * desc_header = usage_output.add_header(_("Description:"));
        for (auto & line : libdnf5::utils::string::split(long_description, "\n")) {
            usage_output.add_line(line, desc_header);
        }
    }

    libdnf5::cli::output::Help help;

    // Arguments used in groups are not printed as ungrouped.
    std::set<Argument *> args_used_in_groups;

    if (!commands_help_header.empty() && !cmds.empty()) {
        const std::set<Argument *> cmds_set(cmds.begin(), cmds.end());

        // Processing commands in groups.
        for (auto * grp : groups) {
            // we'll initialize the header line later when the first line with an argument is added
            struct libscols_line * header{nullptr};
            for (auto * arg : grp->get_arguments()) {
                if (dynamic_cast<Command *>(arg) && cmds_set.count(arg) > 0) {
                    if (!header) {
                        help.add_newline();
                        header = help.add_header(grp->get_header());
                    }
                    help.add_line(arg->get_id(), arg->get_description(), header);
                    args_used_in_groups.insert(arg);
                }
            }
        }

        // gather commands that don't belong to any group
        std::vector<Command *> ungrouped_commands;
        for (auto * cmd : cmds) {
            if (args_used_in_groups.count(cmd) == 0) {
                ungrouped_commands.push_back(cmd);
            }
        }

        // print the commands that don't belong to any group
        // avoid printing `commands_help_header` if the list is empty
        if (!ungrouped_commands.empty()) {
            help.add_newline();
            struct libscols_line * header = help.add_header(commands_help_header);
            for (auto * arg : ungrouped_commands) {
                help.add_line(arg->get_id(), arg->get_description(), header);
            }
        }
    }

    if (!named_args_help_header.empty() && !named_args.empty()) {
        const std::set<Argument *> named_args_set(named_args.begin(), named_args.end());

        // Processing named arguments in groups.
        for (auto * grp : groups) {
            // we'll initialize the header line later when the first line with an argument is added
            struct libscols_line * header{nullptr};
            for (auto * arg : grp->get_arguments()) {
                auto * named_arg = dynamic_cast<NamedArg *>(arg);
                if (named_arg && named_args_set.count(arg) > 0) {
                    if (!header) {
                        help.add_newline();
                        header = help.add_header(grp->get_header());
                    }
                    help.add_line(get_named_arg_names(named_arg), arg->get_description(), header);
                    args_used_in_groups.insert(arg);
                }
            }
        }

        // gather named args that don't belong to any group
        std::vector<NamedArg *> ungrouped_named_args;
        for (auto * arg : named_args) {
            if (args_used_in_groups.count(static_cast<NamedArg *>(arg)) == 0) {
                ungrouped_named_args.push_back(arg);
            }
        }

        // print the named args that don't belong to any group
        // avoid printing `named_args_help_header` if the list is empty
        if (!ungrouped_named_args.empty()) {
            help.add_newline();
            struct libscols_line * header = help.add_header(named_args_help_header);
            for (const auto * arg : ungrouped_named_args) {
                help.add_line(get_named_arg_names(arg), arg->get_description(), header);
            }
        }
    }

    if (!positional_args_help_header.empty() && !pos_args.empty()) {
        const std::set<Argument *> pos_args_set(pos_args.begin(), pos_args.end());

        // Processing positional arguments in groups.
        for (auto * grp : groups) {
            // we'll initialize the header line later when the first line with an argument is added
            struct libscols_line * header{nullptr};
            for (auto * arg : grp->get_arguments()) {
                if (dynamic_cast<PositionalArg *>(arg) && pos_args_set.count(arg) > 0) {
                    if (!header) {
                        help.add_newline();
                        header = help.add_header(grp->get_header());
                    }
                    help.add_line(arg->get_id(), arg->get_description(), header);
                    args_used_in_groups.insert(arg);
                }
            }
        }

        // Processing ungrouped positional arguments.
        help.add_newline();
        struct libscols_line * header = help.add_header(positional_args_help_header);
        for (const auto * arg : pos_args) {
            help.add_line(arg->get_id(), arg->get_description(), header);
        }
    }

    usage_output.print();
    help.print();
}

void ArgumentParser::Command::set_commands_help_header(std::string text) noexcept {
    commands_help_header = std::move(text);
}

void ArgumentParser::Command::set_named_args_help_header(std::string text) noexcept {
    named_args_help_header = std::move(text);
}

void ArgumentParser::Command::set_positional_args_help_header(std::string text) noexcept {
    positional_args_help_header = std::move(text);
}

const std::string & ArgumentParser::Command::get_commands_help_header() const noexcept {
    return commands_help_header;
}

const std::string & ArgumentParser::Command::get_named_args_help_header() const noexcept {
    return named_args_help_header;
}

const std::string & ArgumentParser::Command::get_positional_args_help_header() const noexcept {
    return positional_args_help_header;
}

ArgumentParser::Command * ArgumentParser::Command::get_parent() const noexcept {
    return parent;
}

std::vector<std::string> ArgumentParser::Command::get_invocation() const noexcept {
    std::vector<std::string> invocation = {get_id()};
    if (parent) {
        auto parent_invocation = parent->get_invocation();
        invocation.insert(invocation.begin(), parent_invocation.begin(), parent_invocation.end());
    }
    return invocation;
}

ArgumentParser::Command::Command(ArgumentParser & owner, const std::string & id) : Argument(owner, id) {}

void ArgumentParser::Command::print_complete(
    const char * arg,
    std::vector<ArgumentParser::NamedArg *> named_args,
    size_t used_positional_arguments,
    bool stop_at_named_args) {
    const bool add_description = get_argument_parser().p_impl->complete_add_description;

    // Using the Help class to print the completion suggestions wits description, as it prints a table of two columns
    // which is also what we need here.
    libdnf5::cli::output::Help help;

    // Used to store a list of suggestions when a table is not needed (description is not added).
    std::vector<std::string> suggestions;

    std::string last;

    // After "--", do not suggest commands or named args, only positional arg completions.
    if (!stop_at_named_args) {
        if (used_positional_arguments < get_positional_args().size()) {
            auto pos_arg = get_positional_args()[used_positional_arguments];
            if (pos_arg->get_complete() && pos_arg->complete_hook) {
                auto result = pos_arg->complete_hook(arg);
                if (result.size() == 1) {
                    if (result[0] == arg) {
                        return;
                    }
                    std::cout << result[0] << std::endl;
                    return;
                }
                for (const auto & line : result) {
                    std::cout << line << std::endl;
                }
            }
        }
        return;
    }

    // Search for matching commands.
    if (arg[0] == '\0' || arg[0] != '-') {
        for (const auto * opt : get_commands()) {
            if (!opt->get_complete()) {
                continue;
            }
            auto & name = opt->get_id();
            if (name.compare(0, strlen(arg), arg) == 0) {
                if (add_description) {
                    help.add_line(name, '(' + opt->get_description() + ')', nullptr);
                } else {
                    suggestions.emplace_back(name);
                }
                last = name + ' ';
            }
        }

        // No matching command found. But there may be a positional argument.
        if (last.empty() && used_positional_arguments < get_positional_args().size()) {
            auto pos_arg = get_positional_args()[used_positional_arguments];
            if (pos_arg->get_complete() && pos_arg->complete_hook) {
                auto result = pos_arg->complete_hook(arg);
                if (result.size() == 1) {
                    if (result[0] == arg) {
                        return;
                    }
                    std::cout << result[0] << std::endl;
                    return;
                }
                for (const auto & line : result) {
                    std::cout << line << std::endl;
                }
            }
        }
    }

    // Search for matching named arguments.
    if (arg[0] == '-') {
        for (const auto * opt : named_args) {
            if (!opt->get_complete()) {
                continue;
            }
            if ((arg[1] == '\0' && opt->get_short_name() != '\0') ||
                (arg[1] == opt->get_short_name() && arg[2] == '\0')) {
                std::string name = std::string("-") + opt->get_short_name();
                if (add_description) {
                    std::string extended_name = name;
                    if (opt->get_has_value()) {
                        extended_name += opt->get_arg_value_help().empty() ? "VALUE" : opt->get_arg_value_help();
                    }
                    help.add_line(extended_name, '(' + opt->get_description() + ')', nullptr);
                } else {
                    suggestions.emplace_back(name);
                }
                last = name + ' ';
            }
            if (!opt->get_long_name().empty()) {
                std::string name = "--" + opt->get_long_name();
                if (name.compare(0, strlen(arg), arg) == 0) {
                    if (add_description) {
                        std::string extended_name = name;
                        if (opt->get_has_value()) {
                            extended_name += ' ';
                            extended_name += opt->get_arg_value_help().empty() ? "VALUE" : opt->get_arg_value_help();
                        }
                        help.add_line(extended_name, '(' + opt->get_description() + ')', nullptr);
                    } else {
                        suggestions.emplace_back(name);
                    }
                    last = name + ' ';
                }
            }
        }
    }

    // Prints the completed argument or suggestions if there is more than one solution.
    // Suggestions may be completed with a description.
    if (scols_table_get_nlines(help.get_table()) > 1) {
        help.print();
    } else if (suggestions.size() > 1) {
        for (const auto & suggestion : suggestions) {
            std::cout << suggestion << std::endl;
        }
    } else if (!last.empty() && last != arg) {
        std::cout << last << std::endl;
    }
}


// =============== ArgumentParser::CommandOrdinary methods ===============

ArgumentParser::CommandOrdinary::~CommandOrdinary() = default;


void ArgumentParser::CommandOrdinary::register_command(Command * cmd) {
    for (auto * item : cmds) {
        if (item->id == cmd->id) {
            throw ArgumentParserIdAlreadyRegisteredError(
                M_("Command id \"{}\" already registered for command \"{}\""), cmd->id, id);
        }
    }
    cmd->parent = this;
    cmds.push_back(cmd);
}

void ArgumentParser::CommandOrdinary::register_named_arg(NamedArg * arg) {
    for (auto * item : named_args) {
        if (item->id == arg->id) {
            throw ArgumentParserIdAlreadyRegisteredError(
                M_("Named argument id \"{}\" already registered for command \"{}\""), arg->id, id);
        }
    }
    named_args.push_back(arg);
}

void ArgumentParser::CommandOrdinary::register_positional_arg(PositionalArg * arg) {
    for (auto * item : pos_args) {
        if (item->id == arg->id) {
            throw ArgumentParserIdAlreadyRegisteredError(
                M_("Positional argument id \"{}\" already registered for command \"{}\""), arg->id, id);
        }
    }
    pos_args.push_back(arg);
}

void ArgumentParser::CommandOrdinary::register_group(Group * grp) {
    for (auto * item : groups) {
        if (item->id == grp->id) {
            throw ArgumentParserIdAlreadyRegisteredError(
                M_("Group id \"{}\" already registered for command \"{}\""), grp->id, id);
        }
    }
    groups.push_back(grp);
}

const std::vector<ArgumentParser::Command *> & ArgumentParser::CommandOrdinary::get_commands() const noexcept {
    return cmds;
}

const std::vector<ArgumentParser::NamedArg *> & ArgumentParser::CommandOrdinary::get_named_args() const noexcept {
    return named_args;
}

const std::vector<ArgumentParser::PositionalArg *> & ArgumentParser::CommandOrdinary::get_positional_args()
    const noexcept {
    return pos_args;
}

const std::vector<ArgumentParser::Group *> & ArgumentParser::CommandOrdinary::get_groups() const noexcept {
    return groups;
}

ArgumentParser::Command & ArgumentParser::CommandOrdinary::get_command(const std::string & id) const {
    if (auto ret = find_arg(cmds, id)) {
        return *ret;
    }
    throw ArgumentParserNotFoundError(M_("Command id \"{}\" does not contain subcommand with id \"{}\""), this->id, id);
}

ArgumentParser::NamedArg & ArgumentParser::CommandOrdinary::get_named_arg(const std::string & id) const {
    if (auto ret = find_arg(named_args, id)) {
        return *ret;
    }
    throw ArgumentParserNotFoundError(
        M_("Command id \"{}\" does not contain named argument with id \"{}\""), this->id, id);
}

ArgumentParser::PositionalArg & ArgumentParser::CommandOrdinary::get_positional_arg(const std::string & id) const {
    if (auto ret = find_arg(pos_args, id)) {
        return *ret;
    }
    throw ArgumentParserNotFoundError(
        M_("Command id \"{}\" does not contain positional argument with id \"{}\""), this->id, id);
}

ArgumentParser::Group & ArgumentParser::CommandOrdinary::get_group(const std::string & id) const {
    if (auto ret = find_arg(groups, id)) {
        return *ret;
    }
    throw ArgumentParserNotFoundError(M_("Command id \"{}\" does not contain group with id \"{}\""), this->id, id);
}

void ArgumentParser::CommandOrdinary::set_parse_hook_func(ParseHookFunc && func) {
    parse_hook = std::move(func);
}

const ArgumentParser::Command::ParseHookFunc & ArgumentParser::CommandOrdinary::get_parse_hook_func() const noexcept {
    return parse_hook;
}

void ArgumentParser::CommandOrdinary::parse(const char * option, int argc, const char * const argv[]) {
    std::vector<NamedArg *> extended_named_args;
    bool inherit_named_args_from_parent = owner.p_impl->inherit_named_args && parent;
    if (inherit_named_args_from_parent) {
        auto named_args_count = named_args.size();
        for (auto previous = parent; previous; previous = previous->parent) {
            named_args_count += previous->get_named_args().size();
        }
        extended_named_args.reserve(named_args_count);
        extended_named_args.insert(extended_named_args.end(), named_args.begin(), named_args.end());
        for (auto previous = parent; previous; previous = previous->parent) {
            auto & prev_named_args = previous->get_named_args();
            extended_named_args.insert(extended_named_args.end(), prev_named_args.begin(), prev_named_args.end());
        }
    }
    size_t used_positional_arguments = 0;
    int short_option_idx = 0;
    bool stop_at_named_args = true;
    for (int i = 1; i < argc;) {
        if (stop_at_named_args && std::strcmp(argv[i], "--") == 0 &&
            (!owner.p_impl->complete_arg_ptr || argv + i != owner.p_impl->complete_arg_ptr)) {
            stop_at_named_args = false;
            ++i;
            short_option_idx = 0;
            continue;
        }
        char unused_short_option = 0;
        if (owner.p_impl->complete_arg_ptr) {
            if (argv + i > owner.p_impl->complete_arg_ptr) {
                return;
            } else if (argv + i == owner.p_impl->complete_arg_ptr) {
                print_complete(
                    argv[i],
                    inherit_named_args_from_parent ? extended_named_args : named_args,
                    used_positional_arguments,
                    stop_at_named_args);
                return;
            }
        }
        bool used = false;
        const auto * tmp = argv[i];
        if (stop_at_named_args && *tmp == '-') {
            bool long_option = *++tmp == '-';
            if (long_option) {
                ++tmp;
            }
            const auto * assign_ptr = strchr(tmp, '=');
            for (auto * opt : (inherit_named_args_from_parent ? extended_named_args : named_args)) {
                if (long_option) {
                    if (!opt->get_long_name().empty() &&
                        (assign_ptr ? std::string(tmp).compare(
                                          0, static_cast<size_t>(assign_ptr - tmp), opt->get_long_name()) == 0
                                    : opt->get_long_name() == tmp)) {
                        i += opt->parse_long(tmp, argc - i, &argv[i]);
                        used = true;
                        break;
                    }
                } else {
                    if (opt->get_short_name() != '\0' && opt->get_short_name() == tmp[short_option_idx]) {
                        auto used_args = opt->parse_short(tmp + short_option_idx, argc - i, &argv[i]);
                        if (used_args > 0) {
                            i += used_args;
                            short_option_idx = 0;
                        } else {
                            ++short_option_idx;
                        }
                        used = true;
                        break;
                    }
                }
            }
            if (!used && !long_option) {
                unused_short_option = tmp[short_option_idx];
            }
        }
        if (!used && stop_at_named_args) {
            for (auto & cmd : cmds) {
                if (cmd->id == argv[i]) {
                    if (const auto * arg = get_conflict_argument()) {
                        auto [fmt_message, conflict_option] = get_conflict_arg_msg(arg);
                        throw ArgumentParserConflictingArgumentsError(
                            fmt_message, std::string(option), conflict_option);
                    }
                    // the last subcommand wins
                    owner.p_impl->selected_command = cmd;
                    cmd->parse(argv[i], argc - i, &argv[i]);
                    i = argc;
                    used = true;
                    // The subcommand processed the completion of the argument. There is no need to continue parsing.
                    if (owner.p_impl->complete_arg_ptr) {
                        return;
                    }
                    break;
                }
            }
        }
        if (!used && (!stop_at_named_args || *argv[i] != '-') && used_positional_arguments < pos_args.size()) {
            auto * pos_arg = pos_args[used_positional_arguments];
            i += pos_arg->parse(argv[i], argc - i, &argv[i], stop_at_named_args);
            auto nrepeats = pos_arg->get_nrepeats();
            if ((nrepeats > 0 && pos_arg->parse_count >= nrepeats) || nrepeats == PositionalArg::OPTIONAL) {
                ++used_positional_arguments;
            }
            used = true;
        }
        if (!used) {
            std::string unknown_argument;
            if (unused_short_option) {
                unknown_argument = std::string("-") + std::string(1, unused_short_option);
            } else {
                unknown_argument = std::string(argv[i]);
            }
            throw ArgumentParserUnknownArgumentError(
                std::string(id),
                unknown_argument,
                M_("Unknown argument \"{}\" for command \"{}\""),
                unknown_argument,
                id);
        }
    }
    ++parse_count;

    // Test that all required positional arguments are present.
    for (const auto * pos_arg : pos_args) {
        const auto nrepeats = pos_arg->get_nrepeats();
        if (nrepeats > 0 || nrepeats == PositionalArg::AT_LEAST_ONE) {
            const auto nvals = pos_arg->get_nvals();
            if (pos_arg->get_parse_count() == 0 && nvals != PositionalArg::UNLIMITED &&
                nvals != PositionalArg::OPTIONAL) {
                throw ArgumentParserMissingPositionalArgumentError(
                    M_("Missing positional argument \"{}\" for command \"{}\""), pos_arg->get_id(), id);
            }
        }
    }

    if (parse_hook) {
        parse_hook(this, option, argc, argv);
    }
}

ArgumentParser::CommandOrdinary::CommandOrdinary(ArgumentParser & owner, const std::string & id) : Command(owner, id) {}


// =============== ArgumentParser::CommandAlias methods ===============

ArgumentParser::CommandAlias::~CommandAlias() = default;

void ArgumentParser::CommandAlias::register_command(Command * cmd) {
    attached_command.register_command(cmd);
}

void ArgumentParser::CommandAlias::register_named_arg(NamedArg * arg) {
    attached_command.register_named_arg(arg);
}

void ArgumentParser::CommandAlias::register_positional_arg(PositionalArg * arg) {
    attached_command.register_positional_arg(arg);
}

void ArgumentParser::CommandAlias::register_group(Group * grp) {
    attached_command.register_group(grp);
}

std::vector<std::string> ArgumentParser::CommandAlias::get_invocation() const noexcept {
    return attached_command.get_invocation();
}

const std::vector<ArgumentParser::Command *> & ArgumentParser::CommandAlias::get_commands() const noexcept {
    return attached_command.get_commands();
}

const std::vector<ArgumentParser::NamedArg *> & ArgumentParser::CommandAlias::get_named_args() const noexcept {
    return attached_command.get_named_args();
}

const std::vector<ArgumentParser::PositionalArg *> & ArgumentParser::CommandAlias::get_positional_args()
    const noexcept {
    return attached_command.get_positional_args();
}

const std::vector<ArgumentParser::Group *> & ArgumentParser::CommandAlias::get_groups() const noexcept {
    return attached_command.get_groups();
}

ArgumentParser::Command & ArgumentParser::CommandAlias::get_command(const std::string & id) const {
    return attached_command.get_command(id);
}

ArgumentParser::NamedArg & ArgumentParser::CommandAlias::get_named_arg(const std::string & id) const {
    return attached_command.get_named_arg(id);
}

ArgumentParser::PositionalArg & ArgumentParser::CommandAlias::get_positional_arg(const std::string & id) const {
    return attached_command.get_positional_arg(id);
}

ArgumentParser::Group & ArgumentParser::CommandAlias::get_group(const std::string & id) const {
    return attached_command.get_group(id);
}

void ArgumentParser::CommandAlias::set_parse_hook_func(ParseHookFunc && func) {
    attached_command.set_parse_hook_func(std::move(func));
}

const ArgumentParser::Command::ParseHookFunc & ArgumentParser::CommandAlias::get_parse_hook_func() const noexcept {
    return attached_command.get_parse_hook_func();
}

void ArgumentParser::CommandAlias::parse(const char * option, int argc, const char * const argv[]) {
    const auto number_of_required_values = static_cast<int>(required_values.size());
    if (argc <= number_of_required_values) {
        throw ArgumentParserNamedArgMissingValueError(
            M_("Missing value for command alias \"{}\""), std::string(option));
    }

    // Invoke the attached named arguments
    for (auto & target_named_arg : attached_named_args) {
        auto & target_arg = owner.get_named_arg(target_named_arg.id_path, false);
        const char * args[2];
        int args_count = 1;
        std::string value;
        if (target_arg.get_has_value()) {
            value = format_string(target_named_arg.value, number_of_required_values + 1, argv);
            args[args_count++] = value.c_str();
        }
        args[0] = option;
        target_arg.parse_long(option, args_count, args);
    }

    auto & cmds = attached_command.get_commands();
    auto & named_args = attached_command.get_named_args();
    auto & pos_args = attached_command.get_positional_args();
    std::vector<NamedArg *> extended_named_args;
    bool inherit_named_args_from_parent = owner.p_impl->inherit_named_args && parent;
    if (inherit_named_args_from_parent) {
        auto named_args_count = named_args.size();
        for (auto previous = parent; previous; previous = previous->parent) {
            named_args_count += previous->get_named_args().size();
        }
        extended_named_args.reserve(named_args_count);
        extended_named_args.insert(extended_named_args.end(), named_args.begin(), named_args.end());
        for (auto previous = parent; previous; previous = previous->parent) {
            auto & prev_named_args = previous->get_named_args();
            extended_named_args.insert(extended_named_args.end(), prev_named_args.begin(), prev_named_args.end());
        }
    }
    size_t used_positional_arguments = 0;
    int short_option_idx = 0;
    bool stop_at_named_args = true;
    for (int i = 1 + number_of_required_values; i < argc;) {
        if (stop_at_named_args && std::strcmp(argv[i], "--") == 0 &&
            (!owner.p_impl->complete_arg_ptr || argv + i != owner.p_impl->complete_arg_ptr)) {
            stop_at_named_args = false;
            ++i;
            short_option_idx = 0;
            continue;
        }
        if (owner.p_impl->complete_arg_ptr) {
            if (argv + i > owner.p_impl->complete_arg_ptr) {
                return;
            } else if (argv + i == owner.p_impl->complete_arg_ptr) {
                print_complete(
                    argv[i],
                    inherit_named_args_from_parent ? extended_named_args : named_args,
                    used_positional_arguments,
                    stop_at_named_args);
                return;
            }
        }
        bool used = false;
        const auto * tmp = argv[i];
        if (stop_at_named_args && *tmp == '-') {
            bool long_option = *++tmp == '-';
            if (long_option) {
                ++tmp;
            }
            const auto * assign_ptr = strchr(tmp, '=');
            for (auto * opt : (inherit_named_args_from_parent ? extended_named_args : named_args)) {
                if (long_option) {
                    if (!opt->get_long_name().empty() &&
                        (assign_ptr ? std::string(tmp).compare(
                                          0, static_cast<size_t>(assign_ptr - tmp), opt->get_long_name()) == 0
                                    : opt->get_long_name() == tmp)) {
                        i += opt->parse_long(tmp, argc - i, &argv[i]);
                        used = true;
                        break;
                    }
                } else {
                    if (opt->get_short_name() != '\0' && opt->get_short_name() == tmp[short_option_idx]) {
                        auto used_args = opt->parse_short(tmp + short_option_idx, argc - i, &argv[i]);
                        if (used_args > 0) {
                            i += used_args;
                            short_option_idx = 0;
                        } else {
                            ++short_option_idx;
                        }
                        used = true;
                        break;
                    }
                }
            }
        }
        if (!used && stop_at_named_args) {
            for (auto & cmd : cmds) {
                if (cmd->id == argv[i]) {
                    if (const auto * arg = get_conflict_argument()) {
                        auto [fmt_message, conflict_option] = get_conflict_arg_msg(arg);
                        throw ArgumentParserConflictingArgumentsError(
                            fmt_message, std::string(option), conflict_option);
                    }
                    // the last subcommand wins
                    owner.p_impl->selected_command = cmd;
                    cmd->parse(argv[i], argc - i, &argv[i]);
                    i = argc;
                    used = true;
                    // The subcommand processed the completion of the argument. There is no need to continue parsing.
                    if (owner.p_impl->complete_arg_ptr) {
                        return;
                    }
                    break;
                }
            }
        }
        if (!used && (!stop_at_named_args || *argv[i] != '-') && used_positional_arguments < pos_args.size()) {
            auto * pos_arg = pos_args[used_positional_arguments];
            i += pos_arg->parse(argv[i], argc - i, &argv[i], stop_at_named_args);
            auto nrepeats = pos_arg->get_nrepeats();
            if ((nrepeats > 0 && pos_arg->parse_count >= nrepeats) || nrepeats == PositionalArg::OPTIONAL) {
                ++used_positional_arguments;
            }
            used = true;
        }
        if (!used) {
            throw ArgumentParserUnknownArgumentError(
                std::string(id),
                std::string(argv[i]),
                M_("Unknown argument \"{}\" for command \"{}\""),
                std::string(argv[i]),
                id);
        }
    }
    ++parse_count;

    // Test that all required positional arguments are present.
    for (const auto * pos_arg : pos_args) {
        const auto nrepeats = pos_arg->get_nrepeats();
        if (nrepeats > 0 || nrepeats == PositionalArg::AT_LEAST_ONE) {
            const auto nvals = pos_arg->get_nvals();
            if (pos_arg->get_parse_count() == 0 && nvals != PositionalArg::UNLIMITED &&
                nvals != PositionalArg::OPTIONAL) {
                throw ArgumentParserMissingPositionalArgumentError(
                    M_("Missing positional argument \"{}\" for command \"{}\""), pos_arg->get_id(), id);
            }
        }
    }

    if (auto & parse_hook = attached_command.get_parse_hook_func()) {
        parse_hook(&attached_command, option, argc, argv);
    }
}

ArgumentParser::Command & ArgumentParser::CommandAlias::get_attached_command() noexcept {
    return attached_command;
}

void ArgumentParser::CommandAlias::attach_named_arg(const std::string & id_path, const std::string & value) {
    attached_named_args.push_back({id_path, value});
}

void ArgumentParser::CommandAlias::add_required_value(const std::string & value_help, const std::string & descr) {
    required_values.emplace_back(value_help, descr);
}

ArgumentParser::CommandAlias::CommandAlias(ArgumentParser & owner, const std::string & id, Command & attached_command)
    : Command(owner, id),
      attached_command(attached_command) {}


// =============== ArgumentParser methods ===============

ArgumentParser::ArgumentParser() : p_impl(new ArgumentParserImpl) {}

ArgumentParser::~ArgumentParser() = default;

ArgumentParser::CommandOrdinary * ArgumentParser::add_new_command(const std::string & id) {
    std::unique_ptr<CommandOrdinary> arg(new CommandOrdinary(*this, id));
    auto * ptr = arg.get();
    p_impl->cmds.push_back(std::move(arg));
    return ptr;
}

ArgumentParser::CommandAlias * ArgumentParser::add_new_command_alias(
    const std::string & id, Command & attached_command) {
    std::unique_ptr<CommandAlias> arg(new CommandAlias(*this, id, attached_command));
    auto * ptr = arg.get();
    p_impl->cmds.push_back(std::move(arg));
    return ptr;
}

ArgumentParser::NamedArg * ArgumentParser::add_new_named_arg(const std::string & id) {
    std::unique_ptr<NamedArg> arg(new NamedArg(*this, id));
    auto * ptr = arg.get();
    p_impl->named_args.push_back(std::move(arg));
    return ptr;
}

ArgumentParser::PositionalArg * ArgumentParser::add_new_positional_arg(
    const std::string & id, std::vector<std::unique_ptr<libdnf5::Option>> * values) {
    std::unique_ptr<PositionalArg> arg(new PositionalArg(*this, id, values));
    auto * ptr = arg.get();
    p_impl->pos_args.push_back(std::move(arg));
    return ptr;
}

ArgumentParser::PositionalArg * ArgumentParser::add_new_positional_arg(
    const std::string & id,
    int nargs,
    libdnf5::Option * init_value,
    std::vector<std::unique_ptr<libdnf5::Option>> * values) {
    std::unique_ptr<PositionalArg> arg(new PositionalArg(*this, id, nargs, init_value, values));
    auto * ptr = arg.get();
    p_impl->pos_args.push_back(std::move(arg));
    return ptr;
}

ArgumentParser::Group * ArgumentParser::add_new_group(const std::string & id) {
    std::unique_ptr<Group> group(new Group(id));
    auto * ptr = group.get();
    p_impl->groups.push_back(std::move(group));
    return ptr;
}

std::vector<ArgumentParser::Argument *> * ArgumentParser::add_conflict_args_group(
    std::unique_ptr<std::vector<Argument *>> && conflict_args_group) {
    auto * ptr = conflict_args_group.get();
    p_impl->conflict_args_groups.push_back(std::move(conflict_args_group));
    return ptr;
}

libdnf5::Option * ArgumentParser::add_init_value(std::unique_ptr<libdnf5::Option> && src) {
    auto * ptr = src.get();
    p_impl->values_init.push_back(std::move(src));
    return ptr;
}

std::vector<std::unique_ptr<libdnf5::Option>> * ArgumentParser::add_new_values() {
    std::unique_ptr<std::vector<std::unique_ptr<libdnf5::Option>>> tmp(
        new std::vector<std::unique_ptr<libdnf5::Option>>);
    auto * ptr = tmp.get();
    p_impl->values.push_back(std::move(tmp));
    return ptr;
}

std::vector<std::unique_ptr<libdnf5::Option>> * ArgumentParser::add_values(
    std::unique_ptr<std::vector<std::unique_ptr<libdnf5::Option>>> && values) {
    auto * ptr = values.get();
    p_impl->values.push_back(std::move(values));
    return ptr;
}

void ArgumentParser::set_root_command(Command * command) noexcept {
    p_impl->root_command = command;
}

ArgumentParser::Command * ArgumentParser::get_root_command() noexcept {
    return p_impl->root_command;
}

ArgumentParser::Command * ArgumentParser::get_selected_command() noexcept {
    return p_impl->selected_command;
}


void ArgumentParser::parse(int argc, const char * const argv[]) {
    p_impl->assert_root_command();

    // mark root command as selected; overwrite with a subcommand in Command::parse()
    p_impl->selected_command = p_impl->root_command;
    p_impl->root_command->parse(argv[0], argc, argv);
}

void ArgumentParser::reset_parse_count() {
    for (auto & i : p_impl->pos_args) {
        i->reset_parse_count();
    }
    for (auto & i : p_impl->named_args) {
        i->reset_parse_count();
    }
    for (auto & i : p_impl->cmds) {
        i->reset_parse_count();
    }
}

void ArgumentParser::set_inherit_named_args(bool enable) noexcept {
    p_impl->inherit_named_args = enable;
}

bool ArgumentParser::get_inherit_named_args() const noexcept {
    return p_impl->inherit_named_args;
}

ArgumentParser::Command & ArgumentParser::get_command(const std::string & id_path) {
    p_impl->assert_root_command();

    auto * cmd = p_impl->root_command;
    if (id_path.empty()) {
        return *cmd;
    }
    std::string::size_type start_pos = 0;
    auto dot_pos = id_path.find('.');
    while (dot_pos != id_path.npos) {
        cmd = &cmd->get_command(std::string(id_path, start_pos, dot_pos - start_pos));
        start_pos = dot_pos + 1;
        dot_pos = id_path.find('.', start_pos);
    }
    cmd = &cmd->get_command(std::string(id_path, start_pos));
    return *cmd;
}

template <class Arg>
static const std::vector<Arg *> & get_command_args(ArgumentParser::Command & command) {
    if constexpr (std::is_same<Arg, ArgumentParser::NamedArg>::value) {
        return command.get_named_args();
    }
    if constexpr (std::is_same<Arg, ArgumentParser::PositionalArg>::value) {
        return command.get_positional_args();
    }
}

template <class Arg>
static Arg * get_arg(ArgumentParser::Command * root_command, const std::string & id_path, bool search_in_parent) {
    auto * cmd = root_command;
    std::string::size_type start_pos = 0;
    auto dot_pos = id_path.find('.');
    auto arg_id_name = dot_pos == id_path.npos ? id_path : std::string(id_path, id_path.rfind('.') + 1);
    Arg * ret = nullptr;
    while (dot_pos != id_path.npos) {
        if (search_in_parent) {
            if (auto tmp = find_arg(get_command_args<Arg>(*cmd), arg_id_name)) {
                ret = tmp;
            }
        }
        cmd = &cmd->get_command(std::string(id_path, start_pos, dot_pos - start_pos));
        start_pos = dot_pos + 1;
        dot_pos = id_path.find('.', start_pos);
    }
    if (auto tmp = find_arg(get_command_args<Arg>(*cmd), arg_id_name)) {
        ret = tmp;
    }
    return ret;
}

ArgumentParser::NamedArg & ArgumentParser::get_named_arg(const std::string & id_path, bool search_in_parent) {
    p_impl->assert_root_command();

    if (auto ret = get_arg<ArgumentParser::NamedArg>(p_impl->root_command, id_path, search_in_parent)) {
        return *ret;
    }
    throw ArgumentParserNotFoundError(M_("Named argument with path id \"{}\" not found"), id_path);
}


ArgumentParser::PositionalArg & ArgumentParser::get_positional_arg(const std::string & id_path, bool search_in_parent) {
    p_impl->assert_root_command();

    if (auto ret = get_arg<ArgumentParser::PositionalArg>(p_impl->root_command, id_path, search_in_parent)) {
        return *ret;
    }
    throw ArgumentParserNotFoundError(M_("Positional argument with path id \"{}\" not found"), id_path);
}

const std::vector<std::unique_ptr<ArgumentParser::Command>> & ArgumentParser::get_commands() const noexcept {
    return p_impl->cmds;
}

const std::vector<std::unique_ptr<ArgumentParser::NamedArg>> & ArgumentParser::get_named_args() const noexcept {
    return p_impl->named_args;
}

const std::vector<std::unique_ptr<ArgumentParser::PositionalArg>> & ArgumentParser::get_positional_args()
    const noexcept {
    return p_impl->pos_args;
}


void ArgumentParser::complete(int argc, const char * const argv[], int complete_arg_idx) {
    if (complete_arg_idx < 1 || complete_arg_idx >= argc) {
        return;
    }
    p_impl->complete_arg_ptr = argv + complete_arg_idx;
    try {
        parse(argc, argv);
    } catch (...) {
    }
}


void ArgumentParser::set_complete_add_description(bool enable) noexcept {
    p_impl->complete_add_description = enable;
}


bool ArgumentParser::get_complete_add_description() noexcept {
    return p_impl->complete_add_description;
}

}  // namespace libdnf5::cli
