/*
 * Copyright (C)  2007-2008 Voice Sistem SRL
 *
 * This file is part of Kamailio, a free SIP server.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * Kamailio is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version
 *
 * Kamailio is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

/*!
 * \file
 * \brief Kamailio dialplan :: database interface
 * \ingroup dialplan
 * Module: \ref dialplan
 */


#include <stdlib.h>
#include <string.h>

#include "../../core/dprint.h"
#include "../../core/ut.h"
#include "../../lib/srdb1/db.h"
#include "../../core/re.h"
#include "dp_db.h"
#include "dialplan.h"

str dp_db_url = str_init(DEFAULT_RODB_URL);
str dp_table_name = str_init(DP_TABLE_NAME);
str dpid_column = str_init(DPID_COL);
str pr_column = str_init(PR_COL);
str match_op_column = str_init(MATCH_OP_COL);
str match_exp_column = str_init(MATCH_EXP_COL);
str match_len_column = str_init(MATCH_LEN_COL);
str subst_exp_column = str_init(SUBST_EXP_COL);
str repl_exp_column = str_init(REPL_EXP_COL);
str attrs_column = str_init(ATTRS_COL);

extern int dp_fetch_rows;
extern int dp_match_dynamic;

static db1_con_t *dp_db_handle = 0; /* database connection handle */
static db_func_t dp_dbf;

#define GET_STR_VALUE(_res, _values, _index)                      \
	do {                                                          \
		if(VAL_NULL((_values) + (_index))) {                      \
			LM_ERR(" values %d is NULL - not allowed\n", _index); \
			(_res).s = 0;                                         \
			(_res).len = 0;                                       \
			goto err;                                             \
		}                                                         \
		(_res).s = VAL_STR((_values) + (_index)).s;               \
		(_res).len = strlen(VAL_STR((_values) + (_index)).s);     \
	} while(0);

void destroy_rule(dpl_node_t *rule);
void destroy_hash(int);

dpl_node_t *build_rule(db_val_t *values);
int add_rule2hash(dpl_node_t *, int);

void list_rule(dpl_node_t *);
void list_hash(int h_index);


static dpl_id_p *dp_rules_hash = NULL;
static int *dp_crt_idx = NULL;
static int *dp_next_idx = NULL;


/**
 * check if string has pvs
 * returns -1 if error, 0 if found, 1 if not found
 */
int dpl_check_pv(str *in)
{
	char *p;
	pv_spec_t *spec = NULL;
	str s;
	int len;

	if(in == NULL || in->s == NULL)
		return -1;

	LM_DBG("parsing [%.*s]\n", in->len, in->s);

	if(in->len == 0)
		return 1;

	p = in->s;

	while(is_in_str(p, in)) {
		while(is_in_str(p, in) && *p != PV_MARKER)
			p++;
		if(*p == '\0' || !is_in_str(p, in))
			break;
		/* last char is $ ? */
		if(!is_in_str(p + 1, in))
			break;
		s.s = p;
		s.len = in->s + in->len - p;
		len = 0;
		spec = pv_spec_lookup(&s, &len);
		if(spec != NULL) {
			/* found a variable */
			LM_DBG("string [%.*s] has variables\n", in->len, in->s);
			return 0;
		}
		if(len)
			p += len;
		else
			p++;
	}

	/* not found */
	return 1;
}

int init_db_data(void)
{
	if(!dp_table_name.s || dp_table_name.len <= 0) {
		LM_ERR("invalid database table name\n");
		return -1;
	}

	/* Find a database module */
	if(db_bind_mod(&dp_db_url, &dp_dbf) < 0) {
		LM_ERR("unable to bind to a database driver\n");
		return -1;
	}

	if(dp_connect_db() != 0)
		return -1;

	if(db_check_table_version(
			   &dp_dbf, dp_db_handle, &dp_table_name, DP_TABLE_VERSION)
			< 0) {
		DB_TABLE_VERSION_ERROR(dp_table_name);
		goto error;
	}

	if(dp_load_db() != 0) {
		LM_ERR("failed to load database data\n");
		goto error;
	}

	dp_disconnect_db();

	return 0;
error:

	dp_disconnect_db();
	return -1;
}


