// SPDX-License-Identifier: GPL-3.0-or-later

#include "rrd.h"
#include "rrdfunctions-internals.h"
#include "streaming/protocol/commands.h"

#define MAX_FUNCTION_LENGTH (PLUGINSD_LINE_MAX - 512) // we need some space for the rest of the line

// ----------------------------------------------------------------------------

// we keep a dictionary per RRDSET with these functions
// the dictionary is created on demand (only when a function is added to an RRDSET)

// ----------------------------------------------------------------------------

static void rrd_functions_insert_callback(const DICTIONARY_ITEM *item __maybe_unused, void *func, void *rrdhost) {
    RRDHOST *host = rrdhost;
    struct rrd_host_function *rdcf = func;

    rrd_collector_started();
    rdcf->collector = rrd_collector_acquire_current_thread();
    rdcf->rrdhost_state_id = object_state_id(&host->state_id);
    rdcf->unregistered = false;

    if(!rdcf->priority)
        rdcf->priority = RRDFUNCTIONS_PRIORITY_DEFAULT;

//    internal_error(true, "FUNCTIONS: adding function '%s' on host '%s', collection tid %d, %s",
//                   dictionary_acquired_item_name(item), rrdhost_hostname(host),
//                   rdcf->collector->tid, rdcf->collector->running ? "running" : "NOT running");
}

static void rrd_functions_cleanup(struct rrd_host_function *rdcf) {
    rrd_collector_release(rdcf->collector);
    string_freez(rdcf->help);
    string_freez(rdcf->tags);
}

static void rrd_functions_delete_callback(const DICTIONARY_ITEM *item __maybe_unused, void *func,
                                          void *rrdhost __maybe_unused) {
    struct rrd_host_function *rdcf = func;
    rrd_functions_cleanup(rdcf);
}

