/*
 * Copyright (C) 2013-21 Robert Boisvert
 *
 * This file is part of the mohqueue module for Kamailio, a free SIP server.
 *
 * The mohqueue module 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
 *
 * The mohqueue module 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 "mohqueue_mod.h"
#include "mohq_db.h"
#include "mohq_funcs.h"

/**********
* mohqueue definitions
**********/

str MOHQCSTR_ID = STR_STATIC_INIT("id");
str MOHQCSTR_URI = STR_STATIC_INIT("uri");
str MOHQCSTR_MDIR = STR_STATIC_INIT("mohdir");
str MOHQCSTR_MFILE = STR_STATIC_INIT("mohfile");
str MOHQCSTR_NAME = STR_STATIC_INIT("name");
str MOHQCSTR_DEBUG = STR_STATIC_INIT("debug");

static str *mohq_columns[] = {&MOHQCSTR_ID, &MOHQCSTR_URI, &MOHQCSTR_MDIR,
		&MOHQCSTR_MFILE, &MOHQCSTR_NAME, &MOHQCSTR_DEBUG, NULL};

/**********
* mohqcalls definitions
**********/

str CALLCSTR_CALL = STR_STATIC_INIT("call_id");
str CALLCSTR_CNTCT = STR_STATIC_INIT("call_contact");
str CALLCSTR_FROM = STR_STATIC_INIT("call_from");
str CALLCSTR_MOHQ = STR_STATIC_INIT("mohq_id");
str CALLCSTR_STATE = STR_STATIC_INIT("call_status");
str CALLCSTR_TIME = STR_STATIC_INIT("call_time");

static str *call_columns[] = {&CALLCSTR_STATE, &CALLCSTR_CALL, &CALLCSTR_MOHQ,
		&CALLCSTR_FROM, &CALLCSTR_CNTCT, &CALLCSTR_TIME, NULL};

/**********
* local function declarations
**********/

void set_call_key(db_key_t *, int, int);
void set_call_val(db_val_t *, int, int, void *);

/**********
* local functions
**********/

/**********
* Fill Call Keys
*
* INPUT:
*   Arg (1) = row pointer
*   Arg (2) = column count
* OUTPUT: none
**********/

void fill_call_keys(db_key_t *prkeys, int ncnt)

{
	int nidx;
	for(nidx = 0; nidx < ncnt; nidx++) {
		set_call_key(prkeys, nidx, nidx);
	}
	return;
}

/**********
* Fill Call Values
*
* INPUT:
*   Arg (1) = row pointer
*   Arg (2) = call struct pointer
*   Arg (3) = column count
* OUTPUT: none
**********/

void fill_call_vals(db_val_t *prvals, call_lst *pcall, int ncnt)

{
	int nstate = pcall->call_state / 100;
	set_call_val(prvals, CALLCOL_STATE, CALLCOL_STATE, &nstate);
	if(!ncnt) {
		return;
	}
	set_call_val(prvals, CALLCOL_MOHQ, CALLCOL_MOHQ, &pcall->pmohq->mohq_id);
	set_call_val(prvals, CALLCOL_CALL, CALLCOL_CALL, pcall->call_id);
	set_call_val(prvals, CALLCOL_FROM, CALLCOL_FROM, pcall->call_from);
	set_call_val(prvals, CALLCOL_CNTCT, CALLCOL_CNTCT, pcall->call_contact);
	set_call_val(prvals, CALLCOL_TIME, CALLCOL_TIME, &pcall->call_time);
	return;
}

/**********
* Set Call Column Key
*
* INPUT:
*   Arg (1) = row pointer
*   Arg (2) = column number
*   Arg (3) = column id
* OUTPUT: none
**********/

void set_call_key(db_key_t *prkeys, int ncol, int ncolid)

{
	prkeys[ncol] = call_columns[ncolid];
	return;
}