int dp_connect_db(void)
{
	if(dp_dbf.init == 0) {
		LM_CRIT("null dp_dbf\n");
		return -1;
	}

	if(dp_db_handle) {
		LM_CRIT("BUG: connection to database already open\n");
		return -1;
	}

	if((dp_db_handle = dp_dbf.init(&dp_db_url)) == 0) {
		LM_ERR("unable to connect to the database\n");
		return -1;
	}

	return 0;
}


void dp_disconnect_db(void)
{
	if(dp_db_handle) {
		dp_dbf.close(dp_db_handle);
		dp_db_handle = 0;
	}
}

static void *pcre2_malloc(size_t size, void *ext)
{
	return shm_malloc(size);
}

static void pcre2_free(void *ptr, void *ext)
{
	if(ptr) {
		shm_free(ptr);
		ptr = NULL;
	}
}

int init_data(void)
{
	int *p;

	if((dpl_gctx = pcre2_general_context_create(pcre2_malloc, pcre2_free, NULL))
			== NULL) {
		LM_ERR("pcre2 general context creation failed\n");
		return -1;
	}
	if((dpl_ctx = pcre2_compile_context_create(dpl_gctx)) == NULL) {
		LM_ERR("pcre2 compile context creation failed\n");
		return -1;
	}

	dp_rules_hash = (dpl_id_p *)shm_malloc(2 * sizeof(dpl_id_p));
	if(!dp_rules_hash) {
		LM_ERR("out of shm memory\n");
		return -1;
	}
	dp_rules_hash[0] = dp_rules_hash[1] = 0;

	p = (int *)shm_malloc(2 * sizeof(int));
	if(!p) {
		LM_ERR("out of shm memory\n");
		return -1;
	}
	dp_crt_idx = p;
	dp_next_idx = p + 1;
	*dp_crt_idx = *dp_next_idx = 0;

	LM_DBG("trying to initialize data from db\n");
	if(init_db_data() != 0)
		return -1;

	return 0;
}


void destroy_data(void)
{
	if(dpl_ctx) {
		pcre2_compile_context_free(dpl_ctx);
	}

	if(dpl_gctx) {
		pcre2_general_context_free(dpl_gctx);
	}

#if 0
	/* let shm-free be done all-at-once by process shutdown */
	if(dp_rules_hash) {
		destroy_hash(0);
		destroy_hash(1);
		shm_free(dp_rules_hash);
		dp_rules_hash = 0;
	}
#endif

	if(dp_crt_idx) {
		shm_free(dp_crt_idx);
	}
}


