/*
 * Copyright (C) 2007 Voice System 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
 *
 */

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

#include "../../core/dprint.h"
#include "../../core/ut.h"
#include "../../lib/srdb1/db.h"
#include "../../core/config.h"
#include "../../core/socket_info.h"
#include "../../modules/tm/dlg.h"
#include "../../modules/tm/tm_load.h"
#include "../../core/counters.h"
#include "../../core/locking.h"
#include "dlg_timer.h"
#include "dlg_hash.h"
#include "dlg_req_within.h"
#include "dlg_db_handler.h"
#include "dlg_ng_stats.h"

#define MAX_FWD_HDR "Max-Forwards: " MAX_FWD CRLF
#define MAX_FWD_HDR_LEN (sizeof(MAX_FWD_HDR) - 1)

extern str dlg_extra_hdrs;
extern int dlg_db_mode;
extern struct dialog_ng_counters_h dialog_ng_cnts_h;

int free_tm_dlg(dlg_t *td)
{
	if(td) {
		if(td->route_set)
			free_rr(&td->route_set);
		pkg_free(td);
	}
	return 0;
}

dlg_t *build_dlg_t(struct dlg_cell *cell, int dir)
{

	dlg_t *td = NULL;
	str cseq;
	unsigned int loc_seq;
	str route_set;
	str contact;

	struct dlg_cell_out *dlg_out = 0;
	struct dlg_entry_out *dlg_entry_out = 0;

	/* if trying to send by to callee we need to get the corresponding dlg_out cell */
	lock_get(cell->dlg_out_entries_lock);
	dlg_entry_out = &cell->dlg_entry_out;

	dlg_out = dlg_entry_out->first;
	//must be concurrent call - lets choose - TODO - ie. check if there is more

	if(!dlg_out) {
		LM_ERR("Trying to send BYE for dialog with no callee leg\n");
		lock_release(cell->dlg_out_entries_lock);
		return NULL;
	}

	td = (dlg_t *)pkg_malloc(sizeof(dlg_t));
	if(!td) {

		LM_ERR("out of pkg memory\n");
		lock_release(cell->dlg_out_entries_lock);
		return NULL;
	}
	memset(td, 0, sizeof(dlg_t));

	if(dir == DLG_CALLER_LEG) {
		cseq = cell->first_req_cseq;
		route_set = cell->caller_route_set;
		contact = cell->caller_contact;
		td->rem_uri = cell->from_uri;
		td->loc_uri = dlg_out->to_uri;
		td->id.rem_tag = cell->from_tag;
		td->id.loc_tag = dlg_out->to_tag;
		td->send_sock = cell->caller_bind_addr;
	} else {
		cseq = dlg_out->callee_cseq;
		route_set = dlg_out->callee_route_set;
		contact = dlg_out->callee_contact;
		td->rem_uri = dlg_out->to_uri;
		td->loc_uri = cell->from_uri;
		td->id.rem_tag = dlg_out->to_tag;
		td->id.loc_tag = cell->from_tag;
		td->send_sock = dlg_out->callee_bind_addr;
	}

	if(str2int(&cseq, &loc_seq) != 0) {
		LM_ERR("invalid cseq\n");
		goto error;
	}

	/*we don not increase here the cseq as this will be done by TM*/
	td->loc_seq.value = loc_seq;
	td->loc_seq.is_set = 1;

	/*route set*/
	if(route_set.s && route_set.len) {

		if(parse_rr_body(route_set.s, route_set.len, &td->route_set) != 0) {
			LM_ERR("failed to parse route set\n");
			goto error;
		}
	}

	if(contact.s == 0 || contact.len == 0) {

		LM_ERR("no contact available\n");
		goto error;
	}

	td->id.call_id = cell->callid;
	td->rem_target = contact;
	td->state = DLG_CONFIRMED;

	lock_release(cell->dlg_out_entries_lock);
	return td;

error:
	lock_release(cell->dlg_out_entries_lock);
	free_tm_dlg(td);
	return NULL;
}

