/*
 * Copyright (c) 2016 MariaDB Corporation Ab
 *
 * Use of this software is governed by the Business Source License included
 * in the LICENSE.TXT file and at www.mariadb.com/bsl11.
 *
 * Change Date: 2020-01-01
 *
 * On the date above, in accordance with the Business Source License, use
 * of this software will be governed by version 2 or later of the General
 * Public License.
 */

#include <maxscale/modulecmd.h>

#include <string>

#include <maxscale/alloc.h>
#include <maxscale/config.h>
#include <maxscale/pcre2.h>
#include <maxscale/platform.h>
#include <maxscale/spinlock.h>

#include "internal/filter.h"
#include "internal/modules.h"
#include "internal/monitor.h"

/** Size of the error buffer */
#define MODULECMD_ERRBUF_SIZE 512

/** Thread local error buffer */
thread_local char *errbuf = NULL;

/** Parameter passed to functions that do not always expect arguments */
static const MODULECMD_ARG MODULECMD_NO_ARGUMENTS = {0, NULL};

/**
 * A registered domain
 */
typedef struct modulecmd_domain
{
    char                    *domain; /**< The domain */
    MODULECMD               *commands; /**< List of registered commands */
    struct modulecmd_domain *next; /**< Next domain */
} MODULECMD_DOMAIN;

/**
 * Internal functions
 */

/** The global list of registered domains */
static MODULECMD_DOMAIN *modulecmd_domains = NULL;
static SPINLOCK modulecmd_lock = SPINLOCK_INIT;

static inline void prepare_error()
{
    if (errbuf == NULL)
    {
        errbuf = (char*)MXS_MALLOC(MODULECMD_ERRBUF_SIZE);
        MXS_ABORT_IF_NULL(errbuf);
        errbuf[0] = '\0';
    }
}

/**
 * @brief Reset error message
 *
 * This should be the first function called in every API function that can
 * generate errors.
 */
static void reset_error()
{
    prepare_error();
    errbuf[0] = '\0';
}

static void report_argc_mismatch(const MODULECMD *cmd, int argc)
{
    if (cmd->arg_count_min == cmd->arg_count_max)
    {
        modulecmd_set_error("Expected %d arguments, got %d.", cmd->arg_count_min, argc);
    }
    else
    {
        modulecmd_set_error("Expected between %d and %d arguments, got %d.", cmd->arg_count_min, cmd->arg_count_max,
                            argc);
    }
}

static MODULECMD_DOMAIN* domain_create(const char *domain)
{
    MODULECMD_DOMAIN *rval = (MODULECMD_DOMAIN*)MXS_MALLOC(sizeof(*rval));
    char *dm = MXS_STRDUP(domain);

    if (rval && dm)
    {
        rval->domain = dm;
        rval->commands = NULL;
        rval->next = NULL;
    }
    else
    {
        MXS_FREE(rval);
        MXS_FREE(dm);
        rval = NULL;
    }

    return rval;
}

static void domain_free(MODULECMD_DOMAIN *dm)
{
    if (dm)
    {
        MXS_FREE(dm->domain);
        MXS_FREE(dm);
    }
}

static MODULECMD_DOMAIN* get_or_create_domain(const char *domain)
{

    MODULECMD_DOMAIN *dm;

    for (dm = modulecmd_domains; dm; dm = dm->next)
    {
        if (strcasecmp(dm->domain, domain) == 0)
        {
            return dm;
        }
    }

    if ((dm = domain_create(domain)))
    {
        dm->next = modulecmd_domains;
        modulecmd_domains = dm;
    }

    return dm;
}

static MODULECMD* command_create(const char *identifier, const char *domain,
                                 enum modulecmd_type type, MODULECMDFN entry_point,
                                 int argc, modulecmd_arg_type_t* argv,
                                 const char *description)
{
    ss_dassert((argc && argv) || (argc == 0 && argv == NULL));
    ss_dassert(description);
    MODULECMD *rval = (MODULECMD*)MXS_MALLOC(sizeof(*rval));
    char *id = MXS_STRDUP(identifier);
    char *dm = MXS_STRDUP(domain);
    char *desc = MXS_STRDUP(description);
    modulecmd_arg_type_t *types = (modulecmd_arg_type_t*)MXS_MALLOC(sizeof(*types) * (argc ? argc : 1));

    if (rval && id  && dm && types && desc)
    {
        int argc_min = 0;

        for (int i = 0; i < argc; i++)
        {
            if (MODULECMD_ARG_IS_REQUIRED(&argv[i]))
            {
                argc_min++;
            }
            types[i] = argv[i];
        }

        if (argc == 0)
        {
            /** The command requires no arguments */
            types[0].type = MODULECMD_ARG_NONE;
            types[0].description = "";
        }

        rval->type = type;
        rval->func = entry_point;
        rval->identifier = id;
        rval->domain = dm;
        rval->description = desc;
        rval->arg_types = types;
        rval->arg_count_min = argc_min;
        rval->arg_count_max = argc;
        rval->next = NULL;
    }
    else
    {
        MXS_FREE(rval);
        MXS_FREE(id);
        MXS_FREE(dm);
        MXS_FREE(types);
        MXS_FREE(desc);
        rval = NULL;
    }

    return rval;
}