static bool rrd_functions_conflict_callback(const DICTIONARY_ITEM *item __maybe_unused, void *func,
                                            void *new_func, void *rrdhost) {
    RRDHOST *host = rrdhost; (void)host;
    struct rrd_host_function *rdcf = func;
    struct rrd_host_function *new_rdcf = new_func;

    rrd_collector_started();

    bool changed = false;

    if(__atomic_load_n(&rdcf->unregistered, __ATOMIC_ACQUIRE)) {
        __atomic_store_n(&rdcf->unregistered, false, __ATOMIC_RELEASE);
        changed = true;
    }

    if(rdcf->collector != thread_rrd_collector) {
        nd_log(NDLS_DAEMON, NDLP_DEBUG,
               "FUNCTIONS: function '%s' of host '%s' changed collector from %d to %d",
               dictionary_acquired_item_name(item), rrdhost_hostname(host),
               rrd_collector_tid(rdcf->collector), rrd_collector_tid(thread_rrd_collector));

        new_rdcf->collector = rdcf->collector;
        rdcf->collector = rrd_collector_acquire_current_thread();
        changed = true;
    }

    if(rdcf->rrdhost_state_id != object_state_id(&host->state_id)) {
        nd_log(NDLS_DAEMON, NDLP_DEBUG,
               "FUNCTIONS: function '%s' of host '%s' changed state id from %u to %u",
               dictionary_acquired_item_name(item), rrdhost_hostname(host),
               rdcf->rrdhost_state_id,
               object_state_id(&host->state_id));

        rdcf->rrdhost_state_id = object_state_id(&host->state_id);
        changed = true;
    }

    if(rdcf->execute_cb != new_rdcf->execute_cb) {
        nd_log(NDLS_DAEMON, NDLP_DEBUG,
               "FUNCTIONS: function '%s' of host '%s' changed execute callback",
               dictionary_acquired_item_name(item), rrdhost_hostname(host));

        SWAP(rdcf->execute_cb, new_rdcf->execute_cb);
        changed = true;
    }

    if(rdcf->help != new_rdcf->help) {
        nd_log(NDLS_DAEMON, NDLP_DEBUG,
               "FUNCTIONS: function '%s' of host '%s' changed help text",
               dictionary_acquired_item_name(item), rrdhost_hostname(host));

        SWAP(rdcf->help, new_rdcf->help);
        changed = true;
    }

    if(rdcf->tags != new_rdcf->tags) {
        nd_log(NDLS_DAEMON, NDLP_DEBUG,
               "FUNCTIONS: function '%s' of host '%s' changed tags",
               dictionary_acquired_item_name(item), rrdhost_hostname(host));

        SWAP(rdcf->tags, new_rdcf->tags);
        changed = true;
    }

    if(rdcf->options != new_rdcf->options) {
        // options are derived from the name and the tags (get_function_options());
        // keeping them in sync with the swapped tags is what makes a re-registration
        // with the "hidden" tag actually restrict the function
        nd_log(NDLS_DAEMON, NDLP_DEBUG,
               "FUNCTIONS: function '%s' of host '%s' changed options",
               dictionary_acquired_item_name(item), rrdhost_hostname(host));

        SWAP(rdcf->options, new_rdcf->options);
        changed = true;
    }

    if(rdcf->timeout != new_rdcf->timeout) {
        nd_log(NDLS_DAEMON, NDLP_DEBUG,
               "FUNCTIONS: function '%s' of host '%s' changed timeout (from %d to %d)",
               dictionary_acquired_item_name(item), rrdhost_hostname(host),
               rdcf->timeout, new_rdcf->timeout);

        SWAP(rdcf->timeout, new_rdcf->timeout);
        changed = true;
    }

    if(rdcf->version != new_rdcf->version) {
        nd_log(NDLS_DAEMON, NDLP_DEBUG,
               "FUNCTIONS: function '%s' of host '%s' changed version (from %"PRIu32", to %"PRIu32")",
               dictionary_acquired_item_name(item), rrdhost_hostname(host),
               rdcf->version, new_rdcf->version);

        SWAP(rdcf->version, new_rdcf->version);
        changed = true;
    }

    if(rdcf->priority != new_rdcf->priority) {
        nd_log(NDLS_DAEMON, NDLP_DEBUG,
               "FUNCTIONS: function '%s' of host '%s' changed priority",
               dictionary_acquired_item_name(item), rrdhost_hostname(host));

        SWAP(rdcf->priority, new_rdcf->priority);
        changed = true;
    }

    if(rdcf->access != new_rdcf->access) {
        nd_log(NDLS_DAEMON, NDLP_DEBUG,
               "FUNCTIONS: function '%s' of host '%s' changed access level",
               dictionary_acquired_item_name(item), rrdhost_hostname(host));

        SWAP(rdcf->access, new_rdcf->access);
        changed = true;
    }

    if(rdcf->sync != new_rdcf->sync) {
        nd_log(NDLS_DAEMON, NDLP_DEBUG,
               "FUNCTIONS: function '%s' of host '%s' changed sync/async mode",
               dictionary_acquired_item_name(item), rrdhost_hostname(host));

        SWAP(rdcf->sync, new_rdcf->sync);
        changed = true;
    }

    if(rdcf->execute_cb_data != new_rdcf->execute_cb_data) {
        nd_log(NDLS_DAEMON, NDLP_DEBUG,
               "FUNCTIONS: function '%s' of host '%s' changed execute callback data",
               dictionary_acquired_item_name(item), rrdhost_hostname(host));

        SWAP(rdcf->execute_cb_data, new_rdcf->execute_cb_data);
        changed = true;
    }

//    internal_error(true, "FUNCTIONS: adding function '%s' on host '%s', collection tid %d, %s",
//                   dictionary_acquired_item_name(item), rrdhost_hostname(host),
//                   rdcf->collector->tid, rdcf->collector->running ? "running" : "NOT running");

    rrd_functions_cleanup(new_rdcf);

    return changed;
}