/*callback function to handle responses to the BYE request */
void bye_reply_cb(struct cell *t, int type, struct tmcb_params *ps)
{

	struct dlg_cell *dlg;
	int event, old_state, new_state, unref, ret;
	struct dlg_cell_out *dlg_out = 0;

	if(ps->param == NULL || *ps->param == NULL) {
		LM_ERR("invalid parameter\n");
		return;
	}

	if(ps->code < 200) {
		LM_DBG("receiving a provisional reply\n");
		return;
	}

	LM_DBG("receiving a final reply %d\n", ps->code);

	dlg = (struct dlg_cell *)(*(ps->param));
	event = DLG_EVENT_REQBYE;

	//get the corresponding dlg out structure for this REQ
	struct dlg_entry_out *dlg_entry_out = &dlg->dlg_entry_out;
	lock_get(dlg->dlg_out_entries_lock);
	dlg_out = dlg_entry_out->first; //TODO check for concurrent call
	if(!dlg_out)
		return;

	next_state_dlg(
			dlg, event, &old_state, &new_state, &unref, &dlg_out->to_tag);

	lock_release(dlg->dlg_out_entries_lock);

	if(new_state == DLG_STATE_DELETED && old_state != DLG_STATE_DELETED) {

		LM_DBG("removing dialog with h_entry %u and h_id %u\n", dlg->h_entry,
				dlg->h_id);

		/* remove from timer */
		ret = remove_dialog_timer(&dlg->tl);
		if(ret < 0) {
			LM_CRIT("unable to unlink the timer on dlg %p [%u:%u] "
					"with clid '%.*s'\n",
					dlg, dlg->h_entry, dlg->h_id, dlg->callid.len,
					dlg->callid.s);
		} else if(ret > 0) {
			LM_WARN("inconsistent dlg timer data on dlg %p [%u:%u] "
					"with clid '%.*s'\n",
					dlg, dlg->h_entry, dlg->h_id, dlg->callid.len,
					dlg->callid.s);
		} else {
			unref++;
		}

		counter_add(dialog_ng_cnts_h.active, -1);

		/* dialog terminated (BYE) */
		run_dlg_callbacks(
				DLGCB_TERMINATED, dlg, ps->req, ps->rpl, DLG_DIR_NONE, 0);

		/* derefering the dialog */
		unref_dlg(dlg,
				unref); /*removed the extra +1 on the unref that is usually added in the unref of next_state_dlg */
	}

	if(new_state == DLG_STATE_DELETED && old_state == DLG_STATE_DELETED) {
		/* trash the dialog from DB and memory */
		if(dlg_db_mode)
			remove_dialog_in_from_db(dlg);

		/* force delete from mem */
		unref_dlg(dlg, 1);
	}
}

static inline int build_extra_hdr(
		struct dlg_cell *cell, str *extra_hdrs, str *str_hdr)
{
	char *p;

	str_hdr->len = MAX_FWD_HDR_LEN + dlg_extra_hdrs.len;
	if(extra_hdrs && extra_hdrs->len > 0)
		str_hdr->len += extra_hdrs->len;

	str_hdr->s = (char *)pkg_malloc(str_hdr->len * sizeof(char));
	if(!str_hdr->s) {
		LM_ERR("out of pkg memory\n");
		goto error;
	}

	memcpy(str_hdr->s, MAX_FWD_HDR, MAX_FWD_HDR_LEN);
	p = str_hdr->s + MAX_FWD_HDR_LEN;
	if(dlg_extra_hdrs.len) {
		memcpy(p, dlg_extra_hdrs.s, dlg_extra_hdrs.len);
		p += dlg_extra_hdrs.len;
	}
	if(extra_hdrs && extra_hdrs->len > 0)
		memcpy(p, extra_hdrs->s, extra_hdrs->len);

	return 0;

error:
	return -1;
}

/* cell- pointer to a struct dlg_cell
 * dir- direction: the request will be sent to:
 * 		DLG_CALLER_LEG (0): caller
 * 		DLG_CALLEE_LEG (1): callee
 */
static inline int send_bye(struct dlg_cell *cell, int dir, str *hdrs)
{
	uac_req_t uac_r;
	dlg_t *dialog_info;
	str met = {"BYE", 3};
	int result;
	/* do not send BYE request for non-confirmed dialogs (not supported) */
	if(cell->state != DLG_STATE_CONFIRMED) {
		LM_ERR("terminating only 1 side of non-confirmed dialogs not supported "
			   "by this function\n");
		return -1;
	}

	/*verify direction*/
	if((dialog_info = build_dlg_t(cell, dir)) == 0) {
		LM_ERR("failed to create dlg_t\n");
		goto err;
	}

	LM_DBG("sending BYE to %s\n",
			(dir == DLG_CALLER_LEG) ? "caller" : "callee");

	ref_dlg(cell, 1);

	memset(&uac_r, '\0', sizeof(uac_req_t));
	set_uac_req(&uac_r, &met, hdrs, NULL, dialog_info, TMCB_LOCAL_COMPLETED,
			bye_reply_cb, (void *)cell);

	result = d_tmb.t_request_within(&uac_r);

	if(result < 0) {
		LM_ERR("failed to send the BYE request\n");
		goto err1;
	}

	free_tm_dlg(dialog_info);

	LM_DBG("BYE sent to %s\n", (dir == 0) ? "caller" : "callee");
	return 0;

err1:
	unref_dlg(cell, 1);
err:
	if(dialog_info)
		free_tm_dlg(dialog_info);
	return -1;
}

