/*
 * 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 <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <pwd.h>

#include <sys/stat.h>
#include <sys/types.h>

#include <glib.h>

#include <crm/crm.h>
#include <crm/cib/internal.h>
#include <crm/common/xml.h>

static GHashTable *cib_op_callback_table = NULL;

#define op_common(cib) do {                                             \
        if(cib == NULL) {                                               \
            return -EINVAL;						\
        } else if(cib->delegate_fn == NULL) {                           \
            return -EPROTONOSUPPORT;                                    \
        }                                                               \
    } while(0)

static int
cib_client_set_op_callback(cib_t *cib,
                           void (*callback) (const xmlNode * msg, int call_id,
                                             int rc, xmlNode * output))
{
    if (callback == NULL) {
        crm_info("Un-Setting operation callback");

    } else {
        crm_trace("Setting operation callback");
    }
    cib->op_callback = callback;
    return pcmk_ok;
}

static gint
ciblib_GCompareFunc(gconstpointer a, gconstpointer b)
{
    int rc = 0;
    const cib_notify_client_t *a_client = a;
    const cib_notify_client_t *b_client = b;

    CRM_CHECK(a_client->event != NULL && b_client->event != NULL, return 0);
    rc = strcmp(a_client->event, b_client->event);
    if (rc == 0) {
        if (a_client->callback == b_client->callback) {
            return 0;
        } else if (((long)a_client->callback) < ((long)b_client->callback)) {
            crm_trace("callbacks for %s are not equal: %p < %p",
                      a_client->event, a_client->callback, b_client->callback);
            return -1;
        }
        crm_trace("callbacks for %s are not equal: %p > %p",
                  a_client->event, a_client->callback, b_client->callback);
        return 1;
    }
    return rc;
}

static int
cib_client_add_notify_callback(cib_t * cib, const char *event,
                               void (*callback) (const char *event,
                                                 xmlNode * msg))
{
    GList *list_item = NULL;
    cib_notify_client_t *new_client = NULL;

    if ((cib->variant != cib_native) && (cib->variant != cib_remote)) {
        return -EPROTONOSUPPORT;
    }

    crm_trace("Adding callback for %s events (%d)",
              event, g_list_length(cib->notify_list));

    new_client = pcmk__assert_alloc(1, sizeof(cib_notify_client_t));
    new_client->event = event;
    new_client->callback = callback;

    list_item = g_list_find_custom(cib->notify_list, new_client,
                                   ciblib_GCompareFunc);

    if (list_item != NULL) {
        crm_warn("Callback already present");
        free(new_client);
        return -EINVAL;

    } else {
        cib->notify_list = g_list_append(cib->notify_list, new_client);

        cib->cmds->register_notification(cib, event, 1);

        crm_trace("Callback added (%d)", g_list_length(cib->notify_list));
    }
    return pcmk_ok;
}

static int
get_notify_list_event_count(cib_t *cib, const char *event)
{
    int count = 0;

    for (GList *iter = g_list_first(cib->notify_list); iter != NULL;
         iter = iter->next) {
        cib_notify_client_t *client = (cib_notify_client_t *) iter->data;

        if (strcmp(client->event, event) == 0) {
            count++;
        }
    }
    crm_trace("event(%s) count : %d", event, count);
    return count;
}

static int
cib_client_del_notify_callback(cib_t *cib, const char *event,
                               void (*callback) (const char *event,
                                                 xmlNode *msg))
{
    GList *list_item = NULL;
    cib_notify_client_t *new_client = NULL;

    if (cib->variant != cib_native && cib->variant != cib_remote) {
        return -EPROTONOSUPPORT;
    }

    if (get_notify_list_event_count(cib, event) == 0) {
        crm_debug("The callback of the event does not exist(%s)", event);
        return pcmk_ok;
    }

    crm_debug("Removing callback for %s events", event);

    new_client = pcmk__assert_alloc(1, sizeof(cib_notify_client_t));
    new_client->event = event;
    new_client->callback = callback;

    list_item = g_list_find_custom(cib->notify_list, new_client, ciblib_GCompareFunc);

    if (list_item != NULL) {
        cib_notify_client_t *list_client = list_item->data;

        cib->notify_list = g_list_remove(cib->notify_list, list_client);
        free(list_client);

        crm_trace("Removed callback");

    } else {
        crm_trace("Callback not present");
    }

    if (get_notify_list_event_count(cib, event) == 0) {
        /* When there is not the registration of the event, the processing turns off a notice. */
        cib->cmds->register_notification(cib, event, 0);
    }

    free(new_client);
    return pcmk_ok;
}