void rrd_functions_host_init(RRDHOST *host) {
    if(host->functions) return;

    host->functions = dictionary_create_advanced(DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE,
                                                 &dictionary_stats_category_functions, sizeof(struct rrd_host_function));

    dictionary_register_insert_callback(host->functions, rrd_functions_insert_callback, host);
    dictionary_register_delete_callback(host->functions, rrd_functions_delete_callback, host);
    dictionary_register_conflict_callback(host->functions, rrd_functions_conflict_callback, host);
}

void rrd_functions_host_destroy(RRDHOST *host) {
    dictionary_destroy(host->functions);
    host->functions = NULL;
}

// ----------------------------------------------------------------------------

static inline bool is_function_restricted(const char *name, const char *tags) {
    return (name && name[0] == '_' && name[1] == '_') || (tags && strstr(tags, RRDFUNCTIONS_TAG_HIDDEN) != NULL);
}

// Reports whether a function name is a reserved dynamic-configuration name -
// the `config` catch-all or any per-config `config <id>` function. Exposed so
// the pluginsd FUNCTION handler can refuse external registrations that would
// otherwise overwrite (swap) the built-in dyncfg execute callback.
bool rrd_function_name_is_dyncfg(const char *name) {
    if(!name || !*name)
        return false;

    if(strncmp(name, PLUGINSD_FUNCTION_CONFIG, sizeof(PLUGINSD_FUNCTION_CONFIG) - 1) != 0)
        return false;

    char c = name[sizeof(PLUGINSD_FUNCTION_CONFIG) - 1];
    if(c == 0 || isspace((uint8_t)c))
        return true;

    return false;
}

static inline RRD_FUNCTION_OPTIONS get_function_options(RRDSET *st, const char *name, const char *tags) {
    if(rrd_function_name_is_dyncfg(name))
        return RRD_FUNCTION_DYNCFG;

    RRD_FUNCTION_OPTIONS options = st ? RRD_FUNCTION_LOCAL : RRD_FUNCTION_GLOBAL;

    return options | (is_function_restricted(name, tags) ? RRD_FUNCTION_RESTRICTED : 0);
}

void rrd_function_add(RRDHOST *host, RRDSET *st, const char *name, int timeout, int priority, uint32_t version,
                      const char *help, const char *tags,
                      HTTP_ACCESS access, bool sync,
                      rrd_function_execute_cb_t execute_cb, void *execute_cb_data) {

    // RRDSET *st may be NULL in this function
    // to create a GLOBAL function

    if(!tags || !*tags) {
        if(strcmp(name, "systemd-journal") == 0)
            tags = "logs";
        else
            tags = "top";
    }

    if(st && !st->functions_view)
        st->functions_view = dictionary_create_view(host->functions);

    size_t key_size = rrd_functions_strlen_bounded(name, PLUGINSD_LINE_MAX) + 1;
    CLEAN_CHAR_P *key = mallocz(key_size);
    rrd_functions_sanitize(key, name, key_size);

    // text_sanitize() wipes a result that is entirely underscores, so a name like "__" sanitizes
    // to "". Such a name is unusable (it could never be looked up) and it would also defeat the
    // "__" prefix check in is_function_restricted() below, so refuse it outright.
    if(unlikely(!*key)) {
        nd_log(NDLS_DAEMON, NDLP_WARNING,
               "FUNCTIONS: refusing to register function '%s' on host '%s': the name sanitizes to an empty string",
               name, rrdhost_hostname(host));
        return;
    }

    // Classify from the sanitized key, not the raw name: the key is what the dictionary is
    // indexed by, so classifying the raw name lets " config" land on the "config" key without
    // the DYNCFG option, which would hand a dyncfg-reserved entry to a regular function.
    // Captured before the insert because the conflict callback swaps options with the
    // previous value, leaving tmp.options holding the old one.
    RRD_FUNCTION_OPTIONS options = get_function_options(st, key, tags);

    struct rrd_host_function tmp = {
        .collector = NULL,
        .sync = sync,
        .timeout = timeout,
        .version = version,
        .priority = priority,
        .options = options,
        .access = access,
        .execute_cb = execute_cb,
        .execute_cb_data = execute_cb_data,
        .help = string_strdupz(help),
        .tags = string_strdupz(tags),
    };
    const DICTIONARY_ITEM *item = dictionary_set_and_acquire_item(host->functions, key, &tmp, sizeof(tmp));

    if(st)
        dictionary_view_set(st->functions_view, key, item);
    else
        rrdhost_flag_set(host, RRDHOST_FLAG_GLOBAL_FUNCTIONS_UPDATED);

    dictionary_acquired_item_release(host->functions, item);

    // Refresh the cloud function manifest. Only DYNCFG is excluded: it is derived from the
    // key, so such an entry can never enter or leave the manifest. RESTRICTED comes from the
    // tags and can be added by a re-registration, which drops the function OUT of the
    // manifest - that transition has to be reported too, so it must not be filtered here.
    // Placed after the insert so the arm always follows the change it reports.
    if(!(options & RRD_FUNCTION_DYNCFG))
        aclk_arm_node_manifest(host);
}

