/*
 * Copyright 2024-2025 the Pacemaker project contributors
 *
 * The version control history for this file may have further details.
 *
 * This source code is licensed under the GNU Lesser General Public License
 * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
 */

#include <crm_internal.h>

#include <stdbool.h>

#include <glib.h>   // GSList, GString

#include "crmcommon_private.h"

/*!
 * \internal
 * \brief Output an option's possible values
 *
 * \param[in,out] out     Output object
 * \param[in]     option  Option whose possible values to add
 */
static void
add_possible_values_default(pcmk__output_t *out,
                            const pcmk__cluster_option_t *option)
{
    const char *id = _("Possible values");
    GString *buf = g_string_sized_new(256);

    pcmk__assert(option->type != NULL);

    if (pcmk__is_set(option->flags, pcmk__opt_generated)) {
        id = _("Possible values (generated by Pacemaker)");
    }

    if ((option->values != NULL) && (strcmp(option->type, "select") == 0)) {
        // If there is no default value, don't look for one
        bool found_default = (option->default_value == NULL);

        gchar **values = g_strsplit(option->values, ", ", 0);

        for (gchar **value = values; *value != NULL; value++) {
            if (buf->len > 0) {
                g_string_append(buf, ", ");
            }
            g_string_append_c(buf, '"');
            g_string_append(buf, *value);
            g_string_append_c(buf, '"');

            if (!found_default
                && pcmk__str_eq(*value, option->default_value,
                                pcmk__str_none)) {
                found_default = true;
                g_string_append(buf, _(" (default)"));
            }
        }
        g_strfreev(values);

    } else if (option->default_value != NULL) {
        pcmk__g_strcat(buf,
                       option->type, _(" (default: \""), option->default_value,
                       "\")", NULL);

    } else {
        pcmk__g_strcat(buf, option->type, _(" (no default)"), NULL);
    }

    out->list_item(out, id, "%s", buf->str);
    g_string_free(buf, TRUE);
}

/*!
 * \internal
 * \brief Output a single option's metadata
 *
 * \param[in,out] out     Output object
 * \param[in]     option  Option to add
 */
static void
add_option_metadata_default(pcmk__output_t *out,
                            const pcmk__cluster_option_t *option)
{
    const char *desc_short = option->description_short;
    const char *desc_long = option->description_long;

    pcmk__assert((desc_short != NULL) || (desc_long != NULL));

    if (desc_short == NULL) {
        desc_short = desc_long;
        desc_long = NULL;
    }

    out->list_item(out, option->name, "%s", _(desc_short));

    out->begin_list(out, NULL, NULL, NULL);

    if (desc_long != NULL) {
        out->list_item(out, NULL, "%s", _(desc_long));
    }
    add_possible_values_default(out, option);
    out->end_list(out);
}

/*!
 * \internal
 * \brief Output the metadata for a list of options
 *
 * \param[in,out] out   Output object
 * \param[in]     args  Message-specific arguments
 *
 * \return Standard Pacemaker return code
 *
 * \note \p args should contain the following:
 *       -# Fake resource agent name for the option list (ignored)
 *       -# Short description of option list
 *       -# Long description of option list
 *       -# Filter: Group of <tt>enum pcmk__opt_flags</tt>; output an option
 *          only if its \c flags member has all these flags set
 *       -# <tt>NULL</tt>-terminated list of options whose metadata to format
 *       -# All: If \c true, output all options; otherwise, exclude advanced and
 *          deprecated options unless \c pcmk__opt_advanced and
 *          \c pcmk__opt_deprecated flags (respectively) are set in the filter.
 */
PCMK__OUTPUT_ARGS("option-list", "const char *", "const char *", "const char *",
                  "uint32_t", "const pcmk__cluster_option_t *", "bool")