/*load rules from DB*/
int dp_load_db(void)
{
	int i, nr_rows;
	db1_res_t *res = 0;
	db_val_t *values;
	db_row_t *rows;
	db_key_t query_cols[DP_TABLE_COL_NO] = {&dpid_column, &pr_column,
			&match_op_column, &match_exp_column, &match_len_column,
			&subst_exp_column, &repl_exp_column, &attrs_column};

	db_key_t order = &pr_column;

	dpl_node_t *rule;

	LM_DBG("init\n");
	if((*dp_crt_idx) != (*dp_next_idx)) {
		LM_WARN("a load command already generated, aborting reload...\n");
		return 0;
	}

	if(dp_dbf.use_table(dp_db_handle, &dp_table_name) < 0) {
		LM_ERR("error in use_table %.*s\n", dp_table_name.len, dp_table_name.s);
		return -1;
	}

	if(DB_CAPABILITY(dp_dbf, DB_CAP_FETCH)) {
		if(dp_dbf.query(dp_db_handle, 0, 0, 0, query_cols, 0, DP_TABLE_COL_NO,
				   order, 0)
				< 0) {
			LM_ERR("failed to query database!\n");
			return -1;
		}
		if(dp_dbf.fetch_result(dp_db_handle, &res, dp_fetch_rows) < 0) {
			LM_ERR("failed to fetch\n");
			if(res)
				dp_dbf.free_result(dp_db_handle, res);
			return -1;
		}
	} else {
		/*select the whole table and all the columns*/
		if(dp_dbf.query(dp_db_handle, 0, 0, 0, query_cols, 0, DP_TABLE_COL_NO,
				   order, &res)
				< 0) {
			LM_ERR("failed to query database\n");
			return -1;
		}
	}

	nr_rows = RES_ROW_N(res);

	*dp_next_idx = ((*dp_crt_idx) == 0) ? 1 : 0;
	destroy_hash(*dp_next_idx);

	if(nr_rows == 0) {
		LM_WARN("no data in the db\n");
		goto end;
	}

	do {
		for(i = 0; i < RES_ROW_N(res); i++) {
			rows = RES_ROWS(res);

			values = ROW_VALUES(rows + i);

			if((rule = build_rule(values)) == 0)
				goto err2;

			if(add_rule2hash(rule, *dp_next_idx) != 0)
				goto err2;
		}
		if(DB_CAPABILITY(dp_dbf, DB_CAP_FETCH)) {
			if(dp_dbf.fetch_result(dp_db_handle, &res, dp_fetch_rows) < 0) {
				LM_ERR("failure while fetching!\n");
				if(res)
					dp_dbf.free_result(dp_db_handle, res);
				return -1;
			}
		} else {
			break;
		}
	} while(RES_ROW_N(res) > 0);


end:
	/*update data*/
	*dp_crt_idx = *dp_next_idx;
	list_hash(*dp_crt_idx);
	dp_dbf.free_result(dp_db_handle, res);
	return 0;

err2:
	if(rule)
		destroy_rule(rule);
	destroy_hash(*dp_next_idx);
	dp_dbf.free_result(dp_db_handle, res);
	*dp_next_idx = *dp_crt_idx;
	return -1;
}


int dpl_str_to_shm(str src, str *dest, int mterm)
{
	int mdup = 0;

	if(src.len == 0 || src.s == 0)
		return 0;

	if(mterm != 0 && PV_MARKER == '$') {
		if(src.len > 1 && src.s[src.len - 1] == '$'
				&& src.s[src.len - 2] != '$') {
			mdup = 1;
		}
	}
	dest->s = (char *)shm_malloc((src.len + 1 + mdup) * sizeof(char));
	if(!dest->s) {
		LM_ERR("out of shm memory\n");
		return -1;
	}

	memcpy(dest->s, src.s, src.len);
	dest->s[src.len] = '\0';
	dest->len = src.len;
	if(mdup) {
		dest->s[dest->len] = '$';
		dest->len++;
		dest->s[dest->len] = '\0';
	}

	return 0;
}


/* Compile pcre pattern
 * if mtype==0 - return pointer using shm
 * if mtype==1 - return pcre2_code pointer that has to be pcre2_code_free() */
pcre2_code *reg_ex_comp(const char *pattern, int *cap_cnt, int mtype)
{
	pcre2_code *re;
	int pcre_error_num = 0;
	char pcre_error[128];
	size_t pcre_erroffset;
	int rc;

	re = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, 0,
			&pcre_error_num, &pcre_erroffset, mtype == 0 ? dpl_ctx : NULL);
	if(re == NULL) {
		switch(pcre2_get_error_message(
				pcre_error_num, (PCRE2_UCHAR *)pcre_error, 128)) {
			case PCRE2_ERROR_NOMEMORY:
				snprintf(pcre_error, 128,
						"unknown error[%d]: pcre2 error buffer too small",
						pcre_error_num);
				break;
			case PCRE2_ERROR_BADDATA:
				snprintf(pcre_error, 128, "unknown pcre2 error[%d]",
						pcre_error_num);
				break;
		}
		LM_ERR("PCRE compilation of '%s' failed at offset %zu: %s\n", pattern,
				pcre_erroffset, pcre_error);
		return NULL;
	}
	rc = pcre2_pattern_info(re, PCRE2_INFO_CAPTURECOUNT, cap_cnt);
	if(rc != 0) {
		pcre2_code_free(re);
		LM_ERR("pcre_fullinfo on compiled pattern '%s' yielded error: %d\n",
				pattern, rc);
		return NULL;
	}
	return re;
}


