/*
 * Copyright 2015-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 <crm/crm.h>
#include <crm/lrmd.h>
#include <crm/common/xml.h>
#include <crm/common/alerts_internal.h>
#include <crm/common/cib_internal.h>
#include <crm/common/xml_internal.h>

const char *pcmk__alert_keys[PCMK__ALERT_INTERNAL_KEY_MAX] = {
    [PCMK__alert_key_recipient] = "CRM_alert_recipient",
    [PCMK__alert_key_node] = "CRM_alert_node",
    [PCMK__alert_key_nodeid] = "CRM_alert_nodeid",
    [PCMK__alert_key_rsc] = "CRM_alert_rsc",
    [PCMK__alert_key_task] = "CRM_alert_task",
    [PCMK__alert_key_interval] = "CRM_alert_interval",
    [PCMK__alert_key_desc] = "CRM_alert_desc",
    [PCMK__alert_key_status] = "CRM_alert_status",
    [PCMK__alert_key_target_rc] = "CRM_alert_target_rc",
    [PCMK__alert_key_rc] = "CRM_alert_rc",
    [PCMK__alert_key_kind] = "CRM_alert_kind",
    [PCMK__alert_key_version] = "CRM_alert_version",
    [PCMK__alert_key_node_sequence] = PCMK__ALERT_NODE_SEQUENCE,
    [PCMK__alert_key_timestamp] = "CRM_alert_timestamp",
    [PCMK__alert_key_attribute_name] = "CRM_alert_attribute_name",
    [PCMK__alert_key_attribute_value] = "CRM_alert_attribute_value",
    [PCMK__alert_key_timestamp_epoch] = "CRM_alert_timestamp_epoch",
    [PCMK__alert_key_timestamp_usec] = "CRM_alert_timestamp_usec",
    [PCMK__alert_key_exec_time] = "CRM_alert_exec_time",
};

/*!
 * \brief Create a new alert entry structure
 *
 * \param[in] id  ID to use
 * \param[in] path  Path to alert agent executable
 *
 * \return Pointer to newly allocated alert entry
 * \note Non-string fields will be filled in with defaults.
 *       It is the caller's responsibility to free the result,
 *       using pcmk__free_alert().
 */
pcmk__alert_t *
pcmk__alert_new(const char *id, const char *path)
{
    pcmk__alert_t *entry = pcmk__assert_alloc(1, sizeof(pcmk__alert_t));

    pcmk__assert((id != NULL) && (path != NULL));
    entry->id = pcmk__str_copy(id);
    entry->path = pcmk__str_copy(path);
    entry->timeout = PCMK__ALERT_DEFAULT_TIMEOUT_MS;
    entry->flags = pcmk__alert_default;
    return entry;
}

void
pcmk__free_alert(pcmk__alert_t *entry)
{
    if (entry) {
        free(entry->id);
        free(entry->path);
        free(entry->tstamp_format);
        free(entry->recipient);

        g_strfreev(entry->select_attribute_name);
        if (entry->envvars) {
            g_hash_table_destroy(entry->envvars);
        }
        free(entry);
    }
}

/*!
 * \internal
 * \brief Duplicate an alert entry
 *
 * \param[in] entry  Alert entry to duplicate
 *
 * \return Duplicate of alert entry
 */
pcmk__alert_t *
pcmk__dup_alert(const pcmk__alert_t *entry)
{
    pcmk__alert_t *new_entry = pcmk__alert_new(entry->id, entry->path);

    new_entry->timeout = entry->timeout;
    new_entry->flags = entry->flags;
    new_entry->envvars = pcmk__str_table_dup(entry->envvars);
    new_entry->tstamp_format = pcmk__str_copy(entry->tstamp_format);
    new_entry->recipient = pcmk__str_copy(entry->recipient);
    if (entry->select_attribute_name) {
        new_entry->select_attribute_name = g_strdupv(entry->select_attribute_name);
    }
    return new_entry;
}