/*static void early_transaction_destroyed(struct cell* t, int type, struct tmcb_params *param) {
    struct dlg_cell *dlg = (struct dlg_cell *) (*param->param);

    if (!dlg)
        return;

    LM_DBG("Early transaction destroyed\n");
}*/

/* side =
 * 0: caller
 * 1: callee
 * 2: all
 */
int dlg_terminate(struct dlg_cell *dlg, struct sip_msg *msg, str *reason,
		int side, str *extra_hdrs)
{

	struct cell *t;
	str default_reason = {"call failed", 11};
	int cfg_cmd = 0;
	str default_extra_headers = {0, 0};

	if(!dlg) {
		LM_ERR("calling end_dialog with NULL pointer dlg\n");
		return -1;
	}

	if(!extra_hdrs)
		extra_hdrs = &default_extra_headers;


	if(msg) {
		//assume called from cfg command -> dlg_terminate, as opposed to internal API or mi interface
		cfg_cmd = 1;
	}

	if(!reason || reason->len <= 0 || !reason->s) {
		reason = &default_reason;
	}

	if(dlg->state != DLG_STATE_CONFIRMED) {
		if(side != 2) {
			LM_ERR("can't terminate only 1 side of an early dialog\n");
			return -1;
		}
		if(dlg->transaction) {
			LM_DBG("terminating early dialog with %d outbound forks on "
				   "transaction %p\n",
					dlg->transaction->nr_of_outgoings, dlg->transaction);

			t = dlg->transaction;

			if(t && t != (void *)-1 && t->uas.request) {
				if(t->method.len != 6 || t->method.s[0] != 'I'
						|| t->method.s[1] != 'N' || t->method.s[2] != 'V') {
					//well this is the transaction of a subsequent request within the dialog
					//and the dialog is not confirmed yet, so it is a PRACK or an UPDATE
					//could also be an option, but the important thing is how am i going to get
					//the transaction of the invite, that is the one i have to cancel
					LM_WARN("this is not my transaction so where am i?\n");
					return 1; //TODO - need to check why we got in here once before? this crashed on t_reply as t seemed invalid
				}

				//TODO: here we are assuming none of the CALLEE's have sent a 200, in
				//which case we would have to send an ACK, BYE
				//so right now - we are sending 488 to caller and CANCEL's to all CALLEEs

				LM_DBG("tearing down dialog in EARLY state - no clients "
					   "responded > 199\n");
				if(cfg_cmd) {
					d_tmb.t_reply(msg, 488, reason->s);
					d_tmb.t_release(msg);
				} else {
					d_tmb.t_reply(t->uas.request, 488, reason->s);
					d_tmb.t_release(t->uas.request);
				}
			}
		} else {
			LM_WARN("can't terminate early dialog without a transaction\n");
			return -1;
		}
	} else {
		LM_DBG("terminating confirmed dialog\n");
		if(side == DLG_CALLER_LEG /* 0 */ || side == DLG_CALLEE_LEG /* 1 */) {
			if(dlg_bye(dlg, (extra_hdrs->len > 0) ? extra_hdrs : NULL, side)
					< 0)
				return -1;

		} else {
			if(dlg_bye_all(dlg, (extra_hdrs->len > 0) ? extra_hdrs : NULL) < 0)
				return -1;
		}
	}
	return 1;
}

#ifdef MI_REMOVED
/*parameters from MI: callid, from tag, to tag*/
/* TODO: add reason parameter to mi interface */
struct mi_root *mi_terminate_dlg(struct mi_root *cmd_tree, void *param)
{

	struct mi_node *node;
	struct dlg_cell *dlg = NULL;
	str mi_extra_hdrs = {"Reason: mi_terminated\r\n", 23};
	int status, msg_len;
	char *msg;

	str callid = {NULL, 0};
	str ftag = {NULL, 0};
	str ttag = {NULL, 0};
	str reason = {"mi_terminated", 13};