/*compile the expressions, and if ok, build the rule */
dpl_node_t *build_rule(db_val_t *values)
{
	pcre2_code *match_comp, *subst_comp;
	struct subst_expr *repl_comp;
	dpl_node_t *new_rule;
	str match_exp, subst_exp, repl_exp, attrs;
	int matchop;
	int cap_cnt = 0;
	unsigned int tflags = 0;

	matchop = VAL_INT(values + 2);

	if((matchop != DP_REGEX_OP) && (matchop != DP_EQUAL_OP)
			&& (matchop != DP_FNMATCH_OP)) {
		LM_ERR("invalid value for match operator\n");
		return NULL;
	}

	match_comp = subst_comp = 0;
	repl_comp = 0;
	new_rule = 0;

	GET_STR_VALUE(match_exp, values, 3);
	if(matchop == DP_REGEX_OP) {
		if(unlikely(dp_match_dynamic == 1)) {
			if(dpl_check_pv(&match_exp) == 0) {
				tflags |= DP_TFLAGS_PV_MATCH;
			}
		}
		if(!(tflags & DP_TFLAGS_PV_MATCH)) {
			match_comp = reg_ex_comp(match_exp.s, &cap_cnt, 0);
			if(!match_comp) {
				LM_ERR("failed to compile match expression %.*s\n",
						match_exp.len, match_exp.s);
				goto err;
			}
		}
	}

	GET_STR_VALUE(repl_exp, values, 6);
	if(repl_exp.len && repl_exp.s) {
		repl_comp = repl_exp_parse(repl_exp);
		if(!repl_comp) {
			LM_ERR("failed to compile replacing expression %.*s\n",
					repl_exp.len, repl_exp.s);
			goto err;
		}
	}

	cap_cnt = 0;
	GET_STR_VALUE(subst_exp, values, 5);
	if(subst_exp.s && subst_exp.len) {
		if(unlikely(dp_match_dynamic == 1)) {
			if(dpl_check_pv(&subst_exp) == 0) {
				tflags |= DP_TFLAGS_PV_SUBST;
			}
		}
		if(!(tflags & DP_TFLAGS_PV_SUBST)) {
			subst_comp = reg_ex_comp(subst_exp.s, &cap_cnt, 0);
			if(!subst_comp) {
				LM_ERR("failed to compile subst expression %.*s\n",
						subst_exp.len, subst_exp.s);
				goto err;
			}
			if(cap_cnt > MAX_REPLACE_WITH) {
				LM_ERR("subst expression %.*s has too many sub-expressions\n",
						subst_exp.len, subst_exp.s);
				goto err;
			}
		}
	}

	LM_DBG("building rule for [%d:%.*s/%.*s/%.*s]\n", matchop, match_exp.len,
			ZSW(match_exp.s), subst_exp.len, ZSW(subst_exp.s), repl_exp.len,
			ZSW(repl_exp.s));
	if(!(tflags & (DP_TFLAGS_PV_SUBST | DP_TFLAGS_PV_MATCH)) && repl_comp
			&& (cap_cnt < repl_comp->max_pmatch)
			&& (repl_comp->max_pmatch != 0)) {
		LM_ERR("repl_exp %.*s refers to %d sub-expressions, but "
			   "subst_exp %.*s has only %d\n",
				repl_exp.len, repl_exp.s, repl_comp->max_pmatch, subst_exp.len,
				subst_exp.s, cap_cnt);
		goto err;
	}

	new_rule = (dpl_node_t *)shm_malloc(sizeof(dpl_node_t));
	if(!new_rule) {
		LM_ERR("out of shm memory(new_rule)\n");
		goto err;
	}
	memset(new_rule, 0, sizeof(dpl_node_t));

	if(dpl_str_to_shm(
			   match_exp, &new_rule->match_exp, tflags & DP_TFLAGS_PV_MATCH)
			!= 0)
		goto err;

	if(dpl_str_to_shm(
			   subst_exp, &new_rule->subst_exp, tflags & DP_TFLAGS_PV_SUBST)
			!= 0)
		goto err;

	if(dpl_str_to_shm(repl_exp, &new_rule->repl_exp, 0) != 0)
		goto err;

	/*set the rest of the rule fields*/
	new_rule->dpid = VAL_INT(values);
	new_rule->pr = VAL_INT(values + 1);
	new_rule->matchlen = VAL_INT(values + 4);
	new_rule->matchop = matchop;
	GET_STR_VALUE(attrs, values, 7);
	if(dpl_str_to_shm(attrs, &new_rule->attrs, 0) != 0)
		goto err;

	LM_DBG("attrs are: '%.*s'\n", new_rule->attrs.len, new_rule->attrs.s);

	new_rule->match_comp = match_comp;
	new_rule->subst_comp = subst_comp;
	new_rule->repl_comp = repl_comp;
	new_rule->tflags = tflags;

	return new_rule;

err:
	if(match_comp)
		pcre2_code_free(match_comp);
	if(subst_comp)
		pcre2_code_free(subst_comp);
	if(repl_comp)
		repl_expr_free(repl_comp);
	if(new_rule)
		destroy_rule(new_rule);
	return NULL;
}