void
pcmk__add_alert_key(GHashTable *table, enum pcmk__alert_keys_e name,
                    const char *value)
{
    pcmk__assert((table != NULL) && (name >= 0)
                 && (name < PCMK__ALERT_INTERNAL_KEY_MAX));
    if (value == NULL) {
        crm_trace("Removing alert key %s", pcmk__alert_keys[name]);
        g_hash_table_remove(table, pcmk__alert_keys[name]);
    } else {
        crm_trace("Inserting alert key %s = '%s'",
                  pcmk__alert_keys[name], value);
        pcmk__insert_dup(table, pcmk__alert_keys[name], value);
    }
}

void
pcmk__add_alert_key_int(GHashTable *table, enum pcmk__alert_keys_e name,
                        int value)
{
    pcmk__assert((table != NULL) && (name >= 0)
                 && (name < PCMK__ALERT_INTERNAL_KEY_MAX));
    crm_trace("Inserting alert key %s = %d", pcmk__alert_keys[name], value);
    g_hash_table_insert(table, pcmk__str_copy(pcmk__alert_keys[name]),
                        pcmk__itoa(value));
}

#define READABLE_DEFAULT pcmk__readable_interval(PCMK__ALERT_DEFAULT_TIMEOUT_MS)

/*!
 * \internal
 * \brief Unpack options for an alert or alert recipient from its
 *        meta-attributes in the CIB XML configuration
 *
 * \param[in,out] xml          Alert or recipient XML
 * \param[in,out] entry        Where to store unpacked values
 * \param[in,out] max_timeout  Max timeout of all alerts and recipients thus far
 *
 * \return Standard Pacemaker return code
 */
static int
unpack_alert_options(xmlNode *xml, pcmk__alert_t *entry, guint *max_timeout)
{
    GHashTable *config_hash = pcmk__strkey_table(free, free);
    crm_time_t *now = crm_time_new(NULL);
    const char *value = NULL;
    int rc = pcmk_rc_ok;

    pcmk_rule_input_t rule_input = {
        .now = now,
    };

    pcmk_unpack_nvpair_blocks(xml, PCMK_XE_META_ATTRIBUTES, NULL, &rule_input,
                              config_hash, NULL);
    crm_time_free(now);

    value = g_hash_table_lookup(config_hash, PCMK_META_ENABLED);
    if ((value != NULL) && !crm_is_true(value)) {
        // No need to continue unpacking
        rc = pcmk_rc_disabled;
        goto done;
    }

    value = g_hash_table_lookup(config_hash, PCMK_META_TIMEOUT);
    if (value != NULL) {
        long long timeout_ms = crm_get_msec(value);

        entry->timeout = (int) QB_MIN(timeout_ms, INT_MAX);
        if (entry->timeout <= 0) {
            if (entry->timeout == 0) {
                crm_trace("Alert %s uses default timeout (%s)",
                          entry->id, READABLE_DEFAULT);
            } else {
                pcmk__config_warn("Using default timeout (%s) for alert %s "
                                  "because '%s' is not a valid timeout",
                                  entry->id, value, READABLE_DEFAULT);
            }
            entry->timeout = PCMK__ALERT_DEFAULT_TIMEOUT_MS;
        } else {
            crm_trace("Alert %s uses timeout of %s",
                      entry->id, pcmk__readable_interval(entry->timeout));
        }
        if (entry->timeout > *max_timeout) {
            *max_timeout = entry->timeout;
        }
    }
    value = g_hash_table_lookup(config_hash, PCMK_META_TIMESTAMP_FORMAT);
    if (value != NULL) {
        /* hard to do any checks here as merely anything can
         * can be a valid time-format-string
         */
        entry->tstamp_format = strdup(value);
        crm_trace("Alert %s uses timestamp format '%s'",
                  entry->id, entry->tstamp_format);
    }

done:
    g_hash_table_destroy(config_hash);
    return rc;
}

/*!
 * \internal
 * \brief Unpack agent parameters for an alert or alert recipient into an
 *        environment variable list based on its CIB XML configuration
 *
 * \param[in]     xml    Alert or recipient XML
 * \param[in,out] entry  Alert entry to create environment variables for
 */