	if(d_table == NULL)
		goto end;

	node = cmd_tree->node.kids;

	if(node == NULL || node->next == NULL || node->next->next == NULL)
		return init_mi_tree(400, MI_MISSING_PARM_S, MI_MISSING_PARM_LEN);

	if(!node->value.s || !node->value.len) {
		goto error;
	} else {
		callid = node->value;
	}
	node = node->next;
	if(!node->value.s || !node->value.len) {
		goto error;
	} else {
		ftag = node->value;
	}
	node = node->next;
	if(!node->value.s || !node->value.len) {
		goto error;
	} else {
		ttag = node->value;
	}

	if(node->next) {
		node = node->next;
		if(node->value.len && node->value.s)
			mi_extra_hdrs = node->value;
	}

	unsigned int dir = DLG_DIR_NONE;
	LM_DBG("Looking for callid [%.*s]\n", callid.len, callid.s);
	dlg = get_dlg(&callid, &ftag, &ttag, &dir); //increments ref count!

	if(dlg) {
		LM_DBG("Found dialog to terminate and it is in state [%i] [%.*s]\n",
				dlg->state, mi_extra_hdrs.len, mi_extra_hdrs.s);

		if(dlg_terminate(dlg, 0, &reason, /* all sides of a dialog*/ 2,
				   &mi_extra_hdrs)
				< 0) {
			status = 500;
			msg = MI_DLG_OPERATION_ERR;
			msg_len = MI_DLG_OPERATION_ERR_LEN;
		} else {
			status = 200;
			msg = MI_OK_S;
			msg_len = MI_OK_LEN;
		}
		unref_dlg(dlg, 1);

		return init_mi_tree(status, msg, msg_len);
	}
end:
	return init_mi_tree(404, MI_DIALOG_NOT_FOUND, MI_DIALOG_NOT_FOUND_LEN);

error:
	return init_mi_tree(400, MI_BAD_PARM_S, MI_BAD_PARM_LEN);
}
#endif

int dlg_bye(struct dlg_cell *dlg, str *hdrs, int side)
{
	str all_hdrs = {0, 0};
	int ret;

	if(side == DLG_CALLER_LEG) {
		if(dlg->dflags & DLG_FLAG_CALLERBYE)
			return -1;
		dlg->dflags |= DLG_FLAG_CALLERBYE;
	} else {
		if(dlg->dflags & DLG_FLAG_CALLEEBYE)
			return -1;
		dlg->dflags |= DLG_FLAG_CALLEEBYE;
	}
	if((build_extra_hdr(dlg, hdrs, &all_hdrs)) != 0) {
		LM_ERR("failed to build dlg headers\n");
		return -1;
	}
	ret = send_bye(dlg, side, &all_hdrs);
	pkg_free(all_hdrs.s);
	return ret;
}

/* Wrapper for terminating dialog from API - from other modules */
int w_api_terminate_dlg(
		str *callid, str *ftag, str *ttag, str *hdrs, str *reason)
{
	struct dlg_cell *dlg;

	unsigned int dir = DLG_DIR_NONE;
	dlg = get_dlg(callid, ftag, ttag, &dir); //increments ref count!

	if(!dlg) {
		LM_ERR("Asked to tear down non existent dialog\n");
		return -1;
	}

	unref_dlg(dlg, 1);

	return dlg_terminate(dlg, NULL, reason, 2, hdrs);
}

int dlg_bye_all(struct dlg_cell *dlg, str *hdrs)
{
	str all_hdrs = {0, 0};
	int ret;

	if((build_extra_hdr(dlg, hdrs, &all_hdrs)) != 0) {
		LM_ERR("failed to build dlg headers\n");
		return -1;
	}

	ret = send_bye(dlg, DLG_CALLER_LEG, &all_hdrs);
	ret |= send_bye(dlg, DLG_CALLEE_LEG, &all_hdrs);

	pkg_free(all_hdrs.s);
	return ret;
}


/* Wrapper for terminating dialog from API - from other modules */
int w_api_lookup_terminate_dlg(
		unsigned int h_entry, unsigned int h_id, str *hdrs)
{
	struct dlg_cell *dlg;

	dlg = lookup_dlg(h_entry, h_id); //increments ref count!

	if(!dlg) {
		LM_ERR("Asked to tear down non existent dialog\n");
		return -1;
	}

	unref_dlg(dlg, 1);

	return dlg_terminate(dlg, NULL, NULL /*reason*/, 2, hdrs);
}