int add_rule2hash(dpl_node_t *rule, int h_index)
{
	dpl_id_p crt_idp, last_idp;
	dpl_index_p indexp, last_indexp, new_indexp;
	int new_id;

	if(!dp_rules_hash) {
		LM_ERR("data not allocated\n");
		return -1;
	}

	new_id = 0;

	/*search for the corresponding dpl_id*/
	for(crt_idp = last_idp = dp_rules_hash[h_index]; crt_idp != NULL;
			last_idp = crt_idp, crt_idp = crt_idp->next)
		if(crt_idp->dp_id == rule->dpid)
			break;

	/*didn't find a dpl_id*/
	if(!crt_idp) {
		crt_idp = (dpl_id_t *)shm_malloc(sizeof(dpl_id_t));
		if(!crt_idp) {
			LM_ERR("out of shm memory (crt_idp)\n");
			return -1;
		}
		memset(crt_idp, 0, sizeof(dpl_id_t));
		crt_idp->dp_id = rule->dpid;
		new_id = 1;
		LM_DBG("new dpl_id %i\n", rule->dpid);
	}

	/*search for the corresponding dpl_index*/
	for(indexp = last_indexp = crt_idp->first_index; indexp != NULL;
			last_indexp = indexp, indexp = indexp->next) {
		if(indexp->len == rule->matchlen)
			goto add_rule;
		if((rule->matchlen != 0)
				&& ((indexp->len) ? (indexp->len > rule->matchlen) : 1))
			goto add_index;
	}

add_index:
	LM_DBG("new index , len %i\n", rule->matchlen);

	new_indexp = (dpl_index_t *)shm_malloc(sizeof(dpl_index_t));
	if(!new_indexp) {
		LM_ERR("out of shm memory\n");
		goto err;
	}
	memset(new_indexp, 0, sizeof(dpl_index_t));
	new_indexp->next = indexp;
	new_indexp->len = rule->matchlen;

	/*add as first index*/
	if(last_indexp == indexp) {
		crt_idp->first_index = new_indexp;
	} else {
		last_indexp->next = new_indexp;
	}

	indexp = new_indexp;

add_rule:
	rule->next = 0;
	if(!indexp->first_rule)
		indexp->first_rule = rule;

	if(indexp->last_rule)
		indexp->last_rule->next = rule;

	indexp->last_rule = rule;

	if(new_id) {
		crt_idp->next = dp_rules_hash[h_index];
		dp_rules_hash[h_index] = crt_idp;
	}
	LM_DBG("added the rule id %i index %i pr %i next %p to the "
		   "index with %i len\n",
			rule->dpid, rule->matchlen, rule->pr, rule->next, indexp->len);

	return 0;

err:
	if(new_id)
		shm_free(crt_idp);
	return -1;
}