static void command_free(MODULECMD *cmd)
{
    if (cmd)
    {
        MXS_FREE(cmd->identifier);
        MXS_FREE(cmd->domain);
        MXS_FREE(cmd->arg_types);
        MXS_FREE(cmd);
    }
}

static bool domain_has_command(MODULECMD_DOMAIN *dm, const char *id)
{
    for (MODULECMD *cmd = dm->commands; cmd; cmd = cmd->next)
    {
        if (strcasecmp(cmd->identifier, id) == 0)
        {
            return true;
        }
    }
    return false;
}

static bool process_argument(const MODULECMD *cmd, modulecmd_arg_type_t *type, const void* value,
                             struct arg_node *arg, const char **err)
{
    bool rval = false;

    if (!MODULECMD_ARG_IS_REQUIRED(type) && value == NULL)
    {
        arg->type.type = MODULECMD_ARG_NONE;
        rval = true;
    }
    else if (value)
    {
        switch (MODULECMD_GET_TYPE(type))
        {
        case MODULECMD_ARG_NONE:
            arg->type.type = MODULECMD_ARG_NONE;
            rval = true;
            break;

        case MODULECMD_ARG_STRING:
            if ((arg->value.string = MXS_STRDUP((char*)value)))
            {
                arg->type.type = MODULECMD_ARG_STRING;
                rval = true;
            }
            else
            {
                *err = "memory allocation failed";
            }
            break;

        case MODULECMD_ARG_BOOLEAN:
            {
                int truthval = config_truth_value((char*)value);
                if (truthval != -1)
                {
                    arg->value.boolean = truthval;
                    arg->type.type = MODULECMD_ARG_BOOLEAN;
                    rval = true;
                }
                else
                {
                    *err = "not a boolean value";
                }
            }
            break;

        case MODULECMD_ARG_SERVICE:
            if ((arg->value.service = service_find((char*)value)))
            {
                if (MODULECMD_ALLOW_NAME_MISMATCH(type) ||
                    strcmp(cmd->domain, arg->value.service->routerModule) == 0)
                {
                    arg->type.type = MODULECMD_ARG_SERVICE;
                    rval = true;
                }
                else
                {
                    *err = "router and domain names don't match";
                }
            }
            else
            {
                *err = "service not found";
            }
            break;

        case MODULECMD_ARG_SERVER:
            if ((arg->value.server = server_find_by_unique_name((char*)value)))
            {
                if (MODULECMD_ALLOW_NAME_MISMATCH(type) ||
                    strcmp(cmd->domain, arg->value.server->protocol) == 0)
                {
                    arg->type.type = MODULECMD_ARG_SERVER;
                    rval = true;
                }
                else
                {
                    *err = "server and domain names don't match";
                }
            }
            else
            {
                *err = "server not found";
            }
            break;

        case MODULECMD_ARG_SESSION:
            if ((arg->value.session = session_get_by_id(strtoul((const char*)value, NULL, 0))))
            {
                arg->type.type = MODULECMD_ARG_SESSION;
            }
            rval = true;
            break;

        case MODULECMD_ARG_DCB:
            arg->type.type = MODULECMD_ARG_DCB;
            arg->value.dcb = (DCB*)value;
            rval = true;
            break;

        case MODULECMD_ARG_MONITOR:
            if ((arg->value.monitor = monitor_find((char*)value)))
            {
                const char* eff_name = mxs_module_get_effective_name(arg->value.monitor->module_name);
                if (MODULECMD_ALLOW_NAME_MISMATCH(type) || strcasecmp(cmd->domain, eff_name) == 0)
                {
                    arg->type.type = MODULECMD_ARG_MONITOR;
                    rval = true;
                }
                else
                {
                    *err = "monitor and domain names don't match";
                }
            }
            else
            {
                *err = "monitor not found";
            }
            break;

        case MODULECMD_ARG_FILTER:
            if ((arg->value.filter = filter_def_find((char*)value)))
            {
                const char* eff_name = mxs_module_get_effective_name(arg->value.filter->module);
                if (MODULECMD_ALLOW_NAME_MISMATCH(type) || strcasecmp(cmd->domain, eff_name) == 0)
                {
                    arg->type.type = MODULECMD_ARG_FILTER;
                    rval = true;
                }
                else
                {
                    *err = "filter and domain names don't match";
                }
            }
            else
            {
                *err = "filter not found";
            }
            break;

        default:
            ss_dassert(false);
            MXS_ERROR("Undefined argument type: %0lx", type->type);
            *err = "internal error";
            break;
        }
    }
    else
    {
        *err = "required argument";
    }

