/**********************************************************************************************
*	File: maat_rule.c
*   Description:
*	Authors: Liu WenTan <liuwentan@geedgenetworks.com>
*	Date:    2022-10-31
*   Copyright: (c) Since 2022 Geedge Networks, Ltd. All rights reserved.
***********************************************************************************************
*/

#include <unistd.h>
#include <stdio.h>
#include <pthread.h>
#include <linux/limits.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <assert.h>

#include "json2iris.h"
#include "log/log.h"
#include "maat_utils.h"
#include "maat_rule.h"
#include "maat_config_monitor.h"
#include "maat_redis_monitor.h"
#include "maat_table.h"
#include "maat_compile.h"
#include "maat_plugin.h"
#include "maat_stat.h"
#include "ip_matcher.h"
#include "alignment.h"
#include "maat_garbage_collection.h"

#define MODULE_MAAT_RULE	 module_name_str("maat.rule")

#define	ITEM_RULE_MAGIC 0x4d3c2b1a
struct maat_item_inner *maat_item_inner_new(long long item_id, long long group_id,
                                            long long district_id)
{
	struct maat_item_inner *item = ALLOC(struct maat_item_inner, 1);
	item->magic_num = ITEM_RULE_MAGIC;
	item->item_id = item_id;
	item->group_id = group_id;
	item->district_id = district_id;

	return item;
}

void maat_item_inner_free(void *item_inner)
{
    if (NULL == item_inner) {
        return;
    }
    
    struct maat_item_inner *item = (struct maat_item_inner *)item_inner;
	assert(item->magic_num == ITEM_RULE_MAGIC);
	item->magic_num = 0;

	free(item);
}

struct maat_runtime* maat_runtime_create(long long version, struct maat *maat_instance)
{
	struct maat_runtime *maat_rt = ALLOC(struct maat_runtime, 1);

	maat_rt->version = version;
	int ret = table_manager_runtime_create(maat_instance->tbl_mgr, maat_instance->opts.nr_worker_thread,
                                           maat_instance->garbage_bin);
    if (ret < 0) {
        FREE(maat_rt);
        return NULL;
    }

    maat_rt->ref_tbl_mgr = maat_instance->tbl_mgr;
	maat_rt->max_table_num = table_manager_table_size(maat_instance->tbl_mgr);
    maat_rt->sequence_map = maat_kv_store_new();
    maat_rt->logger = maat_instance->logger;
    maat_rt->ref_garbage_bin = maat_instance->garbage_bin;
    maat_rt->ref_cnt = alignment_int64_array_alloc(maat_instance->opts.nr_worker_thread);
    
    return maat_rt;
}

void maat_runtime_commit(struct maat_runtime *maat_rt, int update_type, 
                         long long maat_rt_version, struct log_handle *logger)
{
	for (size_t i = 0; i < maat_rt->max_table_num; i++) {
		table_manager_commit_runtime(maat_rt->ref_tbl_mgr, i, update_type, maat_rt_version);
	}

    maat_rt->last_update_time = time(NULL);
}

void maat_start_cb(long long new_version, int update_type, void *u_param)
{
	struct maat *maat_instance = (struct maat *)u_param;
	maat_instance->new_version = new_version;
    size_t i = 0;
    size_t max_table_cnt = table_manager_table_size(maat_instance->tbl_mgr);
    enum table_type table_type = TABLE_TYPE_INVALID;

	if (update_type == MAAT_UPDATE_TYPE_FULL) {
		maat_instance->creating_maat_rt = maat_runtime_create(new_version, maat_instance);
        for (i = 0; i < max_table_cnt; i++) {
            table_type = table_manager_get_table_type(maat_instance->tbl_mgr, i);
            if (table_type == TABLE_TYPE_COMPILE) {
                // compile runtime need a reference to maat runtime
                void *compile_rt = table_manager_get_updating_runtime(maat_instance->tbl_mgr, i);
                compile_runtime_init(compile_rt, maat_instance->creating_maat_rt);
            }
        }
    } else {
		maat_instance->maat_version = new_version;
	}

    for (i = 0; i < max_table_cnt; i++) {
        table_type = table_manager_get_table_type(maat_instance->tbl_mgr, i);
        if (table_type == TABLE_TYPE_PLUGIN) {
            void *schema = table_manager_get_schema(maat_instance->tbl_mgr, i);
            plugin_table_all_callback_start((struct plugin_schema *)schema, update_type);
        }
    }
}

