/**
 * Copyright (C) 2018 Jose Luis Verdeguer
 *
 * 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
 *
 */


#include "../../core/mem/shm_mem.h"
#include "../../lib/srdb1/db.h"
#include "secfilter.h"


int mod_version = 1;

/* Database variables */
static db_func_t db_funcs;		 /* Database API functions */
static db1_con_t *db_handle = 0; /* Database connection handle */


/* Check module version */
static int check_version(void)
{
	/* Connect to DB */
	db_handle = db_funcs.init(&secf_db_url);
	if(db_handle == NULL) {
		LM_ERR("Invalid db handle\n");
		return -1;
	}

	if(db_check_table_version(
			   &db_funcs, db_handle, &secf_table_name, mod_version)
			< 0) {
		DB_TABLE_VERSION_ERROR(secf_table_name);
		db_funcs.close(db_handle);
		return -1;
	}

	return 0;
}


/**
 * @brief Add a new allocated list element to an existing list
 *
 * Add a new allocated list element to an existing list, the allocation is done
 * from the private memory pool
 * @param s input character
 * @param len length of input character
 * @param last existing list
 * @param total length of total characters in list
 * @return extended list
**/
static struct str_list *shm_append_str_list(
		char *s, int len, struct str_list **last, int *total)
{
	struct str_list *new;

	new = shm_malloc(sizeof(struct str_list));
	if(!new) {
		SHM_MEM_ERROR;
		return 0;
	}
	new->s.s = s;
	new->s.len = len;
	new->next = 0;

	if(*last) {
		(*last)->next = new;
		*last = new;
	}
	*total += len;

	return new;
}


/**
	Action => 0 = blacklist
	          1 = whitelist
	          2 = destination blacklist
	Type (for action = 0 or 1) => 0 = user-agent
				      1 = country
				      2 = domain
				      3 = IP address
				      4 = user
**/
int secf_append_rule(int action, int type, str *value)
{
	secf_info_p ini = NULL;
	secf_info_p last = NULL;
	struct str_list **ini_node = NULL;
	struct str_list **last_node = NULL;
	struct str_list *new = NULL;
	int total = 0;
	char *v = NULL;

	if(action < 0 || action > 2) {
		LM_ERR("Unknown action value %d", action);
		return -1;
	}

	if(action == 1) {
		ini = &(*secf_data)->wl;
		last = &(*secf_data)->wl_last;
	} else {
		ini = &(*secf_data)->bl;
		last = &(*secf_data)->bl_last;
	}

	switch(type) {
		case 0:
			if(action == 2) {
				ini_node = &ini->dst;
				last_node = &last->dst;
			} else {
				ini_node = &ini->ua;
				last_node = &last->ua;
			}
			break;
		case 1:
			ini_node = &ini->country;
			last_node = &last->country;
			break;
		case 2:
			ini_node = &ini->domain;
			last_node = &last->domain;
			break;
		case 3:
			ini_node = &ini->ip;
			last_node = &last->ip;
			break;
		case 4:
			ini_node = &ini->user;
			last_node = &last->user;
			break;
		default:
			LM_ERR("Unknown type value %d", type);
			return -1;
	}

	v = (char *)shm_malloc(sizeof(char) * value->len);
	if(!v) {
		SHM_MEM_ERROR;
		return -1;
	}
	memcpy(v, value->s, value->len);
	LM_DBG("ini_node:%p last_node:%p\n", *ini_node, *last_node);

	new = shm_append_str_list(v, value->len, last_node, &total);
	if(!new) {
		LM_ERR("can't append new node\n");
		shm_free(v);
		return -1;
	}
	LM_DBG("new node[%p] str:'%.*s'[%d]\n", new, new->s.len, new->s.s,
			new->s.len);

	*last_node = new;
	if(!*ini_node) {
		LM_DBG("ini_node[%p] was NULL, this is the first node\n", ini_node);
		*ini_node = new;
	}
	LM_DBG("ini_node:%p last_node:%p\n", *ini_node, *last_node);

	return 0;
}