/**********
* Set Call Column Value
*
* INPUT:
*   Arg (1) = row pointer
*   Arg (2) = column number
*   Arg (3) = column id
*   Arg (4) = value pointer
* OUTPUT: none
**********/

void set_call_val(db_val_t *prvals, int ncol, int ncolid, void *pdata)

{
	/**********
* fill based on column
**********/

	switch(ncolid) {
		case CALLCOL_MOHQ:
		case CALLCOL_STATE:
			prvals[ncol].val.int_val = *((int *)pdata);
			prvals[ncol].type = DB1_INT;
			prvals[ncol].nul = 0;
			break;
		case CALLCOL_CALL:
		case CALLCOL_CNTCT:
		case CALLCOL_FROM:
			prvals[ncol].val.string_val = (char *)pdata;
			prvals[ncol].type = DB1_STRING;
			prvals[ncol].nul = 0;
			break;
		case CALLCOL_TIME:
			prvals[ncol].val.time_val = *((time_t *)pdata);
			prvals[ncol].type = DB1_DATETIME;
			prvals[ncol].nul = 0;
			break;
	}
	return;
}

/**********
* external functions
**********/

/**********
* Add Call Record
*
* INPUT:
*   Arg (1) = call index
* OUTPUT: none
**********/

void add_call_rec(int ncall_idx)

{
	/**********
* o fill column names and values
* o insert new record
**********/

	char *pfncname = "add_call_rec: ";
	db1_con_t *pconn = mohq_dbconnect();
	if(!pconn) {
		return;
	}
	db_func_t *pdb = pmod_data->pdb;
	pdb->use_table(pconn, &pmod_data->pcfg->db_ctable);
	db_key_t prkeys[CALL_COLCNT];
	fill_call_keys(prkeys, CALL_COLCNT);
	db_val_t prvals[CALL_COLCNT];
	call_lst *pcall = &pmod_data->pcall_lst[ncall_idx];
	pcall->call_time = time(0);
	fill_call_vals(prvals, pcall, CALL_COLCNT);
	if(pdb->insert(pconn, prkeys, prvals, CALL_COLCNT) < 0) {
		LM_WARN("%sUnable to add new row to %s\n", pfncname,
				pmod_data->pcfg->db_ctable.s);
	}
	mohq_dbdisconnect(pconn);
	return;
}

/**********
* Clear Call Records
*
* INPUT:
*   Arg (1) = connection pointer
* OUTPUT: none
**********/

void clear_calls(db1_con_t *pconn)

{
	/**********
* delete all records
**********/

	char *pfncname = "clear_calls: ";
	db_func_t *pdb = pmod_data->pdb;
	pdb->use_table(pconn, &pmod_data->pcfg->db_ctable);
	if(pdb->delete(pconn, 0, 0, 0, 0) < 0) {
		LM_WARN("%sUnable to delete all rows from %s\n", pfncname,
				pmod_data->pcfg->db_ctable.s);
	}
	return;
}

/**********
* Delete Call Record
*
* INPUT:
*   Arg (1) = call pointer
* OUTPUT: none
**********/

void delete_call_rec(call_lst *pcall)

{
	/**********
* o setup to delete based on call ID
* o delete record
**********/

	char *pfncname = "delete_call_rec: ";
	db1_con_t *pconn = mohq_dbconnect();
	if(!pconn) {
		return;
	}
	db_func_t *pdb = pmod_data->pdb;
	pdb->use_table(pconn, &pmod_data->pcfg->db_ctable);
	db_key_t prkeys[1];
	set_call_key(prkeys, 0, CALLCOL_CALL);
	db_val_t prvals[1];
	set_call_val(prvals, 0, CALLCOL_CALL, pcall->call_id);
	if(pdb->delete(pconn, prkeys, 0, prvals, 1) < 0) {
		LM_WARN("%sUnable to delete row from %s\n", pfncname,
				pmod_data->pcfg->db_ctable.s);
	}
	mohq_dbdisconnect(pconn);
	return;
}