int maat_update_cb(const char *table_name, const char *line, void *u_param)
{
	if (NULL == table_name || NULL == line || NULL == u_param) {
		return 0;
	}
	
	struct maat *maat_instance =(struct maat *)u_param; 
	struct maat_runtime* maat_rt = NULL;
	int table_id = table_manager_get_table_id(maat_instance->tbl_mgr, table_name);
    if (table_id < 0) {
        log_error(maat_instance->logger, MODULE_MAAT_RULE, 
				  "[%s:%d] update warning, unknown table name %s", 
                  __FUNCTION__, __LINE__, table_name);
        return -1;
    }

	void *schema = table_manager_get_schema(maat_instance->tbl_mgr, table_id);
    if (NULL == schema) {
        log_error(maat_instance->logger, MODULE_MAAT_RULE, 
                  "[%s:%d] update warning, table name %s doesn't have table schema", 
                  __FUNCTION__, __LINE__, table_name);
        return -1;
    }
    
	int update_type = MAAT_UPDATE_TYPE_INC;
	if (maat_instance->creating_maat_rt != NULL) { //Full update
		maat_rt = maat_instance->creating_maat_rt;
        update_type = MAAT_UPDATE_TYPE_FULL;
	} else {
		maat_rt = maat_instance->maat_rt;
	}
    
	table_manager_update_runtime(maat_rt->ref_tbl_mgr, table_name, table_id, line, update_type);

	return 0;
}

long long maat_runtime_rule_num(struct maat_runtime *maat_rt)
{
	long long total = 0;
	
	for (size_t i = 0; i < maat_rt->max_table_num; i++) {
        long long rule_cnt = table_manager_runtime_rule_count(maat_rt->ref_tbl_mgr, i);
		total += rule_cnt;
        if (rule_cnt != 0) {
            log_info(maat_rt->logger, MODULE_MAAT_RULE, "table:%d rule count:%lld", i, rule_cnt);
        }
	}

	return total;
}

void maat_plugin_table_all_callback_finish(struct table_manager *tbl_mgr)
{
    size_t max_table_cnt = table_manager_table_size(tbl_mgr);
    enum table_type table_type = TABLE_TYPE_INVALID;

    for (size_t i = 0; i < max_table_cnt; i++) {
        table_type = table_manager_get_table_type(tbl_mgr, i);
        if (table_type != TABLE_TYPE_PLUGIN) {
            continue;
        }

        void *plugin_schema = table_manager_get_schema(tbl_mgr, i);
        plugin_table_all_callback_finish((struct plugin_schema *)plugin_schema);
    }
}

void maat_finish_cb(void *u_param)
{
	struct maat *maat_instance = (struct maat *)u_param;

    maat_plugin_table_all_callback_finish(maat_instance->tbl_mgr);

    if (maat_instance->creating_maat_rt != NULL) {
        maat_runtime_commit(maat_instance->creating_maat_rt, MAAT_UPDATE_TYPE_FULL, 
                            maat_instance->creating_maat_rt->version, maat_instance->logger);
        maat_instance->creating_maat_rt->rule_num = maat_runtime_rule_num(maat_instance->creating_maat_rt);
        log_info(maat_instance->logger, MODULE_MAAT_RULE, 
                 "Full config version %llu load %d entries complete", 
                 maat_instance->creating_maat_rt->version, 
                 maat_instance->creating_maat_rt->rule_num);
    } else if (maat_instance->maat_rt != NULL) {
        maat_instance->maat_rt->version = maat_instance->maat_version;
        maat_runtime_commit(maat_instance->maat_rt, MAAT_UPDATE_TYPE_INC,
                            maat_instance->maat_rt->version, maat_instance->logger);
        maat_instance->maat_rt->rule_num = maat_runtime_rule_num(maat_instance->maat_rt);
        log_info(maat_instance->logger, MODULE_MAAT_RULE, 
                 "Inc config version %llu load %d entries complete", 
                 maat_instance->maat_rt->version, 
                 maat_instance->maat_rt->rule_num);
    } else {
        log_info(maat_instance->logger, MODULE_MAAT_RULE,
                 "Version %d has no valid rules, plugin callback complete.", 
                 maat_instance->maat_version);
    }

    maat_instance->new_version = INVALID_VERSION;
}