    return rval;
}

static MODULECMD_ARG* modulecmd_arg_create(int argc)
{
    MODULECMD_ARG* arg = (MODULECMD_ARG*)MXS_MALLOC(sizeof(*arg));
    struct arg_node *argv = (struct arg_node*)MXS_CALLOC(argc, sizeof(*argv));

    if (arg && argv)
    {
        arg->argc = argc;
        arg->argv = argv;
    }
    else
    {
        MXS_FREE(argv);
        MXS_FREE(arg);
        arg = NULL;
    }

    return arg;
}

static void free_argument(struct arg_node *arg)
{
    switch (arg->type.type)
    {
    case MODULECMD_ARG_STRING:
        MXS_FREE(arg->value.string);
        break;

    case MODULECMD_ARG_SESSION:
        session_put_ref(arg->value.session);
        break;

    default:
        break;
    }
}

/**
 * Public functions declared in modulecmd.h
 */

bool modulecmd_register_command(const char *domain, const char *identifier,
                                enum modulecmd_type type, MODULECMDFN entry_point,
                                int argc, modulecmd_arg_type_t *argv,
                                const char *description)
{
    reset_error();
    bool rval = false;
    spinlock_acquire(&modulecmd_lock);

    MODULECMD_DOMAIN *dm = get_or_create_domain(domain);

    if (dm)
    {
        if (domain_has_command(dm, identifier))
        {
            modulecmd_set_error("Command registered more than once: %s::%s", domain, identifier);
            MXS_ERROR("Command registered more than once: %s::%s", domain, identifier);
        }
        else
        {
            MODULECMD *cmd = command_create(identifier, domain, type, entry_point,
                                            argc, argv, description);

            if (cmd)
            {
                cmd->next = dm->commands;
                dm->commands = cmd;
                rval = true;
            }
        }
    }

    spinlock_release(&modulecmd_lock);

    return rval;
}

const MODULECMD* modulecmd_find_command(const char *domain, const char *identifier)
{
    reset_error();

    const char* effective_domain = mxs_module_get_effective_name(domain);

    MODULECMD *rval = NULL;
    spinlock_acquire(&modulecmd_lock);

    for (MODULECMD_DOMAIN *dm = modulecmd_domains; dm; dm = dm->next)
    {
        if (strcasecmp(effective_domain, dm->domain) == 0)
        {
            for (MODULECMD *cmd = dm->commands; cmd; cmd = cmd->next)
            {
                if (strcasecmp(cmd->identifier, identifier) == 0)
                {
                    rval = cmd;
                    break;
                }
            }
            break;
        }
    }

    spinlock_release(&modulecmd_lock);

    if (rval == NULL)
    {
        modulecmd_set_error("Command not found: %s::%s", domain, identifier);
    }

    return rval;
}

MODULECMD_ARG* modulecmd_arg_parse(const MODULECMD *cmd, int argc, const void **argv)
{
    reset_error();

    MODULECMD_ARG* arg = NULL;

    if (argc >= cmd->arg_count_min && argc <= cmd->arg_count_max)
    {
        arg = modulecmd_arg_create(cmd->arg_count_max);
        bool error = false;

        if (arg)
        {
            for (int i = 0; i < cmd->arg_count_max && i < argc; i++)
            {
                const char *err = "";

                if (!process_argument(cmd, &cmd->arg_types[i], argv[i], &arg->argv[i], &err))
                {
                    error = true;
                    modulecmd_set_error("Argument %d, %s: %s", i + 1, err, argv[i] ? (char*)argv[i] : "No argument given");
                    break;
                }
            }

            if (error)
            {
                modulecmd_arg_free(arg);
                arg = NULL;
            }
            else
            {
                arg->argc = argc;
            }
        }
    }
    else
    {
        report_argc_mismatch(cmd, argc);
    }

    return arg;
}

void modulecmd_arg_free(MODULECMD_ARG* arg)
{
    if (arg)
    {
        for (int i = 0; i < arg->argc; i++)
        {
            free_argument(&arg->argv[i]);
        }

        MXS_FREE(arg->argv);
        MXS_FREE(arg);
    }
}

static void modulecmd_clear_error()
{
    prepare_error();
    errbuf[0] = '\0';
}

bool modulecmd_call_command(const MODULECMD *cmd, const MODULECMD_ARG *args, json_t** output)
{
    bool rval = false;
    reset_error();

    if (cmd->arg_count_min > 0 && args == NULL)
    {
        report_argc_mismatch(cmd, 0);
    }
    else
    {
        if (args == NULL)
        {
            args = &MODULECMD_NO_ARGUMENTS;
        }

        modulecmd_clear_error();

        json_t* discard = NULL;
        rval = cmd->func(args, output ? output : &discard);
        json_decref(discard);
    }

    return rval;
}