static gboolean
cib_async_timeout_handler(gpointer data)
{
    struct timer_rec_s *timer = data;

    crm_debug("Async call %d timed out after %ds",
              timer->call_id, timer->timeout);
    cib_native_callback(timer->cib, NULL, timer->call_id, -ETIME);

    // We remove the handler in remove_cib_op_callback()
    return G_SOURCE_CONTINUE;
}

static gboolean
cib_client_register_callback_full(cib_t *cib, int call_id, int timeout,
                                  gboolean only_success, void *user_data,
                                  const char *callback_name,
                                  void (*callback)(xmlNode *, int, int,
                                                   xmlNode *, void *),
                                  void (*free_func)(void *))
{
    cib_callback_client_t *blob = NULL;

    if (call_id < 0) {
        if (only_success == FALSE) {
            callback(NULL, call_id, call_id, NULL, user_data);
        } else {
            crm_warn("CIB call failed: %s", pcmk_strerror(call_id));
        }
        if (user_data && free_func) {
            free_func(user_data);
        }
        return FALSE;
    }

    blob = pcmk__assert_alloc(1, sizeof(cib_callback_client_t));
    blob->id = callback_name;
    blob->only_success = only_success;
    blob->user_data = user_data;
    blob->callback = callback;
    blob->free_func = free_func;

    if (timeout > 0) {
        struct timer_rec_s *async_timer =
            pcmk__assert_alloc(1, sizeof(struct timer_rec_s));

        blob->timer = async_timer;

        async_timer->cib = cib;
        async_timer->call_id = call_id;
        async_timer->timeout = timeout * 1000;
        async_timer->ref = g_timeout_add(async_timer->timeout,
                                         cib_async_timeout_handler,
                                         async_timer);
    }

    crm_trace("Adding callback %s for call %d", callback_name, call_id);
    pcmk__intkey_table_insert(cib_op_callback_table, call_id, blob);

    return TRUE;
}

static gboolean
cib_client_register_callback(cib_t *cib, int call_id, int timeout,
                             gboolean only_success, void *user_data,
                             const char *callback_name,
                             void (*callback) (xmlNode *, int, int, xmlNode *,
                                               void *))
{
    return cib_client_register_callback_full(cib, call_id, timeout,
                                             only_success, user_data,
                                             callback_name, callback, NULL);
}

static int
cib_client_noop(cib_t * cib, int call_options)
{
    op_common(cib);
    return cib_internal_op(cib, PCMK__CIB_REQUEST_NOOP, NULL, NULL, NULL, NULL,
                           call_options, cib->user);
}

static int
cib_client_ping(cib_t * cib, xmlNode ** output_data, int call_options)
{
    op_common(cib);
    return cib_internal_op(cib, CRM_OP_PING, NULL, NULL, NULL, output_data,
                           call_options, cib->user);
}

static int
cib_client_query(cib_t * cib, const char *section, xmlNode ** output_data, int call_options)
{
    return cib->cmds->query_from(cib, NULL, section, output_data, call_options);
}

static int
cib_client_query_from(cib_t * cib, const char *host, const char *section,
                      xmlNode ** output_data, int call_options)
{
    op_common(cib);
    return cib_internal_op(cib, PCMK__CIB_REQUEST_QUERY, host, section, NULL,
                           output_data, call_options, cib->user);
}

static int
is_primary(cib_t *cib)
{
    op_common(cib);
    return cib_internal_op(cib, PCMK__CIB_REQUEST_IS_PRIMARY, NULL, NULL, NULL,
                           NULL, cib_scope_local|cib_sync_call, cib->user);
}