static void
unpack_alert_parameters(const xmlNode *xml, pcmk__alert_t *entry)
{
    xmlNode *child;

    if ((xml == NULL) || (entry == NULL)) {
        return;
    }

    child = pcmk__xe_first_child(xml, PCMK_XE_INSTANCE_ATTRIBUTES, NULL,
                                 NULL);
    if (child == NULL) {
        return;
    }

    if (entry->envvars == NULL) {
        entry->envvars = pcmk__strkey_table(free, free);
    }

    for (child = pcmk__xe_first_child(child, PCMK_XE_NVPAIR, NULL, NULL);
         child != NULL; child = pcmk__xe_next(child, PCMK_XE_NVPAIR)) {

        const char *name = crm_element_value(child, PCMK_XA_NAME);
        const char *value = crm_element_value(child, PCMK_XA_VALUE);

        if (value == NULL) {
            value = "";
        }
        pcmk__insert_dup(entry->envvars, name, value);
        crm_trace("Alert %s: added environment variable %s='%s'",
                  entry->id, name, value);
    }
}

/*!
 * \internal
 * \brief Create filters for an alert or alert recipient based on its
 *        configuration in CIB XML
 *
 * \param[in]     xml    Alert or recipient XML
 * \param[in,out] entry  Alert entry to create filters for
 */
static void
unpack_alert_filter(xmlNode *xml, pcmk__alert_t *entry)
{
    xmlNode *select = pcmk__xe_first_child(xml, PCMK_XE_SELECT, NULL, NULL);
    xmlNode *event_type = NULL;
    uint32_t flags = pcmk__alert_none;

    for (event_type = pcmk__xe_first_child(select, NULL, NULL, NULL);
         event_type != NULL; event_type = pcmk__xe_next(event_type, NULL)) {

        if (pcmk__xe_is(event_type, PCMK_XE_SELECT_FENCING)) {
            flags |= pcmk__alert_fencing;

        } else if (pcmk__xe_is(event_type, PCMK_XE_SELECT_NODES)) {
            flags |= pcmk__alert_node;

        } else if (pcmk__xe_is(event_type, PCMK_XE_SELECT_RESOURCES)) {
            flags |= pcmk__alert_resource;

        } else if (pcmk__xe_is(event_type, PCMK_XE_SELECT_ATTRIBUTES)) {
            xmlNode *attr;
            const char *attr_name;
            int nattrs = 0;

            flags |= pcmk__alert_attribute;
            for (attr = pcmk__xe_first_child(event_type, PCMK_XE_ATTRIBUTE,
                                             NULL, NULL);
                 attr != NULL; attr = pcmk__xe_next(attr, PCMK_XE_ATTRIBUTE)) {

                attr_name = crm_element_value(attr, PCMK_XA_NAME);
                if (attr_name) {
                    if (nattrs == 0) {
                        g_strfreev(entry->select_attribute_name);
                        entry->select_attribute_name = NULL;
                    }
                    ++nattrs;
                    entry->select_attribute_name = pcmk__realloc(entry->select_attribute_name,
                                                                 (nattrs + 1) * sizeof(char*));
                    entry->select_attribute_name[nattrs - 1] = strdup(attr_name);
                    entry->select_attribute_name[nattrs] = NULL;
                }
            }
        }
    }

    if (flags != pcmk__alert_none) {
        entry->flags = flags;
        crm_debug("Alert %s receives events: attributes:%s%s%s%s",
                  entry->id,
                  (pcmk_is_set(flags, pcmk__alert_attribute)?
                   (entry->select_attribute_name? "some" : "all") : "none"),
                  (pcmk_is_set(flags, pcmk__alert_fencing)? " fencing" : ""),
                  (pcmk_is_set(flags, pcmk__alert_node)? " nodes" : ""),
                  (pcmk_is_set(flags, pcmk__alert_resource)? " resources" : ""));
    }
}

/*!
 * \internal
 * \brief Unpack an alert or an alert recipient
 *
 * \param[in,out] alert        Alert or recipient XML
 * \param[in,out] entry        Where to store unpacked values
 * \param[in,out] max_timeout  Max timeout of all alerts and recipients thus far
 *
 * \return Standard Pacemaker return code
 */