/**********
* Connect to DB
*
* INPUT: none
* OUTPUT: DB connection pointer; NULL=failed
**********/

db1_con_t *mohq_dbconnect(void)

{
	str *pdb_url = &pmod_data->pcfg->db_url;
	db1_con_t *pconn = pmod_data->pdb->init(pdb_url);
	if(!pconn) {
		LM_ERR("Unable to connect to DB %s!\n", pdb_url->s);
	}
	return pconn;
}

/**********
* Disconnect from DB
*
* INPUT:
*   Arg (1) = connection pointer
* OUTPUT: none
**********/

void mohq_dbdisconnect(db1_con_t *pconn)

{
	pmod_data->pdb->close(pconn);
	return;
}

/**********
* Update Call Record
*
* INPUT:
*   Arg (1) = call pointer
* OUTPUT: none
**********/

void update_call_rec(call_lst *pcall)

{
	/**********
* o setup to update based on call ID
* o update record
**********/

	char *pfncname = "update_call_rec: ";
	db1_con_t *pconn = mohq_dbconnect();
	if(!pconn) {
		return;
	}
	db_func_t *pdb = pmod_data->pdb;
	pdb->use_table(pconn, &pmod_data->pcfg->db_ctable);
	db_key_t pqkeys[1];
	set_call_key(pqkeys, 0, CALLCOL_CALL);
	db_val_t pqvals[1];
	set_call_val(pqvals, 0, CALLCOL_CALL, pcall->call_id);
	db_key_t pukeys[1];
	set_call_key(pukeys, 0, CALLCOL_STATE);
	db_val_t puvals[1];
	fill_call_vals(puvals, pcall, CALLCOL_STATE);
	if(pdb->update(pconn, pqkeys, 0, pqvals, pukeys, puvals, 1, 1) < 0) {
		LM_WARN("%sUnable to update row in %s\n", pfncname,
				pmod_data->pcfg->db_ctable.s);
	}
	mohq_dbdisconnect(pconn);
	return;
}

/**********
* Update Debug Record
*
* INPUT:
*   Arg (1) = MOH queue pointer
*   Arg (2) = debug flag
* OUTPUT: none
**********/

void update_debug(mohq_lst *pqueue, int bdebug)

{
	/**********
* o setup to update based on queue name
* o update record
**********/

	char *pfncname = "update_debug: ";
	db1_con_t *pconn = mohq_dbconnect();
	if(!pconn) {
		return;
	}
	db_func_t *pdb = pmod_data->pdb;
	pdb->use_table(pconn, &pmod_data->pcfg->db_qtable);
	db_key_t pqkeys[1] = {mohq_columns[MOHQCOL_NAME]};
	db_val_t pqvals[1];
	pqvals->val.string_val = pqueue->mohq_name;
	pqvals->type = DB1_STRING;
	pqvals->nul = 0;
	db_key_t pukeys[1] = {mohq_columns[MOHQCOL_DEBUG]};
	db_val_t puvals[1];
	puvals->val.int_val = bdebug;
	puvals->type = DB1_INT;
	puvals->nul = 0;
	if(pdb->update(pconn, pqkeys, 0, pqvals, pukeys, puvals, 1, 1) < 0) {
		LM_WARN("%sUnable to update row in %s\n", pfncname,
				pmod_data->pcfg->db_qtable.s);
	}
	mohq_dbdisconnect(pconn);
	return;
}

/**********
* Update Message Queue List
*
* INPUT:
*   Arg (1) = connection pointer
* OUTPUT: none
**********/

void update_mohq_lst(db1_con_t *pconn)