static int
set_secondary(cib_t *cib, int call_options)
{
    op_common(cib);
    return cib_internal_op(cib, PCMK__CIB_REQUEST_SECONDARY, NULL, NULL, NULL,
                           NULL, call_options, cib->user);
}

static int
set_all_secondary(cib_t * cib, int call_options)
{
    return -EPROTONOSUPPORT;
}

static int
set_primary(cib_t *cib, int call_options)
{
    op_common(cib);
    crm_trace("Adding cib_scope_local to options");
    return cib_internal_op(cib, PCMK__CIB_REQUEST_PRIMARY, NULL, NULL, NULL,
                           NULL, call_options|cib_scope_local, cib->user);
}

static int
cib_client_bump_epoch(cib_t * cib, int call_options)
{
    op_common(cib);
    return cib_internal_op(cib, PCMK__CIB_REQUEST_BUMP, NULL, NULL, NULL, NULL,
                           call_options, cib->user);
}

static int
cib_client_upgrade(cib_t * cib, int call_options)
{
    op_common(cib);
    return cib_internal_op(cib, PCMK__CIB_REQUEST_UPGRADE, NULL, NULL, NULL,
                           NULL, call_options, cib->user);
}

static int
cib_client_sync(cib_t * cib, const char *section, int call_options)
{
    return cib->cmds->sync_from(cib, NULL, section, call_options);
}

static int
cib_client_sync_from(cib_t * cib, const char *host, const char *section, int call_options)
{
    op_common(cib);
    return cib_internal_op(cib, PCMK__CIB_REQUEST_SYNC_TO_ALL, host, section,
                           NULL, NULL, call_options, cib->user);
}

static int
cib_client_create(cib_t * cib, const char *section, xmlNode * data, int call_options)
{
    op_common(cib);
    return cib_internal_op(cib, PCMK__CIB_REQUEST_CREATE, NULL, section, data,
                           NULL, call_options, cib->user);
}

static int
cib_client_modify(cib_t * cib, const char *section, xmlNode * data, int call_options)
{
    op_common(cib);
    return cib_internal_op(cib, PCMK__CIB_REQUEST_MODIFY, NULL, section, data,
                           NULL, call_options, cib->user);
}

static int
cib_client_replace(cib_t * cib, const char *section, xmlNode * data, int call_options)
{
    op_common(cib);
    return cib_internal_op(cib, PCMK__CIB_REQUEST_REPLACE, NULL, section, data,
                           NULL, call_options, cib->user);
}

static int
cib_client_delete(cib_t * cib, const char *section, xmlNode * data, int call_options)
{
    op_common(cib);
    return cib_internal_op(cib, PCMK__CIB_REQUEST_DELETE, NULL, section, data,
                           NULL, call_options, cib->user);
}

static int
cib_client_delete_absolute(cib_t * cib, const char *section, xmlNode * data, int call_options)
{
    op_common(cib);
    return cib_internal_op(cib, PCMK__CIB_REQUEST_ABS_DELETE, NULL, section,
                           data, NULL, call_options, cib->user);
}

static int
cib_client_erase(cib_t * cib, xmlNode ** output_data, int call_options)
{
    op_common(cib);
    return cib_internal_op(cib, PCMK__CIB_REQUEST_ERASE, NULL, NULL, NULL,
                           output_data, call_options, cib->user);
}

static int
cib_client_init_transaction(cib_t *cib)
{
    int rc = pcmk_rc_ok;

    op_common(cib);

    if (cib->transaction != NULL) {
        // A client can have at most one transaction at a time
        rc = pcmk_rc_already;
    }

    if (rc == pcmk_rc_ok) {
        cib->transaction = pcmk__xe_create(NULL, PCMK__XE_CIB_TRANSACTION);
    }

    if (rc != pcmk_rc_ok) {
        const char *client_id = NULL;

        cib->cmds->client_id(cib, NULL, &client_id);
        crm_err("Failed to initialize CIB transaction for client %s: %s",
                client_id, pcmk_rc_str(rc));
    }
    return pcmk_rc2legacy(rc);
}