void destroy_hash(int index)
{
	dpl_id_p crt_idp;
	dpl_index_p indexp;
	dpl_node_p rulep;

	if(!dp_rules_hash[index])
		return;

	for(crt_idp = dp_rules_hash[index]; crt_idp != NULL;) {

		for(indexp = crt_idp->first_index; indexp != NULL;) {

			for(rulep = indexp->first_rule; rulep != NULL;) {

				destroy_rule(rulep);

				indexp->first_rule = rulep->next;
				shm_free(rulep);
				rulep = 0;
				rulep = indexp->first_rule;
			}
			crt_idp->first_index = indexp->next;
			shm_free(indexp);
			indexp = 0;
			indexp = crt_idp->first_index;
		}

		dp_rules_hash[index] = crt_idp->next;
		shm_free(crt_idp);
		crt_idp = 0;
		crt_idp = dp_rules_hash[index];
	}

	dp_rules_hash[index] = 0;
}


void destroy_rule(dpl_node_t *rule)
{

	if(!rule)
		return;

	LM_DBG("destroying rule with priority %i\n", rule->pr);

	if(rule->match_comp)
		pcre2_code_free(rule->match_comp);

	if(rule->subst_comp)
		pcre2_code_free(rule->subst_comp);

	/*destroy repl_exp*/
	if(rule->repl_comp)
		repl_expr_free(rule->repl_comp);

	if(rule->match_exp.s)
		shm_free(rule->match_exp.s);

	if(rule->subst_exp.s)
		shm_free(rule->subst_exp.s);

	if(rule->repl_exp.s)
		shm_free(rule->repl_exp.s);

	if(rule->attrs.s)
		shm_free(rule->attrs.s);
}


dpl_id_p select_dpid(int id)
{
	dpl_id_p idp;

	if(!dp_rules_hash || !dp_crt_idx)
		return NULL;

	for(idp = dp_rules_hash[*dp_crt_idx]; idp != NULL; idp = idp->next)
		if(idp->dp_id == id)
			return idp;

	return NULL;
}


/*FOR DEBUG PURPOSE*/
void list_hash(int h_index)
{
	dpl_id_p crt_idp;
	dpl_index_p indexp;
	dpl_node_p rulep;


	if(!dp_rules_hash[h_index])
		return;

	for(crt_idp = dp_rules_hash[h_index]; crt_idp != NULL;
			crt_idp = crt_idp->next) {
		LM_DBG("DPID: %i, pointer %p\n", crt_idp->dp_id, crt_idp);
		for(indexp = crt_idp->first_index; indexp != NULL;
				indexp = indexp->next) {
			LM_DBG("INDEX LEN: %i\n", indexp->len);
			for(rulep = indexp->first_rule; rulep != NULL;
					rulep = rulep->next) {
				list_rule(rulep);
			}
		}
	}
}


void list_rule(dpl_node_t *rule)
{
	LM_DBG("RULE %p: pr %i next %p op %d tflags %u match_exp %.*s, "
		   "subst_exp %.*s, repl_exp %.*s and attrs %.*s\n",
			rule, rule->pr, rule->next, rule->matchop, rule->tflags,
			rule->match_exp.len, ZSW(rule->match_exp.s), rule->subst_exp.len,
			ZSW(rule->subst_exp.s), rule->repl_exp.len, ZSW(rule->repl_exp.s),
			rule->attrs.len, ZSW(rule->attrs.s));
}