void modulecmd_set_error(const char *format, ...)
{
    prepare_error();

    va_list list;
    va_start(list, format);
    vsnprintf(errbuf, MODULECMD_ERRBUF_SIZE, format, list);
    va_end(list);
}

const char* modulecmd_get_error()
{
    prepare_error();
    return errbuf;
}

json_t* modulecmd_get_json_error()
{
    json_t* obj = NULL;
    std::string errmsg = modulecmd_get_error();
    modulecmd_clear_error();

    if (errmsg.length())
    {
        json_t* err = json_object();
        json_object_set_new(err, "detail", json_string(errmsg.c_str()));

        json_t* arr = json_array();
        json_array_append_new(arr, err);

        obj = json_object();
        json_object_set_new(obj, "errors", arr);
    }

    return obj;
}

bool modulecmd_foreach(const char *domain_re, const char *ident_re,
                       bool(*fn)(const MODULECMD *cmd, void *data), void *data)
{
    bool rval = true;
    bool stop = false;
    spinlock_acquire(&modulecmd_lock);

    for (MODULECMD_DOMAIN *domain = modulecmd_domains; domain && rval && !stop; domain = domain->next)
    {
        int err;
        mxs_pcre2_result_t d_res = domain_re ?
                                   mxs_pcre2_simple_match(domain_re, domain->domain, PCRE2_CASELESS, &err) :
                                   MXS_PCRE2_MATCH;

        if (d_res == MXS_PCRE2_MATCH)
        {
            for (MODULECMD *cmd = domain->commands; cmd && rval; cmd = cmd->next)
            {
                mxs_pcre2_result_t i_res = ident_re ?
                                           mxs_pcre2_simple_match(ident_re, cmd->identifier, PCRE2_CASELESS, &err) :
                                           MXS_PCRE2_MATCH;

                if (i_res == MXS_PCRE2_MATCH)
                {
                    if (!fn(cmd, data))
                    {
                        stop = true;
                        break;
                    }
                }
                else if (i_res == MXS_PCRE2_ERROR)
                {
                    PCRE2_UCHAR errbuf[MXS_STRERROR_BUFLEN];
                    pcre2_get_error_message(err, errbuf, sizeof(errbuf));
                    MXS_ERROR("Failed to match command identifier with '%s': %s", ident_re, errbuf);
                    modulecmd_set_error("Failed to match command identifier with '%s': %s", ident_re, errbuf);
                    rval = false;
                }

            }
        }
        else if (d_res == MXS_PCRE2_ERROR)
        {
            PCRE2_UCHAR errbuf[MXS_STRERROR_BUFLEN];
            pcre2_get_error_message(err, errbuf, sizeof(errbuf));
            MXS_ERROR("Failed to match command domain with '%s': %s", domain_re, errbuf);
            modulecmd_set_error("Failed to match command domain with '%s': %s", domain_re, errbuf);
            rval = false;
        }
    }

    spinlock_release(&modulecmd_lock);
    return rval;
}

#define format_type(a, b) (MODULECMD_ARG_IS_REQUIRED(a) ? b : "[" b "]")

const char* modulecmd_argtype_to_str(modulecmd_arg_type_t *type)
{
    const char* rval = "UNKNOWN";

    switch (MODULECMD_GET_TYPE(type))
    {
    case MODULECMD_ARG_NONE:
        rval = format_type(type, "NONE");
        break;

    case MODULECMD_ARG_STRING:
        rval = format_type(type, "STRING");
        break;

    case MODULECMD_ARG_BOOLEAN:
        rval = format_type(type, "BOOLEAN");
        break;

    case MODULECMD_ARG_SERVICE:
        rval = format_type(type, "SERVICE");
        break;

    case MODULECMD_ARG_SERVER:
        rval = format_type(type, "SERVER");
        break;

    case MODULECMD_ARG_SESSION:
        rval = format_type(type, "SESSION");
        break;

    case MODULECMD_ARG_DCB:
        rval = format_type(type, "DCB");
        break;

    case MODULECMD_ARG_MONITOR:
        rval = format_type(type, "MONITOR");
        break;

    case MODULECMD_ARG_FILTER:
        rval = format_type(type, "FILTER");
        break;

    default:
        ss_dassert(false);
        MXS_ERROR("Unknown type");
        break;
    }

    return rval;
}

bool modulecmd_arg_is_present(const MODULECMD_ARG *arg, int idx)
{
    return arg->argc > idx &&
           MODULECMD_GET_TYPE(&arg->argv[idx].type) != MODULECMD_ARG_NONE;
}