{
	/**********
* o reset checked flag on all queues
* o read queues from table
**********/

	char *pfncname = "update_mohq_lst: ";
	if(!pconn) {
		return;
	}
	db_func_t *pdb = pmod_data->pdb;
	mohq_lst *pqlst = pmod_data->pmohq_lst;
	int nidx;
	for(nidx = 0; nidx < pmod_data->mohq_cnt; nidx++) {
		pqlst[nidx].mohq_flags &= ~MOHQF_CHK;
	}
	pdb->use_table(pconn, &pmod_data->pcfg->db_qtable);
	db_key_t prkeys[MOHQ_COLCNT];
	for(nidx = 0; nidx < MOHQ_COLCNT; nidx++) {
		prkeys[nidx] = mohq_columns[nidx];
	}
	db1_res_t *presult = NULL;
	if(pdb->query(pconn, 0, 0, 0, prkeys, 0, MOHQ_COLCNT, 0, &presult)) {
		LM_ERR("%stable query (%s) failed!\n", pfncname,
				pmod_data->pcfg->db_qtable.s);
		return;
	}
	db_row_t *prows = RES_ROWS(presult);
	int nrows = RES_ROW_N(presult);
	db_val_t *prowvals = NULL;
	char *ptext, *puri;
	mohq_lst *pnewlst;
	for(nidx = 0; nidx < nrows; nidx++) {
		/**********
  * check URI
  **********/

		prowvals = ROW_VALUES(prows + nidx);
		char *pqname = (char *)VAL_STRING(prowvals + MOHQCOL_NAME);
		puri = (char *)VAL_STRING(prowvals + MOHQCOL_URI);
		struct sip_uri puri_parsed[1];
		if(parse_uri(puri, strlen(puri), puri_parsed)) {
			LM_ERR("Queue,Field (%s,%.*s): %s is not a valid URI!\n", pqname,
					STR_FMT(&MOHQCSTR_URI), puri);
			continue;
		}

		/**********
  * check MOHDIR
  **********/

		char *pmohdir;
		if(VAL_NULL(prowvals + MOHQCOL_MDIR)) {
			pmohdir = pmod_data->pcfg->mohdir;
		} else {
			pmohdir = (char *)VAL_STRING(prowvals + MOHQCOL_MDIR);
			if(!*pmohdir) {
				pmohdir = pmod_data->pcfg->mohdir;
			} else {
				/**********
      * mohdir
      * o exists?
      * o directory?
      **********/

				struct stat psb[1];
				if(lstat(pmohdir, psb)) {
					LM_ERR("Queue,Field (%s,%.*s): Unable to find %s!\n",
							pqname, STR_FMT(&MOHQCSTR_MDIR), pmohdir);
					continue;
				} else {
					if((psb->st_mode & S_IFMT) != S_IFDIR) {
						LM_ERR("Queue,Field (%s,%.*s): %s is not a "
							   "directory!\n",
								pqname, STR_FMT(&MOHQCSTR_MDIR), pmohdir);
						continue;
					}
				}
			}
		}

		/**********
  * check for MOH files
  **********/

		rtpmap **pmohfiles =
				find_MOH(pmohdir, (char *)VAL_STRING(prowvals + MOHQCOL_MFILE));
		if(!pmohfiles[0]) {
			LM_ERR("Queue,Field (%s,%.*s): Unable to find MOH files "
				   "(%s/%s.*)!\n",
					pqname, STR_FMT(&MOHQCSTR_MDIR), pmohdir,
					(char *)VAL_STRING(prowvals + MOHQCOL_MFILE));
			continue;
		}

		/**********
  * find matching queues
  **********/

		int bfnd = 0;
		int nidx2;
		for(nidx2 = 0; nidx2 < pmod_data->mohq_cnt; nidx2++) {
			if(!strcasecmp(pqlst[nidx2].mohq_uri, puri)) {
				/**********
      * o data the same?
      * o mark as found
      **********/

				if(strcmp(pqlst[nidx2].mohq_mohdir, pmohdir)) {
					strcpy(pqlst[nidx2].mohq_mohdir, pmohdir);
					LM_INFO("Queue,Field (%s,%.*s): Changed\n", pqname,
							STR_FMT(&MOHQCSTR_MDIR));
				}
				ptext = (char *)VAL_STRING(prowvals + MOHQCOL_MFILE);
				if(strcmp(pqlst[nidx2].mohq_mohfile, ptext)) {
					strcpy(pqlst[nidx2].mohq_mohfile, ptext);
					LM_INFO("Queue,Field (%s,%.*s): Changed\n", pqname,
							STR_FMT(&MOHQCSTR_MFILE));
				}
				ptext = (char *)VAL_STRING(prowvals + MOHQCOL_NAME);
				if(strcmp(pqlst[nidx2].mohq_name, ptext)) {
					strcpy(pqlst[nidx2].mohq_name, ptext);
					LM_INFO("Queue,Field (%s,%.*s): Changed\n", pqname,
							STR_FMT(&MOHQCSTR_NAME));
				}
				int bdebug = VAL_INT(prowvals + MOHQCOL_DEBUG) ? MOHQF_DBG : 0;
				if((pqlst[nidx2].mohq_flags & MOHQF_DBG) != bdebug) {
					if(bdebug) {
						pqlst[nidx2].mohq_flags |= MOHQF_DBG;
					} else {
						pqlst[nidx2].mohq_flags &= ~MOHQF_DBG;
					}
					LM_INFO("Queue,Field (%s,%.*s): Changed\n", pqname,
							STR_FMT(&MOHQCSTR_DEBUG));
				}
				bfnd = -1;
				pqlst[nidx2].mohq_flags |= MOHQF_CHK;
				break;
			}
		}

		/**********
  * add new queue
  **********/

		if(!bfnd) {
			/**********
    * o allocate new list
    * o copy old list
    * o add new row
    * o release old list
    * o adjust pointers to new list
    **********/

			int nsize = pmod_data->mohq_cnt + 1;
			pnewlst = (mohq_lst *)shm_malloc(sizeof(mohq_lst) * nsize);
			if(!pnewlst) {
				LM_ERR("%sUnable to allocate shared memory!\n", pfncname);
				return;
			}
			pmod_data->mohq_cnt = nsize;
			if(--nsize) {
				memcpy(pnewlst, pqlst, sizeof(mohq_lst) * nsize);
			}
			pnewlst[nsize].mohq_id = prowvals[MOHQCOL_ID].val.int_val;
			pnewlst[nsize].mohq_flags = MOHQF_CHK;
			strcpy(pnewlst[nsize].mohq_uri, puri);
			strcpy(pnewlst[nsize].mohq_mohdir, pmohdir);
			strcpy(pnewlst[nsize].mohq_mohfile,
					(char *)VAL_STRING(prowvals + MOHQCOL_MFILE));
			strcpy(pnewlst[nsize].mohq_name,
					(char *)VAL_STRING(prowvals + MOHQCOL_NAME));
			if(VAL_INT(prowvals + MOHQCOL_DEBUG)) {
				pnewlst[nsize].mohq_flags |= MOHQF_DBG;
			}
			LM_INFO("Added new queue (%s)\n", pnewlst[nsize].mohq_name);
			if(nsize) {
				shm_free(pmod_data->pmohq_lst);
			}
			pmod_data->pmohq_lst = pnewlst;
			pqlst = pnewlst;
		}
	}

	/**********
* find deleted queues
**********/

	for(nidx = 0; nidx < pmod_data->mohq_cnt; nidx++) {
		/**********
  * o exists?
  * o if not last, replace current with last queue
  **********/

		if(pqlst[nidx].mohq_flags & MOHQF_CHK) {
			continue;
		}
		LM_INFO("Removed queue (%s)\n", pqlst[nidx].mohq_name);
		if(nidx != (pmod_data->mohq_cnt - 1)) {
			memcpy(&pqlst[nidx], &pqlst[pmod_data->mohq_cnt - 1],
					sizeof(mohq_lst));
		}
		--pmod_data->mohq_cnt;
		--nidx;
	}
	return;
}