static int
cib_client_end_transaction(cib_t *cib, bool commit, int call_options)
{
    const char *client_id = NULL;
    int rc = pcmk_ok;

    op_common(cib);
    cib->cmds->client_id(cib, NULL, &client_id);
    client_id = pcmk__s(client_id, "(unidentified)");

    if (commit) {
        if (cib->transaction == NULL) {
            rc = pcmk_rc_no_transaction;

            crm_err("Failed to commit transaction for CIB client %s: %s",
                    client_id, pcmk_rc_str(rc));
            return pcmk_rc2legacy(rc);
        }
        rc = cib_internal_op(cib, PCMK__CIB_REQUEST_COMMIT_TRANSACT, NULL, NULL,
                             cib->transaction, NULL, call_options, cib->user);

    } else {
        // Discard always succeeds
        if (cib->transaction != NULL) {
            crm_trace("Discarded transaction for CIB client %s", client_id);
        } else {
            crm_trace("No transaction found for CIB client %s", client_id);
        }
    }
    free_xml(cib->transaction);
    cib->transaction = NULL;
    return rc;
}

static int
cib_client_fetch_schemas(cib_t *cib, xmlNode **output_data, const char *after_ver,
                         int call_options)
{
    xmlNode *data = pcmk__xe_create(NULL, PCMK__XA_SCHEMA);
    int rc = pcmk_ok;

    crm_xml_add(data, PCMK_XA_VERSION, after_ver);

    rc = cib_internal_op(cib, PCMK__CIB_REQUEST_SCHEMAS, NULL, NULL, data,
                         output_data, call_options, NULL);
    free_xml(data);
    return rc;
}

static void
cib_client_set_user(cib_t *cib, const char *user)
{
    pcmk__str_update(&(cib->user), user);
}

static void
cib_destroy_op_callback(gpointer data)
{
    cib_callback_client_t *blob = data;

    if (blob->timer && blob->timer->ref > 0) {
        g_source_remove(blob->timer->ref);
    }
    free(blob->timer);

    if (blob->user_data && blob->free_func) {
        blob->free_func(blob->user_data);
    }

    free(blob);
}

static void
destroy_op_callback_table(void)
{
    if (cib_op_callback_table != NULL) {
        g_hash_table_destroy(cib_op_callback_table);
        cib_op_callback_table = NULL;
    }
}

char *
get_shadow_file(const char *suffix)
{
    char *cib_home = NULL;
    char *fullname = NULL;
    char *name = crm_strdup_printf("shadow.%s", suffix);
    const char *dir = getenv("CIB_shadow_dir");

    if (dir == NULL) {
        uid_t uid = geteuid();
        struct passwd *pwent = getpwuid(uid);
        const char *user = NULL;

        if (pwent) {
            user = pwent->pw_name;
        } else {
            user = getenv("USER");
            crm_perror(LOG_ERR,
                       "Assuming %s because cannot get user details for user ID %d",
                       (user? user : "unprivileged user"), uid);
        }

        if (pcmk__strcase_any_of(user, "root", CRM_DAEMON_USER, NULL)) {
            dir = CRM_CONFIG_DIR;

        } else {
            const char *home = NULL;

            if ((home = getenv("HOME")) == NULL) {
                if (pwent) {
                    home = pwent->pw_dir;
                }
            }

            dir = pcmk__get_tmpdir();
            if (home && home[0] == '/') {
                int rc = 0;

                cib_home = crm_strdup_printf("%s/.cib", home);

                rc = mkdir(cib_home, 0700);
                if (rc < 0 && errno != EEXIST) {
                    crm_perror(LOG_ERR, "Couldn't create user-specific shadow directory: %s",
                               cib_home);
                    errno = 0;

                } else {
                    dir = cib_home;
                }
            }
        }
    }

    fullname = crm_strdup_printf("%s/%s", dir, name);
    free(cib_home);
    free(name);

    return fullname;
}

