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

#include "health.h"
#include "health_internals.h"

struct variable_lookup_score {
    RRDSET *st;
    const char *source;
    NETDATA_DOUBLE value;
    size_t score;
};

struct variable_lookup_job {
    RRDCALC *rc;
    RRDHOST *host;
    STRING *variable;
    STRING *dim;
    const char *dimension;
    size_t dimension_length;
    enum {
        DIM_SELECT_NORMAL,
        DIM_SELECT_RAW,
        DIM_SELECT_LAST_COLLECTED,
    } dimension_selection;

    struct {
        size_t size;
        size_t used;
        struct variable_lookup_score *array;
    } result;

    struct {
        RRDSET *last_rrdset;
        size_t last_score;
    } score;
};

static void variable_lookup_add_result_with_score(struct variable_lookup_job *vbd, NETDATA_DOUBLE n, RRDSET *st, const char *source __maybe_unused) {
    if(vbd->score.last_rrdset != st && vbd->rc->rrdset) {
        vbd->score.last_rrdset = st;
        vbd->score.last_score = rrdlabels_common_count(vbd->rc->rrdset->rrdlabels, st->rrdlabels);
    }

    if(vbd->result.used >= vbd->result.size) {
        if(!vbd->result.size)
            vbd->result.size = 1;

        vbd->result.size *= 2;
        vbd->result.array = reallocz(vbd->result.array, sizeof(struct variable_lookup_score) * vbd->result.size);
    }

    vbd->result.array[vbd->result.used++] = (struct variable_lookup_score) {
        .value = n,
        .score = vbd->score.last_score,
        .st = st,
        .source = source,
    };
}

static bool variable_lookup_in_chart(struct variable_lookup_job *vbd, RRDSET *st, bool stop_on_match) {
    bool found = false;
    const DICTIONARY_ITEM *item = NULL;
    RRDDIM *rd = NULL;
    dfe_start_read(st->rrddim_root_index, rd) {
        if(rd->id == vbd->dim || rd->name == vbd->dim) {
            item = dictionary_acquired_item_dup(st->rrddim_root_index, rd_dfe.item);
            break;
        }
    }
    dfe_done(rd);

    if (item) {
        switch (vbd->dimension_selection) {
            case DIM_SELECT_NORMAL:
                variable_lookup_add_result_with_score(vbd, (NETDATA_DOUBLE)rd->collector.last_stored_value, st, "last stored value of dimension");
                break;
            case DIM_SELECT_RAW:
                variable_lookup_add_result_with_score(vbd, rrddim_last_collected_as_double(rd), st, "last collected value of dimension");
                break;
            case DIM_SELECT_LAST_COLLECTED:
                variable_lookup_add_result_with_score(vbd, (NETDATA_DOUBLE)rd->collector.last_collected_time.tv_sec, st, "last collected time of dimension");
                break;
        }

        dictionary_acquired_item_release(st->rrddim_root_index, item);
        found = true;
    }
    if(found && stop_on_match) goto cleanup;

    // chart variable
    {
        NETDATA_DOUBLE n;
        if(rrdvar_get_custom_chart_variable_value(st, vbd->variable, &n)) {
            variable_lookup_add_result_with_score(vbd, n, st, "chart variable");
            found = true;
        }
    }
    if(found && stop_on_match) goto cleanup;

cleanup:
    return found;
}

static int foreach_instance_in_context_cb(RRDSET *st, void *data) {
    struct variable_lookup_job *vbd = data;
    return variable_lookup_in_chart(vbd, st, false) ? 1 : 0;
}

