/*
 * Copyright 2004-2024 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 <stdio.h>
#include <sys/types.h>

#include <glib.h>
#include <libxml/tree.h>

#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>

/*!
 * \brief Create a Pacemaker request (for IPC or cluster layer)
 *
 * \param[in] task          What to set as the request's task
 * \param[in] msg_data      What to add as the request's data contents
 * \param[in] host_to       What to set as the request's destination host
 * \param[in] sys_to        What to set as the request's destination system
 * \param[in] sys_from      If not NULL, set as request's origin system
 * \param[in] uuid_from     If not NULL, use in request's origin system
 * \param[in] origin        Name of function that called this one
 *
 * \return XML of new request
 *
 * \note One of sys_from or uuid_from must be non-NULL
 * \note This function should not be called directly, but via the
 *       create_request() wrapper.
 * \note The caller is responsible for freeing the result using free_xml().
 */
xmlNode *
create_request_adv(const char *task, xmlNode *msg_data,
                   const char *host_to, const char *sys_to,
                   const char *sys_from, const char *uuid_from,
                   const char *origin)
{
    static uint ref_counter = 0;

    char *true_from = NULL;
    xmlNode *request = NULL;
    char *reference = crm_strdup_printf("%s-%s-%lld-%u",
                                        (task? task : "_empty_"),
                                        (sys_from? sys_from : "_empty_"),
                                        (long long) time(NULL), ref_counter++);

    if (uuid_from != NULL) {
        true_from = crm_strdup_printf("%s_%s", uuid_from,
                                      (sys_from? sys_from : "none"));
    } else if (sys_from != NULL) {
        true_from = strdup(sys_from);
    } else {
        crm_err("Cannot create IPC request: No originating system specified");
    }

    // host_from will get set for us if necessary by the controller when routed
    request = pcmk__xe_create(NULL, __func__);
    crm_xml_add(request, PCMK_XA_ORIGIN, origin);
    crm_xml_add(request, PCMK__XA_T, PCMK__VALUE_CRMD);
    crm_xml_add(request, PCMK_XA_VERSION, CRM_FEATURE_SET);
    crm_xml_add(request, PCMK__XA_SUBT, PCMK__VALUE_REQUEST);
    crm_xml_add(request, PCMK_XA_REFERENCE, reference);
    crm_xml_add(request, PCMK__XA_CRM_TASK, task);
    crm_xml_add(request, PCMK__XA_CRM_SYS_TO, sys_to);
    crm_xml_add(request, PCMK__XA_CRM_SYS_FROM, true_from);

    /* HOSTTO will be ignored if it is to the DC anyway. */
    if (host_to != NULL && strlen(host_to) > 0) {
        crm_xml_add(request, PCMK__XA_CRM_HOST_TO, host_to);
    }

    if (msg_data != NULL) {
        xmlNode *wrapper = pcmk__xe_create(request, PCMK__XE_CRM_XML);

        pcmk__xml_copy(wrapper, msg_data);
    }
    free(reference);
    free(true_from);

    return request;
}

/*!
 * \brief Create a Pacemaker reply (for IPC or cluster layer)
 *
 * \param[in] original_request   XML of request this is a reply to
 * \param[in] xml_response_data  XML to copy as data section of reply
 * \param[in] origin             Name of function that called this one
 *
 * \return XML of new reply
 *
 * \note This function should not be called directly, but via the
 *       create_reply() wrapper.
 * \note The caller is responsible for freeing the result using free_xml().
 */
xmlNode *
create_reply_adv(const xmlNode *original_request, xmlNode *xml_response_data,
                 const char *origin)
{
    xmlNode *reply = NULL;

    const char *host_from = crm_element_value(original_request, PCMK__XA_SRC);
    const char *sys_from = crm_element_value(original_request,
                                             PCMK__XA_CRM_SYS_FROM);
    const char *sys_to = crm_element_value(original_request,
                                           PCMK__XA_CRM_SYS_TO);
    const char *type = crm_element_value(original_request, PCMK__XA_SUBT);
    const char *operation = crm_element_value(original_request,
                                              PCMK__XA_CRM_TASK);
    const char *crm_msg_reference = crm_element_value(original_request,
                                                      PCMK_XA_REFERENCE);

    if (type == NULL) {
        crm_err("Cannot create new_message, no message type in original message");
        CRM_ASSERT(type != NULL);
        return NULL;
    }

    if (strcmp(type, PCMK__VALUE_REQUEST) != 0) {
        /* Replies should only be generated for request messages, but it's possible
         * we expect replies to other messages right now so this can't be enforced.
         */
        crm_trace("Creating a reply for a non-request original message");
    }

    reply = pcmk__xe_create(NULL, __func__);
    crm_xml_add(reply, PCMK_XA_ORIGIN, origin);
    crm_xml_add(reply, PCMK__XA_T, PCMK__VALUE_CRMD);
    crm_xml_add(reply, PCMK_XA_VERSION, CRM_FEATURE_SET);
    crm_xml_add(reply, PCMK__XA_SUBT, PCMK__VALUE_RESPONSE);
    crm_xml_add(reply, PCMK_XA_REFERENCE, crm_msg_reference);
    crm_xml_add(reply, PCMK__XA_CRM_TASK, operation);

    /* since this is a reply, we reverse the from and to */
    crm_xml_add(reply, PCMK__XA_CRM_SYS_TO, sys_from);
    crm_xml_add(reply, PCMK__XA_CRM_SYS_FROM, sys_to);

    /* HOSTTO will be ignored if it is to the DC anyway. */
    if (host_from != NULL && strlen(host_from) > 0) {
        crm_xml_add(reply, PCMK__XA_CRM_HOST_TO, host_from);
    }

    if (xml_response_data != NULL) {
        xmlNode *wrapper = pcmk__xe_create(reply, PCMK__XE_CRM_XML);

        pcmk__xml_copy(wrapper, xml_response_data);
    }

    return reply;
}