cib_t *
cib_shadow_new(const char *shadow)
{
    cib_t *new_cib = NULL;
    char *shadow_file = NULL;

    CRM_CHECK(shadow != NULL, return NULL);

    shadow_file = get_shadow_file(shadow);
    new_cib = cib_file_new(shadow_file);
    free(shadow_file);

    return new_cib;
}

/*!
 * \brief Create a new CIB connection object, ignoring any active shadow CIB
 *
 * Create a new live, file, or remote CIB connection object based on the values
 * of CIB-related environment variables (CIB_file, CIB_port, CIB_server,
 * CIB_user, and CIB_passwd). The object will not be connected.
 *
 * \return Newly allocated CIB connection object
 * \note The CIB API does not fully support opening multiple CIB connection
 *       objects simultaneously, so the returned object should be treated as a
 *       singleton.
 */
cib_t *
cib_new_no_shadow(void)
{
    const char *shadow = getenv("CIB_shadow");
    cib_t *cib = NULL;

    unsetenv("CIB_shadow");
    cib = cib_new();

    if (shadow != NULL) {
        setenv("CIB_shadow", shadow, 1);
    }
    return cib;
}

/*!
 * \brief Create a new CIB connection object
 *
 * Create a new live, remote, file, or shadow file CIB connection object based
 * on the values of CIB-related environment variables (CIB_shadow, CIB_file,
 * CIB_port, CIB_server, CIB_user, and CIB_passwd). The object will not be
 * connected.
 *
 * \return Newly allocated CIB connection object
 * \note The CIB API does not fully support opening multiple CIB connection
 *       objects simultaneously, so the returned object should be treated as a
 *       singleton.
 */
/* @TODO Ensure all APIs support multiple simultaneous CIB connection objects
 * (at least cib_free_callbacks() currently does not).
 */
cib_t *
cib_new(void)
{
    const char *value = getenv("CIB_shadow");
    int port;

    if (value && value[0] != 0) {
        return cib_shadow_new(value);
    }

    value = getenv("CIB_file");
    if (value) {
        return cib_file_new(value);
    }

    value = getenv("CIB_port");
    if (value) {
        gboolean encrypted = TRUE;
        const char *server = getenv("CIB_server");
        const char *user = getenv("CIB_user");
        const char *pass = getenv("CIB_passwd");

        /* We don't ensure port is valid (>= 0) because cib_new() currently
         * can't return NULL in practice, and introducing a NULL return here
         * could cause core dumps that would previously just cause signon()
         * failures.
         */
        pcmk__scan_port(value, &port);

        value = getenv("CIB_encrypted");
        if (value && crm_is_true(value) == FALSE) {
            crm_info("Disabling TLS");
            encrypted = FALSE;
        }

        if (user == NULL) {
            user = CRM_DAEMON_USER;
            crm_info("Defaulting to user: %s", user);
        }

        if (server == NULL) {
            server = "localhost";
            crm_info("Defaulting to localhost");
        }

        return cib_remote_new(server, user, pass, port, encrypted);
    }

    return cib_native_new();
}

/*!
 * \internal
 * \brief Create a generic CIB connection instance
 *
 * \return Newly allocated and initialized cib_t instance
 *
 * \note This is called by each variant's cib_*_new() function before setting
 *       variant-specific values.
 */