void maat_read_full_config(struct maat *maat_instance)
{
    int ret = -1;
    char err_str[NAME_MAX] = {0};
    struct source_redis_ctx *redis_ctx = NULL;

    switch (maat_instance->opts.input_mode) {
        case DATA_SOURCE_REDIS:
            redis_ctx = &(maat_instance->opts.redis_ctx);
            log_info(maat_instance->logger, MODULE_MAAT_RULE, 
                     "Maat initiate from Redis %s:%hu db%d", 
                     redis_ctx->redis_ip, redis_ctx->redis_port, redis_ctx->redis_db);
            redis_ctx->read_ctx = maat_cmd_connect_redis(redis_ctx->redis_ip,
                                                         redis_ctx->redis_port, 
                                                         redis_ctx->redis_db,
                                                         maat_instance->logger);
            if (redis_ctx->read_ctx != NULL) {
                redis_monitor_traverse(maat_instance->maat_version, redis_ctx,
									   maat_start_cb, maat_update_cb, maat_finish_cb,
									   maat_instance);
            }

            if (NULL == maat_instance->creating_maat_rt) {
                log_error(maat_instance->logger, MODULE_MAAT_RULE, 
                          "[%s:%d] At initiation: NO effective rule in redis %s:%hu db%d",
                          __FUNCTION__, __LINE__, redis_ctx->redis_ip, redis_ctx->redis_port, 
                          redis_ctx->redis_db);
            }
            break;
        case DATA_SOURCE_IRIS_FILE:
			config_monitor_traverse(maat_instance->maat_version,
                                    maat_instance->opts.iris_ctx.full_idx_dir,
                                    maat_start_cb, maat_update_cb, maat_finish_cb,
                                    maat_instance, maat_instance->opts.decrypt_key,
                                    maat_instance->logger);
            if (NULL == maat_instance->creating_maat_rt) {
                log_error(maat_instance->logger, MODULE_MAAT_RULE, 
                          "[%s:%d] At initiation: NO effective rule in %s",
                          __FUNCTION__, __LINE__, maat_instance->opts.iris_ctx.full_idx_dir);
            }
            break;
        case DATA_SOURCE_JSON_FILE:
            ret = load_maat_json_file(maat_instance, maat_instance->opts.json_ctx.json_file,
                                      err_str, sizeof(err_str));
            if (ret < 0) {
                log_error(maat_instance->logger, MODULE_MAAT_RULE, 
                          "[%s:%d] Maat re-initiate with JSON file %s failed: %s", 
                          __FUNCTION__, __LINE__, maat_instance->opts.json_ctx.json_file, err_str);
            }
            
            config_monitor_traverse(maat_instance->maat_version,
                                    maat_instance->opts.json_ctx.iris_file,
                                    maat_start_cb, maat_update_cb, maat_finish_cb,
                                    maat_instance, maat_instance->opts.decrypt_key,
                                    maat_instance->logger);
            if (NULL == maat_instance->creating_maat_rt) {
                log_error(maat_instance->logger, MODULE_MAAT_RULE, 
                          "[%s:%d] At initiation: NO effective rule in %s",
                          __FUNCTION__, __LINE__, maat_instance->opts.json_ctx.iris_file);
            }
            break;
        default:
            break;
    }

    maat_instance->maat_rt = maat_instance->creating_maat_rt;
    maat_instance->creating_maat_rt = NULL;
    maat_instance->is_running = 1;
    if (maat_instance->maat_rt != NULL) {
        maat_instance->maat_version = maat_instance->maat_rt->version;
        maat_instance->last_full_version = maat_instance->maat_rt->version;
    }
}

long long maat_runtime_get_sequence(struct maat_runtime *maat_rt, const char *key)
{
    if (NULL == maat_rt || NULL == key) {
        return -1;
    }

    long long sequence = 0;
    int map_ret = maat_kv_read(maat_rt->sequence_map, key, &sequence);
    if (map_ret < 0) {
        maat_kv_register(maat_rt->sequence_map, key, sequence);
    } else {
        sequence++;
        int ret = maat_kv_write(maat_rt->sequence_map, key, sequence);
        if (ret < 0) {
            return -1;
        }
    }

    return sequence;
}