/*!
 * \brief Get name to be used as identifier for cluster messages
 *
 * \param[in] name  Actual system name to check
 *
 * \return Non-NULL cluster message identifier corresponding to name
 *
 * \note The Pacemaker daemons were renamed in version 2.0.0, but the old names
 *       must continue to be used as the identifier for cluster messages, so
 *       that mixed-version clusters are possible during a rolling upgrade.
 */
const char *
pcmk__message_name(const char *name)
{
    if (name == NULL) {
        return "unknown";

    } else if (!strcmp(name, "pacemaker-attrd")) {
        return "attrd";

    } else if (!strcmp(name, "pacemaker-based")) {
        return CRM_SYSTEM_CIB;

    } else if (!strcmp(name, "pacemaker-controld")) {
        return CRM_SYSTEM_CRMD;

    } else if (!strcmp(name, "pacemaker-execd")) {
        return CRM_SYSTEM_LRMD;

    } else if (!strcmp(name, "pacemaker-fenced")) {
        return "stonith-ng";

    } else if (!strcmp(name, "pacemaker-schedulerd")) {
        return CRM_SYSTEM_PENGINE;

    } else {
        return name;
    }
}

/*!
 * \internal
 * \brief Register handlers for server commands
 *
 * \param[in] handlers  Array of handler functions for supported server commands
 *                      (the final entry must have a NULL command name, and if
 *                      it has a handler it will be used as the default handler
 *                      for unrecognized commands)
 *
 * \return Newly created hash table with commands and handlers
 * \note The caller is responsible for freeing the return value with
 *       g_hash_table_destroy().
 */
GHashTable *
pcmk__register_handlers(const pcmk__server_command_t handlers[])
{
    GHashTable *commands = g_hash_table_new(g_str_hash, g_str_equal);

    if (handlers != NULL) {
        int i;

        for (i = 0; handlers[i].command != NULL; ++i) {
            g_hash_table_insert(commands, (gpointer) handlers[i].command,
                                handlers[i].handler);
        }
        if (handlers[i].handler != NULL) {
            // g_str_hash() can't handle NULL, so use empty string for default
            g_hash_table_insert(commands, (gpointer) "", handlers[i].handler);
        }
    }
    return commands;
}

/*!
 * \internal
 * \brief Process an incoming request
 *
 * \param[in,out] request   Request to process
 * \param[in]     handlers  Command table created by pcmk__register_handlers()
 *
 * \return XML to send as reply (or NULL if no reply is needed)
 */
xmlNode *
pcmk__process_request(pcmk__request_t *request, GHashTable *handlers)
{
    xmlNode *(*handler)(pcmk__request_t *request) = NULL;

    CRM_CHECK((request != NULL) && (request->op != NULL) && (handlers != NULL),
              return NULL);

    if (pcmk_is_set(request->flags, pcmk__request_sync)
        && (request->ipc_client != NULL)) {
        CRM_CHECK(request->ipc_client->request_id == request->ipc_id,
                  return NULL);
    }

    handler = g_hash_table_lookup(handlers, request->op);
    if (handler == NULL) {
        handler = g_hash_table_lookup(handlers, ""); // Default handler
        if (handler == NULL) {
            crm_info("Ignoring %s request from %s %s with no handler",
                     request->op, pcmk__request_origin_type(request),
                     pcmk__request_origin(request));
            return NULL;
        }
    }

    return (*handler)(request);
}

/*!
 * \internal
 * \brief Free memory used within a request (but not the request itself)
 *
 * \param[in,out] request  Request to reset
 */
void
pcmk__reset_request(pcmk__request_t *request)
{
    free(request->op);
    request->op = NULL;

    pcmk__reset_result(&(request->result));
}

// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START

#include <crm/common/xml_compat.h>

gboolean
add_message_xml(xmlNode *msg, const char *field, xmlNode *xml)
{
    xmlNode *holder = pcmk__xe_create(msg, field);

    pcmk__xml_copy(holder, xml);
    return TRUE;
}

xmlNode *
get_message_xml(const xmlNode *msg, const char *field)
{
    xmlNode *child = pcmk__xe_first_child(msg, field, NULL, NULL);

    return pcmk__xe_first_child(child, NULL, NULL, NULL);
}

// LCOV_EXCL_STOP
// End deprecated API