bool rrd_function_del(RRDHOST *host, RRDSET *st, const char *name, bool from_streaming, bool internal) {
    if(unlikely(!name || !*name))
        return false;

    size_t key_size = rrd_functions_strlen_bounded(name, PLUGINSD_LINE_MAX) + 1;
    CLEAN_CHAR_P *key = mallocz(key_size);
    rrd_functions_sanitize(key, name, key_size);

    const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(host->functions, key);
    if(!item)
        return false;

    struct rrd_host_function *rdcf = dictionary_acquired_item_value(item);

    if(!from_streaming && !internal) {
        if(!thread_rrd_collector || rdcf->collector != thread_rrd_collector) {
            nd_log(NDLS_DAEMON, NDLP_WARNING,
                   "FUNCTIONS: refusing to unregister function '%s' - "
                   "collector mismatch (registered by %s, unregister requested by %s)",
                   name,
                   rdcf->collector ? "another collector" : "unknown",
                   thread_rrd_collector ? "current collector" : "non-collector thread");
            dictionary_acquired_item_release(host->functions, item);
            return false;
        }
    }

    // dyncfg config functions are intentionally never streamed to parents
    // (stream_sender_send_global_rrdhost_functions skips RRD_FUNCTION_DYNCFG),
    // so their removals must not be streamed either. Capture this before releasing.
    bool is_dyncfg = (rdcf->options & RRD_FUNCTION_DYNCFG);

    // whether this function is part of the cloud manifest; captured here because
    // rdcf is released (and may be freed) before we can queue the manifest refresh
    bool in_manifest = !(rdcf->options & (RRD_FUNCTION_DYNCFG | RRD_FUNCTION_RESTRICTED));

    // The FUNCTION_DEL protocol command (from external plugins or streaming
    // children) must never remove dyncfg config functions; those are owned and
    // removed exclusively by the dyncfg subsystem (internal deletes).
    if(!internal && is_dyncfg) {
        nd_log(NDLS_DAEMON, NDLP_WARNING,
               "FUNCTIONS: refusing to unregister dyncfg function '%s' via FUNCTION_DEL", name);
        dictionary_acquired_item_release(host->functions, item);
        return false;
    }

    // Mark the function unregistered before removing it from the dictionary.
    // The flag makes the function unavailable to any thread that already holds
    // an acquired reference during the delete window (it cannot be freed until
    // that reference is released), and lets rrd_functions_find_by_name() report
    // a specific "unregistered by the plugin" error in that window.
    __atomic_store_n(&rdcf->unregistered, true, __ATOMIC_RELEASE);

    if(st && st->functions_view)
        dictionary_del(st->functions_view, key);

    // Delete from the index while still holding our acquired reference, then
    // release: this unlinks the item before any concurrent re-registration could
    // resurrect it through the conflict callback (a re-add now allocates a fresh
    // item). The value is freed once the last reference - this one plus any
    // in-flight execution - is released.
    dictionary_del(host->functions, key);
    dictionary_acquired_item_release(host->functions, item);

    if(!st && !is_dyncfg)
        stream_send_function_del(host, key);

    if(!st)
        rrdhost_flag_set(host, RRDHOST_FLAG_GLOBAL_FUNCTIONS_UPDATED);

    dictionary_garbage_collect(host->functions);

    // after the delete, so the arm always follows the change it reports
    if(in_manifest)
        aclk_arm_node_manifest(host);

    return true;
}