static int
option_list_default(pcmk__output_t *out, va_list args)
{
    const char *name G_GNUC_UNUSED = va_arg(args, const char *);
    const char *desc_short = va_arg(args, const char *);
    const char *desc_long = va_arg(args, const char *);
    const uint32_t filter = va_arg(args, uint32_t);
    const pcmk__cluster_option_t *option_list =
        va_arg(args, pcmk__cluster_option_t *);
    const bool all = (bool) va_arg(args, int);

    const bool show_deprecated = all
                                 || pcmk__is_set(filter, pcmk__opt_deprecated);
    const bool show_advanced = all || pcmk__is_set(filter, pcmk__opt_advanced);
    bool old_fancy = false;

    GSList *deprecated = NULL;
    GSList *advanced = NULL;
    GSList *aliased = NULL;     // Options with deprecated aliases (alt_name)

    pcmk__assert((out != NULL) && (desc_short != NULL)
                 && (desc_long != NULL) && (option_list != NULL));

    old_fancy = pcmk__output_text_get_fancy(out);
    pcmk__output_text_set_fancy(out, true);

    out->info(out, "%s", _(desc_short));
    out->spacer(out);
    out->info(out, "%s", _(desc_long));
    out->begin_list(out, NULL, NULL, NULL);

    for (const pcmk__cluster_option_t *option = option_list;
         option->name != NULL; option++) {

        if (!pcmk__all_flags_set(option->flags, filter)) {
            continue;
        }

        // Store deprecated and advanced options to display later if appropriate
        if (pcmk__is_set(option->flags, pcmk__opt_deprecated)) {
            if (show_deprecated) {
                deprecated = g_slist_prepend(deprecated, (gpointer) option);
            }

        } else if (pcmk__is_set(option->flags, pcmk__opt_advanced)) {
            if (show_advanced) {
                advanced = g_slist_prepend(advanced, (gpointer) option);
            }

        } else {
            out->spacer(out);
            add_option_metadata_default(out, option);
        }

        if (show_deprecated && (option->alt_name != NULL)) {
            aliased = g_slist_prepend(aliased, (gpointer) option);
        }
    }

    if (advanced != NULL) {
        advanced = g_slist_reverse(advanced);

        out->spacer(out);
        out->begin_list(out, NULL, NULL, _("ADVANCED OPTIONS"));
        for (const GSList *iter = advanced; iter != NULL; iter = iter->next) {
            const pcmk__cluster_option_t *option = iter->data;

            out->spacer(out);
            add_option_metadata_default(out, option);
        }
        out->end_list(out);
        g_slist_free(advanced);
    }

    if ((deprecated != NULL) || (aliased != NULL)) {
        deprecated = g_slist_reverse(deprecated);
        aliased = g_slist_reverse(aliased);

        out->spacer(out);
        out->begin_list(out, NULL, NULL,
                        _("DEPRECATED OPTIONS (will be removed in a future "
                          "release)"));

        for (const GSList *iter = deprecated; iter != NULL; iter = iter->next) {
            const pcmk__cluster_option_t *option = iter->data;

            out->spacer(out);
            add_option_metadata_default(out, option);
        }

        for (const GSList *iter = aliased; iter != NULL; iter = iter->next) {
            const pcmk__cluster_option_t *option = iter->data;
            char *desc = pcmk__assert_asprintf(_("Deprecated alias for %s"),
                                               option->name);
            pcmk__cluster_option_t alias = *option;

            alias.name = option->alt_name;
            alias.alt_name = NULL;
            alias.flags |= pcmk__opt_deprecated;
            alias.description_short = desc;
            alias.description_long = NULL;

            out->spacer(out);
            add_option_metadata_default(out, &alias);
            free(desc);
        }

        out->end_list(out);
        g_slist_free(deprecated);
        g_slist_free(aliased);
    }

    out->end_list(out);
    pcmk__output_text_set_fancy(out, old_fancy);
    return pcmk_rc_ok;
}

/*!
 * \internal
 * \brief Add a description element to an OCF-like metadata XML node
 *
 * Include a translation based on the current locale if \c ENABLE_NLS is
 * defined.
 *
 * \param[in,out] out       Output object
 * \param[in]     for_long  If \c true, add long description; otherwise, add
 *                          short description
 * \param[in]     desc      Textual description to add
 */
static void
add_desc_xml(pcmk__output_t *out, bool for_long, const char *desc)
{
    const char *tag = (for_long? PCMK_XE_LONGDESC : PCMK_XE_SHORTDESC);
    xmlNode *node = pcmk__output_create_xml_text_node(out, tag, desc);

    pcmk__xe_set(node, PCMK_XA_LANG, PCMK__VALUE_EN);

#ifdef ENABLE_NLS
    if (!pcmk__str_eq(desc, _(desc), pcmk__str_none)) {
        // The locale should have been set already by crm_log_preinit()
        gchar **parts = g_strsplit(setlocale(LC_ALL, NULL), "_", 2);

        node = pcmk__output_create_xml_text_node(out, tag, _(desc));
        pcmk__xe_set(node, PCMK_XA_LANG, parts[0]);

        g_strfreev(parts);
    }
#endif
}

/*!
 * \internal
 * \brief Output an option's possible values
 *
 * Add a \c PCMK_XE_OPTION element for each of the option's possible values.
 *
 * \param[in,out] out     Output object
 * \param[in]     option  Option whose possible values to add
 */