static int
unpack_alert(xmlNode *alert, pcmk__alert_t *entry, guint *max_timeout)
{
    int rc = pcmk_rc_ok;

    unpack_alert_parameters(alert, entry);
    rc = unpack_alert_options(alert, entry, max_timeout);
    if (rc == pcmk_rc_ok) {
        unpack_alert_filter(alert, entry);
    }
    return rc;
}

/*!
 * \internal
 * \brief Unpack a CIB alerts section into a list of alert entries
 *
 * \param[in] alerts  XML of CIB alerts section
 *
 * \return List of unpacked alert entries
 */
GList *
pcmk__unpack_alerts(const xmlNode *alerts)
{
    xmlNode *alert;
    pcmk__alert_t *entry;
    guint max_timeout = 0U;
    GList *alert_list = NULL;

    for (alert = pcmk__xe_first_child(alerts, PCMK_XE_ALERT, NULL, NULL);
         alert != NULL; alert = pcmk__xe_next(alert, PCMK_XE_ALERT)) {

        xmlNode *recipient = NULL;
        int recipients = 0;
        const char *alert_id = pcmk__xe_id(alert);
        const char *alert_path = crm_element_value(alert, PCMK_XA_PATH);

        // Not possible with schema validation enabled
        if (alert_id == NULL) {
            pcmk__config_err("Ignoring invalid alert without " PCMK_XA_ID);
            continue;
        }
        if (alert_path == NULL) {
            pcmk__config_err("Ignoring invalid alert %s without " PCMK_XA_PATH,
                             alert_id);
            continue;
        }

        entry = pcmk__alert_new(alert_id, alert_path);

        if (unpack_alert(alert, entry, &max_timeout) != pcmk_rc_ok) {
            // Don't allow recipients to override if entire alert is disabled
            crm_debug("Alert %s is disabled", entry->id);
            pcmk__free_alert(entry);
            continue;
        }

        if (entry->tstamp_format == NULL) {
            entry->tstamp_format =
                pcmk__str_copy(PCMK__ALERT_DEFAULT_TSTAMP_FORMAT);
        }

        crm_debug("Alert %s: path=%s timeout=%s tstamp-format='%s'",
                  entry->id, entry->path,
                  pcmk__readable_interval(entry->timeout),
                  entry->tstamp_format);

        for (recipient = pcmk__xe_first_child(alert, PCMK_XE_RECIPIENT, NULL,
                                              NULL);
             recipient != NULL;
             recipient = pcmk__xe_next(recipient, PCMK_XE_RECIPIENT)) {

            pcmk__alert_t *recipient_entry = pcmk__dup_alert(entry);

            recipients++;
            recipient_entry->recipient = crm_element_value_copy(recipient,
                                                                PCMK_XA_VALUE);

            if (unpack_alert(recipient, recipient_entry,
                             &max_timeout) != pcmk_rc_ok) {
                crm_debug("Alert %s: recipient %s is disabled",
                          entry->id, recipient_entry->id);
                pcmk__free_alert(recipient_entry);
                continue;
            }
            alert_list = g_list_prepend(alert_list, recipient_entry);
            crm_debug("Alert %s has recipient %s with value %s and %d envvars",
                      entry->id, pcmk__xe_id(recipient),
                      recipient_entry->recipient,
                      (recipient_entry->envvars?
                       g_hash_table_size(recipient_entry->envvars) : 0));
        }

        if (recipients == 0) {
            alert_list = g_list_prepend(alert_list, entry);
        } else { // Recipients were prepended individually above
            pcmk__free_alert(entry);
        }
    }
    return alert_list;
}

/*!
 * \internal
 * \brief Free an alert list generated by pcmk__unpack_alerts()
 *
 * \param[in,out] alert_list  Alert list to free
 */
void
pcmk__free_alerts(GList *alert_list)
{
    if (alert_list != NULL) {
        g_list_free_full(alert_list, (GDestroyNotify) pcmk__free_alert);
    }
}