cib_t *
cib_new_variant(void)
{
    cib_t *new_cib = NULL;

    new_cib = calloc(1, sizeof(cib_t));

    if (new_cib == NULL) {
        return NULL;
    }

    remove_cib_op_callback(0, TRUE); /* remove all */

    new_cib->call_id = 1;
    new_cib->variant = cib_undefined;

    new_cib->type = cib_no_connection;
    new_cib->state = cib_disconnected;

    new_cib->op_callback = NULL;
    new_cib->variant_opaque = NULL;
    new_cib->notify_list = NULL;

    /* the rest will get filled in by the variant constructor */
    new_cib->cmds = calloc(1, sizeof(cib_api_operations_t));

    if (new_cib->cmds == NULL) {
        free(new_cib);
        return NULL;
    }

    // Deprecated method
    new_cib->cmds->set_op_callback = cib_client_set_op_callback;

    new_cib->cmds->add_notify_callback = cib_client_add_notify_callback;
    new_cib->cmds->del_notify_callback = cib_client_del_notify_callback;
    new_cib->cmds->register_callback = cib_client_register_callback;
    new_cib->cmds->register_callback_full = cib_client_register_callback_full;

    new_cib->cmds->noop = cib_client_noop; // Deprecated method
    new_cib->cmds->ping = cib_client_ping;
    new_cib->cmds->query = cib_client_query;
    new_cib->cmds->sync = cib_client_sync;

    new_cib->cmds->query_from = cib_client_query_from;
    new_cib->cmds->sync_from = cib_client_sync_from;

    new_cib->cmds->is_master = is_primary; // Deprecated method

    new_cib->cmds->set_primary = set_primary;
    new_cib->cmds->set_master = set_primary; // Deprecated method

    new_cib->cmds->set_secondary = set_secondary;
    new_cib->cmds->set_slave = set_secondary; // Deprecated method

    new_cib->cmds->set_slave_all = set_all_secondary; // Deprecated method

    new_cib->cmds->upgrade = cib_client_upgrade;
    new_cib->cmds->bump_epoch = cib_client_bump_epoch;

    new_cib->cmds->create = cib_client_create;
    new_cib->cmds->modify = cib_client_modify;
    new_cib->cmds->update = cib_client_modify; // Deprecated method
    new_cib->cmds->replace = cib_client_replace;
    new_cib->cmds->remove = cib_client_delete;
    new_cib->cmds->erase = cib_client_erase;

    // Deprecated method
    new_cib->cmds->delete_absolute = cib_client_delete_absolute;

    new_cib->cmds->init_transaction = cib_client_init_transaction;
    new_cib->cmds->end_transaction = cib_client_end_transaction;

    new_cib->cmds->set_user = cib_client_set_user;

    new_cib->cmds->fetch_schemas = cib_client_fetch_schemas;

    return new_cib;
}

void 
cib_free_notify(cib_t *cib)
{

    if (cib) {
        GList *list = cib->notify_list;

        while (list != NULL) {
            cib_notify_client_t *client = g_list_nth_data(list, 0);

            list = g_list_remove(list, client);
            free(client);
        }
        cib->notify_list = NULL;
    }
}

/*!
 * \brief Free all callbacks for a CIB connection
 *
 * \param[in,out] cib  CIB connection to clean up
 */
void
cib_free_callbacks(cib_t *cib)
{
    cib_free_notify(cib);

    destroy_op_callback_table();
}

/*!
 * \brief Free all memory used by CIB connection
 *
 * \param[in,out] cib  CIB connection to delete
 */
void
cib_delete(cib_t *cib)
{
    cib_free_callbacks(cib);
    if (cib) {
        cib->cmds->free(cib);
    }
}

void
remove_cib_op_callback(int call_id, gboolean all_callbacks)
{
    if (all_callbacks) {
        destroy_op_callback_table();
        cib_op_callback_table = pcmk__intkey_table(cib_destroy_op_callback);
    } else {
        pcmk__intkey_table_remove(cib_op_callback_table, call_id);
    }
}

int
num_cib_op_callbacks(void)
{
    if (cib_op_callback_table == NULL) {
        return 0;
    }
    return g_hash_table_size(cib_op_callback_table);
}

static void
cib_dump_pending_op(gpointer key, gpointer value, gpointer user_data)
{
    int call = GPOINTER_TO_INT(key);
    cib_callback_client_t *blob = value;

    crm_debug("Call %d (%s): pending", call, pcmk__s(blob->id, "without ID"));
}

void
cib_dump_pending_callbacks(void)
{
    if (cib_op_callback_table == NULL) {
        return;
    }
    return g_hash_table_foreach(cib_op_callback_table, cib_dump_pending_op, NULL);
}

cib_callback_client_t*
cib__lookup_id (int call_id)
{
    return pcmk__intkey_table_lookup(cib_op_callback_table, call_id);
}