static bool variable_lookup_context(struct variable_lookup_job *vbd, const char *chart_or_context, const char *dim_id_or_name) {
    struct variable_lookup_job vbd_back = *vbd;

    vbd->dimension = dim_id_or_name;
    vbd->dim = string_strdupz(vbd->dimension);
    vbd->dimension_length = string_strlen(vbd->dim);
    // vbd->dimension_selection = DIM_SELECT_NORMAL;

    bool found = false;

    // lookup chart in host

    RRDSET_ACQUIRED *rsa = rrdset_find_and_acquire(vbd->host, chart_or_context, false);
    if(rsa) {
        if(variable_lookup_in_chart(vbd, rrdset_acquired_to_rrdset(rsa), false))
            found = true;
        rrdset_acquired_release(rsa);
    }

    // lookup context in contexts, then foreach chart

    if(rrdcontext_foreach_instance_with_rrdset_in_context(vbd->host, chart_or_context, foreach_instance_in_context_cb, vbd) > 0)
        found = true;

    string_freez(vbd->dim);

    vbd->dimension = vbd_back.dimension;
    vbd->dim = vbd_back.dim;
    vbd->dimension_length = vbd_back.dimension_length;
    // vbd->dimension_selection = vbd_back.dimension_selection;

    return found;
}

bool alert_variable_from_running_alerts(struct variable_lookup_job *vbd) {
    bool found = false;
    RRDCALC *rc;
    foreach_rrdcalc_in_rrdhost_read(vbd->host, rc) {
        if(rc->config.name == vbd->variable && rc->rrdset) {
            variable_lookup_add_result_with_score(vbd, (NETDATA_DOUBLE)rc->value, rc->rrdset, "alarm value");
            found = true;
        }
    }
    foreach_rrdcalc_in_rrdhost_done(rc);
    return found;
}

static STRING *this_string = NULL,
              *now_string = NULL,
              *after_string = NULL,
              *before_string = NULL,
              *status_string = NULL,
              *removed_string = NULL,
              *uninitialized_string = NULL,
              *undefined_string = NULL,
              *clear_string = NULL,
              *warning_string = NULL,
              *critical_string = NULL,
              *last_collected_t_string = NULL,
              *update_every_string = NULL;

void alert_variable_lookup_cleanup(void) {
    string_freez(this_string);
    string_freez(now_string);
    string_freez(after_string);
    string_freez(before_string);
    string_freez(status_string);
    string_freez(removed_string);
    string_freez(uninitialized_string);
    string_freez(undefined_string);
    string_freez(clear_string);
    string_freez(warning_string);
    string_freez(critical_string);
    string_freez(last_collected_t_string);
    string_freez(update_every_string);
    this_string = NULL;
    now_string = NULL;
    after_string = NULL;
    before_string = NULL;
    status_string = NULL;
    removed_string = NULL;
    uninitialized_string = NULL;
    undefined_string = NULL;
    clear_string = NULL;
    warning_string = NULL;
    critical_string = NULL;
    last_collected_t_string = NULL;
    update_every_string = NULL;
}