static void
add_possible_values_xml(pcmk__output_t *out,
                        const pcmk__cluster_option_t *option)
{
    gchar **values = NULL;

    if ((option->values == NULL)
        || !pcmk__str_eq(option->type, "select", pcmk__str_none)) {
        return;
    }

    values = g_strsplit(option->values, ", ", 0);

    for (gchar **value = values; *value != NULL; value++) {
        pcmk__output_create_xml_node(out, PCMK_XE_OPTION,
                                     PCMK_XA_VALUE, *value,
                                     NULL);
    }

    g_strfreev(values);
}

/*!
 * \internal
 * \brief Map an option type to one suitable for daemon metadata
 *
 * \param[in] type  Option type to map
 *
 * \return String suitable for daemon metadata to display as an option type
 */
static const char *
map_legacy_option_type(const char *type)
{
    // @COMPAT Drop this function when we drop daemon metadata commands
    if (pcmk__str_any_of(type, PCMK_VALUE_DURATION, PCMK_VALUE_TIMEOUT, NULL)) {
        return PCMK__VALUE_TIME;

    } else if (pcmk__str_any_of(type,
                                PCMK_VALUE_NONNEGATIVE_INTEGER,
                                PCMK_VALUE_SCORE, NULL)) {
        return PCMK_VALUE_INTEGER;

    } else if (pcmk__str_eq(type, PCMK_VALUE_VERSION, pcmk__str_none)) {
        return PCMK_VALUE_STRING;

    } else {
        return type;
    }
}

/*!
 * \internal
 * \brief Add a \c PCMK_XE_PARAMETER element to an OCF-like metadata XML node
 *
 * \param[in,out] out            Output object
 * \param[in]     option         Option to add as a \c PCMK_XE_PARAMETER element
 * \param[in]     replaced_with  Name of option that replaces this one (ignored
 *                               if this option is not deprecated)
 */
static void
add_option_metadata_xml(pcmk__output_t *out,
                        const pcmk__cluster_option_t *option,
                        const char *replaced_with)
{
    const char *type = option->type;
    const char *desc_long = option->description_long;
    const char *desc_short = option->description_short;
    const bool advanced = pcmk__is_set(option->flags, pcmk__opt_advanced);
    const bool deprecated = pcmk__is_set(option->flags, pcmk__opt_deprecated);
    const bool generated = pcmk__is_set(option->flags, pcmk__opt_generated);

    // OCF requires "1"/"0" and does not allow "true"/"false
    // @COMPAT Variables no longer needed after we drop legacy mode
    const char *advanced_s = advanced? "1" : "0";
    const char *generated_s = generated? "1" : "0";

    // @COMPAT For daemon metadata only; drop when daemon metadata is dropped
    const bool legacy = pcmk__output_get_legacy_xml(out);
    char *desc_long_legacy = NULL;
    GString *desc_short_legacy = NULL;

    // The standard requires a parameter type
    pcmk__assert(type != NULL);

    // The standard requires long and short parameter descriptions
    pcmk__assert((desc_long != NULL) || (desc_short != NULL));

    if (desc_long == NULL) {
        desc_long = desc_short;
    } else if (desc_short == NULL) {
        desc_short = desc_long;
    }

    if (legacy) {
        // This is ugly but it will go away at a major release bump
        type = map_legacy_option_type(type);

        if (option->values != NULL) {
            desc_long_legacy = pcmk__assert_asprintf("%s  Allowed values: %s",
                                                     desc_long, option->values);
            desc_long = desc_long_legacy;
        }

        if (deprecated || advanced) {
            const size_t init_sz = 1023;

            if (desc_long != option->description_long) {
                /* desc_long was NULL and got assigned desc_short, which was
                 * non-empty. Let desc_long have the "real" description, and put
                 * the flag in desc_short.
                 */
                desc_short = "";
            } else {
                desc_short = pcmk__s(option->description_short, "");
            }

            if (deprecated) {
                pcmk__add_word(&desc_short_legacy, init_sz,
                               "*** Deprecated ***");

                if (replaced_with != NULL) {
                    pcmk__g_strcat(desc_short_legacy,
                                   " *** Replaced with ", replaced_with, " ***",
                                   NULL);
                }
            }
            if (advanced) {
                pcmk__add_word(&desc_short_legacy, init_sz,
                               "*** Advanced Use Only ***");
            }
            pcmk__add_word(&desc_short_legacy, 0, desc_short);

            desc_short = desc_short_legacy->str;
        }

        /* These must be NULL when used as attribute values later.
         * PCMK_XA_ADVANCED and PCMK_XA_GENERATED break validation for some
         * legacy tools.
         */
        advanced_s = NULL;
        generated_s = NULL;
    }

    pcmk__output_xml_create_parent(out, PCMK_XE_PARAMETER,
                                   PCMK_XA_NAME, option->name,
                                   PCMK_XA_ADVANCED, advanced_s,
                                   PCMK_XA_GENERATED, generated_s,
                                   NULL);

    if (deprecated && !legacy) {
        pcmk__output_xml_create_parent(out, PCMK_XE_DEPRECATED, NULL);

        if (replaced_with != NULL) {
            pcmk__output_create_xml_node(out, PCMK_XE_REPLACED_WITH,
                                         PCMK_XA_NAME, replaced_with,
                                         NULL);
        }
        pcmk__output_xml_pop_parent(out);
    }
    add_desc_xml(out, true, desc_long);
    add_desc_xml(out, false, desc_short);

    pcmk__output_xml_create_parent(out, PCMK_XE_CONTENT,
                                   PCMK_XA_TYPE, type,
                                   PCMK_XA_DEFAULT, option->default_value,
                                   NULL);

    add_possible_values_xml(out, option);

    pcmk__output_xml_pop_parent(out);
    pcmk__output_xml_pop_parent(out);

    free(desc_long_legacy);
    if (desc_short_legacy != NULL) {
        g_string_free(desc_short_legacy, TRUE);
    }
}