bool rrd_function_is_available(struct rrd_host_function *rdcf, RRDHOST *host) {
    if(__atomic_load_n(&rdcf->unregistered, __ATOMIC_ACQUIRE))
        return false;

    if(!rrd_collector_running(rdcf->collector))
        return false;

    if(host && rdcf->rrdhost_state_id != object_state_id(&host->state_id))
        return false;

    return true;
}

int rrd_functions_find_by_name(RRDHOST *host, BUFFER *wb, const char *name, size_t key_length, const DICTIONARY_ITEM **item) {
    char buffer[MAX_FUNCTION_LENGTH + 1];
    strncpyz(buffer, name, sizeof(buffer) - 1);
    key_length = strnlen(buffer, sizeof(buffer));
    char *s = NULL;

    OBJECT_STATE_ID state_id = object_state_id(&host->state_id);

    bool found = false;
    bool was_unregistered = false;
    *item = NULL;
    if(host->functions) {
        while (buffer[0]) {
            if((*item = dictionary_get_and_acquire_item(host->functions, buffer))) {
                found = true;

                struct rrd_host_function *rdcf = dictionary_acquired_item_value(*item);
                if(rrd_function_is_available(rdcf, host)) {
                    break;
                }
                else {

                    nd_log(NDLS_DAEMON, NDLP_DEBUG,
                           "Function '%s' is not available. "
                           "host '%s', collector = { tid: %d, running: %s }, host tid { rcv: %d, snd: %d }, host state { id: %u, expected %u }, hops: %d",
                           name,
                           rrdhost_hostname(host),
                           rrd_collector_tid(rdcf->collector),
                           rrd_collector_running(rdcf->collector) ? "yes" : "no",
                           host->stream.rcv.status.tid, host->stream.snd.status.tid,
                           state_id, rdcf->rrdhost_state_id,
                           rrdhost_ingestion_hops(host)
                           );

                    was_unregistered = __atomic_load_n(&rdcf->unregistered, __ATOMIC_ACQUIRE);
                    dictionary_acquired_item_release(host->functions, *item);
                    *item = NULL;
                }
            }

            // if s == NULL, set it to the end of the buffer;
            // this should happen only the first time
            if (unlikely(!s))
                s = &buffer[key_length - 1];

            // skip a word from the end
            while (s >= buffer && !isspace((uint8_t)*s)) *s-- = '\0';

            // skip all spaces
            while (s >= buffer && isspace((uint8_t)*s)) *s-- = '\0';
        }
    }

    buffer_flush(wb);

    if(!(*item)) {
        if(found) {
            if(was_unregistered)
                return rrd_call_function_error(wb,
                                               "This function has been unregistered by the plugin.",
                                               HTTP_RESP_SERVICE_UNAVAILABLE);
            else
                return rrd_call_function_error(wb,
                                               "The plugin that registered this feature, is not currently running.",
                                               HTTP_RESP_SERVICE_UNAVAILABLE);
        }
        else
            return rrd_call_function_error(wb,
                                           "This feature is not available on this host at this time.",
                                           HTTP_RESP_NOT_FOUND);
    }

    return HTTP_RESP_OK;
}

bool rrd_function_available(RRDHOST *host, const char *function) {
    if(!host || !host->functions)
        return false;

    bool ret = false;
    const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(host->functions, function);
    if(item) {
        struct rrd_host_function *rdcf = dictionary_acquired_item_value(item);
        if(rrd_function_is_available(rdcf, host))
            ret = true;

        dictionary_acquired_item_release(host->functions, item);
    }

    return ret;
}