bool alert_variable_lookup_internal(STRING *variable, void *data, NETDATA_DOUBLE *result, BUFFER *wb) {
    struct variable_lookup_job vbd = { 0 };

//    const char *v_name = string2str(variable);
//    bool trace_this = false;
//    if(strcmp(v_name, "btrfs_allocated") == 0)
//        trace_this = true;

    bool found = false;

    const char *source = NULL;
    RRDSET *source_st = NULL;

    RRDCALC *rc = data;
    RRDSET *st = rc->rrdset;

    if(!st)
        return false;

    if(unlikely(!last_collected_t_string)) {
        this_string = string_strdupz("this");
        now_string = string_strdupz("now");
        after_string = string_strdupz("after");
        before_string = string_strdupz("before");
        status_string = string_strdupz("status");
        removed_string = string_strdupz("REMOVED");
        undefined_string = string_strdupz("UNDEFINED");
        uninitialized_string = string_strdupz("UNINITIALIZED");
        clear_string = string_strdupz("CLEAR");
        warning_string = string_strdupz("WARNING");
        critical_string = string_strdupz("CRITICAL");
        last_collected_t_string = string_strdupz("last_collected_t");
        update_every_string = string_strdupz("update_every");
    }

    if(unlikely(variable == this_string)) {
        *result = (NETDATA_DOUBLE)rc->value;
        source = "current alert value";
        source_st = st;
        found = true;
        goto log;
    }

    if(unlikely(variable == after_string)) {
        *result = (NETDATA_DOUBLE)rc->db_after;
        source = "current alert query start time";
        source_st = st;
        found = true;
        goto log;
    }

    if(unlikely(variable == before_string)) {
        *result = (NETDATA_DOUBLE)rc->db_before;
        source = "current alert query end time";
        source_st = st;
        found = true;
        goto log;
    }

    if(unlikely(variable == now_string)) {
        *result = (NETDATA_DOUBLE)now_realtime_sec();
        source = "current wall-time clock timestamp";
        source_st = st;
        found = true;
        goto log;
    }

    if(unlikely(variable == status_string)) {
        *result = (NETDATA_DOUBLE)rc->status;
        source = "current alert status";
        source_st = st;
        found = true;
        goto log;
    }

    if(unlikely(variable == removed_string)) {
        *result = (NETDATA_DOUBLE)RRDCALC_STATUS_REMOVED;
        source = "removed status constant";
        source_st = st;
        found = true;
        goto log;
    }

    if(unlikely(variable == uninitialized_string)) {
        *result = (NETDATA_DOUBLE)RRDCALC_STATUS_UNINITIALIZED;
        source = "uninitialized status constant";
        source_st = st;
        found = true;
        goto log;
    }

    if(unlikely(variable == undefined_string)) {
        *result = (NETDATA_DOUBLE)RRDCALC_STATUS_UNDEFINED;
        source = "undefined status constant";
        source_st = st;
        found = true;
        goto log;
    }

    if(unlikely(variable == clear_string)) {
        *result = (NETDATA_DOUBLE)RRDCALC_STATUS_CLEAR;
        source = "clear status constant";
        source_st = st;
        found = true;
        goto log;
    }

    if(unlikely(variable == warning_string)) {
        *result = (NETDATA_DOUBLE)RRDCALC_STATUS_WARNING;
        source = "warning status constant";
        source_st = st;
        found = true;
        goto log;
    }

    if(unlikely(variable == critical_string)) {
        *result = (NETDATA_DOUBLE)RRDCALC_STATUS_CRITICAL;
        source = "critical status constant";
        source_st = st;
        found = true;
        goto log;
    }

    if(unlikely(variable == last_collected_t_string)) {
        *result = (NETDATA_DOUBLE)st->last_collected_time.tv_sec;
        source = "current instance last_collected_t";
        source_st = st;
        found = true;
        goto log;
    }

    if(unlikely(variable == update_every_string)) {
        *result = (NETDATA_DOUBLE)st->update_every;
        source = "current instance update_every";
        source_st = st;
        found = true;
        goto log;
    }

    // find the dimension id/name

    vbd = (struct variable_lookup_job){
        .rc = rc,
        .host = st->rrdhost,
        .variable = variable,
        .dimension = string2str(variable),
        .dimension_length = string_strlen(variable),
        .dimension_selection = DIM_SELECT_NORMAL,
        .dim = string_dup(variable),
        .result = { 0 },
    };
    if (strendswith_lengths(vbd.dimension, vbd.dimension_length, "_raw", 4)) {
        vbd.dimension_length -= 4;
        vbd.dimension_selection = DIM_SELECT_RAW;
        vbd.dim = string_strndupz(vbd.dimension, vbd.dimension_length);
    } else if (strendswith_lengths(vbd.dimension, vbd.dimension_length, "_last_collected_t", 17)) {
        vbd.dimension_length -= 17;
        vbd.dimension_selection = DIM_SELECT_LAST_COLLECTED;
        vbd.dim = string_strndupz(vbd.dimension, vbd.dimension_length);
    }

    if(variable_lookup_in_chart(&vbd, st, true)) {
        found = true;
        goto find_best_scored;
    }

    // host variables
    {
        NETDATA_DOUBLE n;
        found = rrdvar_get_custom_host_variable_value(vbd.host,  vbd.variable, &n);
        if(found) {
            variable_lookup_add_result_with_score(&vbd, n, st, "host variable");
            goto find_best_scored;
        }
    }

    // alert names
    if(alert_variable_from_running_alerts(&vbd)) {
        found = true;
        goto find_best_scored;
    }

    // find the components of the variable
    {
        char id[string_strlen(vbd.dim) + 1];
        memcpy(id, string2str(vbd.dim), string_strlen(vbd.dim));
        id[string_strlen(vbd.dim)] = '\0';

        char *dot = strrchr(id, '.');
        while(dot) {
            *dot = '\0';

            if(strchr(id, '.') == NULL) break;

            if(variable_lookup_context(&vbd, id, dot + 1))
                found = true;

            char *dot2 = strrchr(id, '.');
            *dot = '.';
            dot = dot2;
        }
    }

find_best_scored:
    if(found && vbd.result.array) {
        struct variable_lookup_score *best = &vbd.result.array[0];
        for (size_t i = 1; i < vbd.result.used; i++)
            if (vbd.result.array[i].score > best->score)
                best = &vbd.result.array[i];

        source = best->source;
        source_st = best->st;
        *result = best->value;
        freez(vbd.result.array);
    }
    else {
        found = false;
        *result = NAN;
    }

log:
#ifdef NETDATA_LOG_HEALTH_VARIABLES_LOOKUP
    if(found) {
        nd_log(NDLS_DAEMON, NDLP_INFO,
               "HEALTH_VARIABLE_LOOKUP: variable '%s' of alert '%s' of chart '%s', context '%s', host '%s' "
               "resolved with %s of chart '%s' and context '%s'",
               string2str(variable),
               string2str(rc->config.name),
               string2str(rc->rrdset->id),
               string2str(rc->rrdset->context),
               string2str(rc->rrdset->rrdhost->hostname),
               source,
               string2str(source_st->id),
               string2str(source_st->context)
               );
    }
    else {
        nd_log(NDLS_DAEMON, NDLP_INFO,
               "HEALTH_VARIABLE_LOOKUP: variable '%s' of alert '%s' of chart '%s', context '%s', host '%s' "
               "could not be resolved",
               string2str(variable),
               string2str(rc->config.name),
               string2str(rc->rrdset->id),
               string2str(rc->rrdset->context),
               string2str(rc->rrdset->rrdhost->hostname)
        );
    }
#endif

    if(unlikely(wb)) {
        buffer_json_member_add_string(wb, "variable", string2str(variable));
        buffer_json_member_add_string(wb, "instance", string2str(st->id));
        buffer_json_member_add_string(wb, "context", string2str(st->context));
        buffer_json_member_add_boolean(wb, "found", found);

        if (found) {
            buffer_json_member_add_double(wb, "value", *result);
            buffer_json_member_add_object(wb, "source");
            {
                buffer_json_member_add_string(wb, "description", source);
                buffer_json_member_add_string(wb, "instance", string2str(source_st->id));
                buffer_json_member_add_string(wb, "context", string2str(source_st->context));
                buffer_json_member_add_uint64(wb, "candidates", vbd.result.used ? vbd.result.used : 1);
            }
            buffer_json_object_close(wb); // source
        }
    }

    string_freez(vbd.dim);

    return found;
}

bool alert_variable_lookup(STRING *variable, void *data, NETDATA_DOUBLE *result) {
    return alert_variable_lookup_internal(variable, data, result, NULL);
}

int alert_variable_lookup_trace(RRDHOST *host __maybe_unused, RRDSET *st, const char *variable, BUFFER *wb) {
    int code = HTTP_RESP_INTERNAL_SERVER_ERROR;

    buffer_flush(wb);
    buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT);

    STRING *v = string_strdupz(variable);
    RRDCALC rc = {
        .rrdset = st,
    };

    NETDATA_DOUBLE n;
    alert_variable_lookup_internal(v, &rc, &n, wb);

    string_freez(v);

    buffer_json_finalize(wb);
    return code;
}