/*!
 * \internal
 * \brief Output the metadata for a list of options as OCF-like XML
 *
 * \param[in,out] out   Output object
 * \param[in]     args  Message-specific arguments
 *
 * \return Standard Pacemaker return code
 *
 * \note \p args should contain the following:
 *       -# Fake resource agent name for the option list
 *       -# Short description of option list
 *       -# Long description of option list
 *       -# Filter: Group of <tt>enum pcmk__opt_flags</tt>; output an option
 *          only if its \c flags member has all these flags set
 *       -# <tt>NULL</tt>-terminated list of options whose metadata to format
 *       -# Whether to output all options (ignored, treated as \c true)
 */
PCMK__OUTPUT_ARGS("option-list", "const char *", "const char *", "const char *",
                  "uint32_t", "const pcmk__cluster_option_t *", "bool")
static int
option_list_xml(pcmk__output_t *out, va_list args)
{
    const char *name = va_arg(args, const char *);
    const char *desc_short = va_arg(args, const char *);
    const char *desc_long = va_arg(args, const char *);
    const uint32_t filter = va_arg(args, uint32_t);
    const pcmk__cluster_option_t *option_list =
        va_arg(args, pcmk__cluster_option_t *);

    pcmk__assert((out != NULL) && (name != NULL) && (desc_short != NULL)
                 && (desc_long != NULL) && (option_list != NULL));

    pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCE_AGENT,
                                   PCMK_XA_NAME, name,
                                   PCMK_XA_VERSION, PACEMAKER_VERSION,
                                   NULL);

    pcmk__output_create_xml_text_node(out, PCMK_XE_VERSION, PCMK_OCF_VERSION);
    add_desc_xml(out, true, desc_long);
    add_desc_xml(out, false, desc_short);

    pcmk__output_xml_create_parent(out, PCMK_XE_PARAMETERS, NULL);

    for (const pcmk__cluster_option_t *option = option_list;
         option->name != NULL; option++) {

        if (!pcmk__all_flags_set(option->flags, filter)) {
            continue;
        }

        add_option_metadata_xml(out, option, NULL);

        if (option->alt_name != NULL) {
            pcmk__cluster_option_t alias = *option;
            char *desc = pcmk__assert_asprintf(_("Deprecated alias for %s"),
                                               option->name);

            alias.name = option->alt_name;
            alias.alt_name = NULL;
            alias.flags |= pcmk__opt_deprecated;
            alias.description_short = desc;
            alias.description_long = NULL;

            add_option_metadata_xml(out, &alias, option->name);
            free(desc);
        }
    }

    pcmk__output_xml_pop_parent(out);
    pcmk__output_xml_pop_parent(out);
    return pcmk_rc_ok;
}

static pcmk__message_entry_t fmt_functions[] = {
    { "option-list", "default", option_list_default },
    { "option-list", "xml", option_list_xml },

    { NULL, NULL, NULL }
};

/*!
 * \internal
 * \brief Register the formatting functions for option lists
 *
 * \param[in,out] out  Output object
 */
void
pcmk__register_option_messages(pcmk__output_t *out) {
    pcmk__register_messages(out, fmt_functions);
}