/**
 * @brief Removes entries from a whitelist or blacklist based on action, type, and value.
 *
 * The function validates the action and type, locates the relevant list, and iterates
 * through its nodes to find and remove all matches for the given value. If matches are found,
 * the corresponding memory is freed, and the list pointers are updated. Logs are generated
 * for each removal and at the end of the process to summarize the result.
 * @param action: Specifies the target list (0 = blacklist, 1 = whitelist, 2 = destination blacklist).
 * @param type: Indicates the type of rule to be removed (e.g., domain, IP, user, etc.).
 * @param value: The specific value to be matched and removed from the list.
 * @returns 0 if one or more entries are successfully removed or -1 if no matches are found or if an invalid action or type is specified.
**/
int secf_remove_rule(int action, int type, str *value)
{
	secf_info_p ini = NULL;
	secf_info_p last = NULL;
	struct str_list **ini_node = NULL;
	struct str_list **last_node = NULL;
	struct str_list *current = NULL;
	struct str_list *previous = NULL;
	int total = 0;

	if(action < 0 || action > 2) {
		LM_ERR("Unknown action value %d", action);
		return -1;
	}

	if(action == 1) {
		ini = &(*secf_data)->wl;
		last = &(*secf_data)->wl_last;
	} else {
		ini = &(*secf_data)->bl;
		last = &(*secf_data)->bl_last;
	}

	switch(type) {
		case 0:
			if(action == 2) {
				ini_node = &ini->dst;
				last_node = &last->dst;
			} else {
				ini_node = &ini->ua;
				last_node = &last->ua;
			}
			break;
		case 1:
			ini_node = &ini->country;
			last_node = &last->country;
			break;
		case 2:
			ini_node = &ini->domain;
			last_node = &last->domain;
			break;
		case 3:
			ini_node = &ini->ip;
			last_node = &last->ip;
			break;
		case 4:
			ini_node = &ini->user;
			last_node = &last->user;
			break;
		default:
			LM_ERR("Unknown type value %d", type);
			return -1;
	}

	current = *ini_node;
	previous = NULL;
	// Iterate through the list and remove matching nodes
	while(current) {
		if(strncmp(current->s.s, value->s, value->len) == 0) {
			LM_DBG("Match found: %.*s\n", current->s.len, current->s.s);
			total++;
			struct str_list *temp = current;
			if(previous) {
				previous->next = current->next;
				if(!previous->next) {
					*last_node = previous;
				}
			} else {
				*ini_node = current->next;
				if(!(*ini_node)) {
					*last_node = NULL;
				}
			}
			shm_free(temp->s.s);
			shm_free(temp);
			LM_DBG("Match removed.\n");
			if(previous) {
				current = previous->next;
			} else {
				current = *ini_node;
			}
			continue;
		}
		previous = current;
		current = current->next;
	}
	if(total > 0) {
		LM_DBG("Total matches removed: %d", total);
		return 0; // Return the total number of removed items
	} else {
		LM_DBG("No matching values found in the list.");
		return -1; // Return -1 on no matches
	}
}

/* Load data from database */
int secf_load_db(void)
{
	db_key_t db_cols[3];
	db1_res_t *db_res = NULL;
	str str_data = STR_NULL;
	int i, action, type;
	int rows = 0;
	int res = 0;

	/* Connect to DB */
	db_handle = db_funcs.init(&secf_db_url);
	if(db_handle == NULL) {
		LM_ERR("Invalid db handle\n");
		return -1;
	}

	/* Choose new hash table and free its old contents */
	if(*secf_data == secf_data_1) {
		*secf_data = secf_data_2;
	} else {
		*secf_data = secf_data_1;
	}
	secf_free_data(*secf_data);

	/* Prepare the data for the query */
	db_cols[0] = &secf_action_col;
	db_cols[1] = &secf_type_col;
	db_cols[2] = &secf_data_col;

	/* Execute query */
	if(db_funcs.use_table(db_handle, &secf_table_name) < 0) {
		LM_ERR("Unable to use table '%.*s'\n", secf_table_name.len,
				secf_table_name.s);
		return -1;
	}
	if(db_funcs.query(db_handle, NULL, NULL, NULL, db_cols, 0, 3, NULL, &db_res)
			< 0) {
		LM_ERR("Failed to query database\n");
		db_funcs.close(db_handle);
		return -1;
	}

	rows = RES_ROW_N(db_res);
	if(rows == 0) {
		LM_DBG("No data found in database\n");
		res = 0;
		goto clean;
	}

	lock_get(&(*secf_data)->lock);
	for(i = 0; i < rows; i++) {
		action = (int)RES_ROWS(db_res)[i].values[0].val.int_val;
		type = (int)RES_ROWS(db_res)[i].values[1].val.int_val;
		str_data.s = (char *)RES_ROWS(db_res)[i].values[2].val.string_val;
		str_data.len = strlen(str_data.s);
		LM_DBG("[%d] append_rule for action:%d type:%d data:%.*s\n", i, action,
				type, str_data.len, str_data.s);

		if(secf_append_rule(action, type, &str_data) < 0) {
			LM_ERR("Can't append_rule with action:%d type:%d\n", action, type);
			res = -1;
			lock_release(&(*secf_data)->lock);
			goto clean;
		}
	}
	lock_release(&(*secf_data)->lock);

clean:
	if(db_funcs.free_result(db_handle, db_res) < 0) {
		LM_DBG("Failed to free the result\n");
	}
	db_funcs.close(db_handle);
	return res;
}


/* Init database connection */
int secf_init_db(void)
{
	if(secf_db_url.s == NULL) {
		LM_ERR("Database not configured\n");
		return -1;
	}

	secf_db_url.len = strlen(secf_db_url.s);

	if(db_bind_mod(&secf_db_url, &db_funcs) < 0) {
		LM_ERR("Unable to bind to db driver - %.*s\n", secf_db_url.len,
				secf_db_url.s);
		return -1;
	}

	if(check_version() == -1)
		return -1;

	return 0;
}