void maat_runtime_destroy(struct maat_runtime *maat_rt)
{
    if (NULL == maat_rt) {
        return;
    }
    
    if (maat_rt->sequence_map != NULL) {
        maat_kv_store_free(maat_rt->sequence_map);
        maat_rt->sequence_map = NULL;
    }

    if (maat_rt->ref_cnt != NULL) {
        alignment_int64_array_free(maat_rt->ref_cnt);
        maat_rt->ref_cnt = NULL;
    }
    
    FREE(maat_rt);
}

void garbage_maat_kv_store_free(void *kv_store, void *arg)
{
    struct maat_kv_store *store = (struct maat_kv_store *)kv_store;
    maat_kv_store_free(store);
}

void garbage_maat_runtime_destroy(void *maat_runtime, void *arg)
{
    struct maat_runtime *maat_rt = (struct maat_runtime *)maat_runtime;
    maat_runtime_destroy(maat_rt);
}

void *rule_monitor_loop(void *arg)
{
    /* Defined by prctl: The name can be up to 16 bytes long, and should 
        be null terminated if it contains fewer bytes. */
    char maat_name[16] = {0};
    struct maat *maat_instance = (struct maat *)arg;

    if (strlen(maat_instance->opts.instance_name) > 0) {
        snprintf(maat_name, sizeof(maat_name), "MAAT_%s", maat_instance->opts.instance_name);
    } else {
        snprintf(maat_name, sizeof(maat_name), "MAAT");
    }

    int ret = prctl(PR_SET_NAME, (unsigned long long)maat_name, NULL, NULL, NULL);
    assert(ret >= 0);

    pthread_mutex_lock(&(maat_instance->background_update_mutex));
    /* if deferred load on */
    if (maat_instance->opts.deferred_load_on != 0) {
        log_info(maat_instance->logger, MODULE_MAAT_RULE, 
                 "Deferred Loading ON, updating in %s:%d", __FUNCTION__, __LINE__);
        maat_read_full_config(maat_instance);
    }
    pthread_mutex_unlock(&(maat_instance->background_update_mutex));

    char md5_tmp[MD5_DIGEST_LENGTH * 2 + 1] = {0};
    char err_str[NAME_MAX] = {0};
    struct stat attrib;
    while (maat_instance->is_running) {
        log_info(maat_instance->logger, MODULE_MAAT_RULE, "%s:%d.............", __FUNCTION__, __LINE__);
        usleep(maat_instance->opts.rule_update_checking_interval_ms * 1000);
		if (0 == pthread_mutex_trylock(&(maat_instance->background_update_mutex))) {
			switch (maat_instance->opts.input_mode) {
                case DATA_SOURCE_REDIS:
                    redis_monitor_traverse(maat_instance->maat_version,
                                            &(maat_instance->opts.redis_ctx),
                                            maat_start_cb, maat_update_cb, maat_finish_cb,
                                            maat_instance);
                    break;
				case DATA_SOURCE_IRIS_FILE:
					config_monitor_traverse(maat_instance->maat_version, 
											maat_instance->opts.iris_ctx.inc_idx_dir,
											maat_start_cb, maat_update_cb, maat_finish_cb,
											maat_instance, maat_instance->opts.decrypt_key,
                                            maat_instance->logger);
					break;
                case DATA_SOURCE_JSON_FILE:
                    memset(md5_tmp, 0, sizeof(md5_tmp));
				    stat(maat_instance->opts.json_ctx.json_file, &attrib);
					if (memcmp(&attrib.st_ctim, &(maat_instance->opts.json_ctx.last_md5_time), sizeof(attrib.st_ctim))) {
						maat_instance->opts.json_ctx.last_md5_time = attrib.st_ctim;
						md5_file(maat_instance->opts.json_ctx.json_file, md5_tmp);
						if (0 != strcmp(md5_tmp, maat_instance->opts.json_ctx.effective_json_md5)) {
							ret = load_maat_json_file(maat_instance, maat_instance->opts.json_ctx.json_file, 
                                                      err_str, sizeof(err_str));
							if (ret < 0) {
								log_error(maat_instance->logger, MODULE_MAAT_RULE, 
                                          "[%s:%d] Maat re-initiate with JSON file %s (md5=%s)failed: %s\n",
										  __FUNCTION__, __LINE__, maat_instance->opts.json_ctx.json_file, 
                                          md5_tmp, err_str);
							} else {
								config_monitor_traverse(0, maat_instance->opts.json_ctx.iris_file,
														maat_start_cb, maat_update_cb, maat_finish_cb,
														maat_instance, maat_instance->opts.decrypt_key,
                                                        maat_instance->logger);
								log_info(maat_instance->logger, MODULE_MAAT_RULE, 
                                         "Maat re-initiate with JSON file %s success, md5: %s\n",
										 maat_instance->opts.json_ctx.json_file, md5_tmp);
							}
						}
					}
                    break;
				default:
					break;
			}

			if (maat_instance->creating_maat_rt != NULL) {
				struct maat_runtime *old_maat_rt = maat_instance->maat_rt;
				maat_instance->maat_rt = maat_instance->creating_maat_rt;

				if (old_maat_rt != NULL) {
                    if (maat_instance->maat_rt->version > old_maat_rt->version) {
                        log_info(maat_instance->logger, MODULE_MAAT_RULE, 
                                 "Maat version updated %lld -> %lld\n", 
                                 old_maat_rt->version, maat_instance->maat_rt->version);
                    } else {
                        log_info(maat_instance->logger, MODULE_MAAT_RULE, 
                                 "Maat version roll back %lld -> %lld\n",
                                 old_maat_rt->version, maat_instance->maat_rt->version);
                    }
                    maat_garbage_bagging(maat_instance->garbage_bin, old_maat_rt, NULL,
                                         garbage_maat_runtime_destroy);
				}

				maat_instance->creating_maat_rt = NULL;
				maat_instance->maat_version = maat_instance->maat_rt->version;
				maat_instance->last_full_version = maat_instance->maat_rt->version;
			}

			if (maat_instance->maat_rt != NULL) {
                time_t time_window = time(NULL) - maat_instance->maat_rt->last_update_time;
                
                if (time_window >= maat_instance->opts.rule_effect_interval_ms / 1000) {
                    maat_runtime_commit(maat_instance->maat_rt, MAAT_UPDATE_TYPE_INC, 
                                        maat_instance->maat_rt->version, maat_instance->logger);
                    log_info(maat_instance->logger, MODULE_MAAT_RULE,
							 "Actual update config version %u, %d entries load to maat runtime.",
							 maat_instance->maat_rt->version, maat_instance->maat_rt->rule_num);
                }
			}

			pthread_mutex_unlock(&(maat_instance->background_update_mutex));
		}
        maat_garbage_collect_routine(maat_instance->garbage_bin);
        if ((1 == maat_instance->opts.stat_on) && (time(NULL) % 2 == 0)) {
            maat_stat_output(maat_instance->stat, maat_instance->maat_version, maat_instance->opts.perf_on);
        }
    }

    maat_runtime_destroy(maat_instance->maat_rt);
    maat_garbage_bin_free(maat_instance->garbage_bin);
    table_manager_destroy(maat_instance->tbl_mgr); //table manager MUST be freed at last.

    if (maat_instance->stat != NULL) {
        maat_stat_free(maat_instance->stat);
        maat_instance->stat = NULL;
    }

    if (maat_instance->opts.input_mode == DATA_SOURCE_REDIS) {
        if (maat_instance->opts.redis_ctx.read_ctx != NULL) {
            redisFree(maat_instance->opts.redis_ctx.read_ctx);
            maat_instance->opts.redis_ctx.read_ctx = NULL;
        }

        if (maat_instance->opts.redis_ctx.write_ctx != NULL) {
            redisFree(maat_instance->opts.redis_ctx.write_ctx);
            maat_instance->opts.redis_ctx.write_ctx = NULL;
        }
    }

    if (maat_instance->opts.accept_tags != NULL) {
        FREE(maat_instance->opts.accept_tags);
        maat_instance->opts.accept_tags = NULL;
    }

    log_handle_destroy(maat_instance->logger);
    FREE(maat_instance);

    return NULL;
}