/*
 * Accounting module with Diameter backend
 *
 * Copyright (C) 2001-2003 FhG Fokus
 * Copyright (C) 2006 Voice Sistem SRL
 *
 * This file is part of Kamailio, a free SIP server.
 *
 * 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
 * \ingroup acc_diameter
 * \brief Acc_diameter:: Core module interface
 *
 * - Module: \ref acc_diameter
 */

/*! \defgroup acc_diameter ACC_DIAMETER :: The Kamailio Diameter Accounting Module
 *
 * The ACC_DIAMETER module is used to account transactions information to
 * DIAMETER (beta version).
 *
 */

#include <stdio.h>
#include <string.h>

#include "../../core/sr_module.h"
#include "../../core/dprint.h"
#include "../../core/mem/mem.h"
#include "../../modules/tm/tm_load.h"
#include "../../core/str.h"
#include "../../core/mod_fix.h"
#include "../../modules/acc/acc_api.h"
#include "../../modules/acc/acc_extra.h"

#include "diam_dict.h"
#include "diam_tcp.h"
#include "diam_message.h"

MODULE_VERSION

static int mod_init(void);
static void destroy(void);
static int child_init(int rank);

int acc_diameter_init(acc_init_info_t *inf);
int acc_diameter_send_request(sip_msg_t *req, acc_info_t *inf);

static int w_acc_diameter_request(sip_msg_t *rq, char *comment, char *foo);
static int acc_api_fixup(void** param, int param_no);
static int free_acc_api_fixup(void** param, int param_no);

int init_acc_diameter(acc_extra_t *leg_info, char *rad_cfg, int srv_type);
int extra2int(struct acc_extra *extra, int *attrs);
int extra2strar(struct acc_extra *extra, sip_msg_t *rq, str *val_arr,
		int *int_arr, char *type_arr);

/*! ACC API structure */
acc_api_t accb;
acc_engine_t _acc_diameter_engine;

/* ----- DIAMETER acc variables ----------- */

/*! \name AccDiamaterVariables  Radius Variables */
/*@{*/
int diameter_flag = -1;
int diameter_missed_flag = -1;
static char *diameter_extra_str = 0;		/*!< diameter extra variables */
struct acc_extra *diameter_extra = 0;
rd_buf_t *rb;				/*!< buffer used to read from TCP connection*/
char* diameter_client_host="localhost";
int diameter_client_port=3000;

/*@}*/

int sockfd = -1;

static cmd_export_t cmds[] = {
	{"acc_diam_request", (cmd_function)w_acc_diameter_request, 1,
		acc_api_fixup, free_acc_api_fixup,
		ANY_ROUTE},
	{0, 0, 0, 0, 0, 0}
};



static param_export_t params[] = {
	{"diameter_flag",        INT_PARAM, &diameter_flag        },
	{"diameter_missed_flag", INT_PARAM, &diameter_missed_flag },
	{"diameter_client_host", PARAM_STRING, &diameter_client_host },
	{"diameter_client_port", INT_PARAM, &diameter_client_port },
	{"diameter_extra",       PARAM_STRING, &diameter_extra_str },
	{0,0,0}
};


struct module_exports exports= {
	"acc_diameter",
	DEFAULT_DLFLAGS, /* dlopen flags */
	cmds,       /* exported functions */
	params,     /* exported params */
	0,          /* exported RPC methods */
	0,          /* exported pseudo-variables */
	0,          /* response function */
	mod_init,   /* initialization module */
	child_init, /* per-child init function */
	destroy    /* destroy function */
};


/************************** INTERFACE functions ****************************/

static int mod_init( void )
{
	/* bind the ACC API */
	if (acc_load_api(&accb)<0) {
		LM_ERR("cannot bind to ACC API\n");
		return -1;
	}

	/* parse the extra string, if any */
	if (diameter_extra_str
			&& (diameter_extra=accb.parse_extra(diameter_extra_str))==0 ) {
		LM_ERR("failed to parse diameter_extra param\n");
		return -1;
	}

	memset(&_acc_diameter_engine, 0, sizeof(acc_engine_t));

	if(diameter_flag != -1)
		_acc_diameter_engine.acc_flag	   = diameter_flag;
	if(diameter_missed_flag != -1)
		_acc_diameter_engine.missed_flag = diameter_missed_flag;
	_acc_diameter_engine.acc_req     = acc_diameter_send_request;
	_acc_diameter_engine.acc_init    = acc_diameter_init;
	memcpy(_acc_diameter_engine.name, "diameter", 8);
	if(accb.register_engine(&_acc_diameter_engine)<0)
	{
		LM_ERR("cannot register ACC DIAMETER engine\n");
		return -1;
	}

	return 0;
}


static int child_init(int rank)
{
	if (rank==PROC_INIT || rank==PROC_MAIN || rank==PROC_TCP_MAIN)
		return 0; /* do nothing for the main process */

	/* open TCP connection */
	LM_DBG("initializing TCP connection\n");

	sockfd = init_mytcp(diameter_client_host, diameter_client_port);
	if(sockfd==-1)
	{
		LM_ERR("TCP connection not established\n");
		return -1;
	}

	LM_DBG("a TCP connection was established on sockfd=%d\n", sockfd);

	/* every child with its buffer */
	rb = (rd_buf_t*)pkg_malloc(sizeof(rd_buf_t));
	if(!rb)
	{
		PKG_MEM_ERROR;
		return -1;
	}
	rb->buf = 0;

	return 0;
}


static void diam_destroy_extras(acc_extra_t *extra)
{
	acc_extra_t *foo;

	while (extra) {
		foo = extra;
		extra = extra->next;
		pkg_free(foo);
	}
}

static void destroy(void)
{
	close_tcp_connection(sockfd);
	if (diameter_extra)
		diam_destroy_extras(diameter_extra);
}

/************************** FIXUP functions ****************************/

static int acc_api_fixup(void** param, int param_no)
{
	struct acc_param *accp;
	char *p;

	p = (char*)*param;
	if (p==0 || p[0]==0) {
		LM_ERR("first parameter is empty\n");
		return E_SCRIPT;
	}

	if (param_no == 1) {
		accp = (struct acc_param*)pkg_malloc(sizeof(struct acc_param));
		if (!accp) {
			PKG_MEM_ERROR;
			return E_OUT_OF_MEM;
		}
		memset( accp, 0, sizeof(struct acc_param));
		accp->reason.s = p;
		accp->reason.len = strlen(p);
		/* any code? */
		if (accp->reason.len>=3 && isdigit((int)p[0])
				&& isdigit((int)p[1]) && isdigit((int)p[2]) ) {
			accp->code = (p[0]-'0')*100 + (p[1]-'0')*10 + (p[2]-'0');
			accp->code_s.s = p;
			accp->code_s.len = 3;
			accp->reason.s += 3;
			for( ; isspace((int)accp->reason.s[0]) ; accp->reason.s++ );
			accp->reason.len = strlen(accp->reason.s);
		}
		*param = (void*)accp;
	}
	return 0;
}

static int free_acc_api_fixup(void** param, int param_no)
{
	if(*param)
	{
		pkg_free(*param);
		*param = 0;
	}
	return 0;
}


/************************** DIAMETER ACC ****************************/

#define AA_REQUEST 265
#define AA_ANSWER  265

#define ACCOUNTING_REQUEST 271
#define ACCOUNTING_ANSWER  271

static int diam_attrs[ACC_CORE_LEN+MAX_ACC_EXTRA+MAX_ACC_LEG];

int acc_diam_init(acc_extra_t *leg_info)
{
	int n;
	int m;

	n = 0;
	/* caution: keep these aligned to core acc output */
	diam_attrs[n++] = AVP_SIP_METHOD;
	diam_attrs[n++] = AVP_SIP_FROM_TAG;
	diam_attrs[n++] = AVP_SIP_TO_TAG;
	diam_attrs[n++] = AVP_SIP_CALLID;
	diam_attrs[n++] = AVP_SIP_STATUS;

	m = extra2int( diameter_extra, diam_attrs+n);
	if (m<0) {
		LM_ERR("extra names for DIAMETER must be integer AVP codes\n");
		return -1;
	}
	n += m;

	m = extra2int( leg_info, diam_attrs+n);
	if (m<0) {
		LM_ERR("leg info names for DIAMTER must be integer AVP codes\n");
		return -1;
	}
	n += m;

	return 0;
}


int diam_status(struct sip_msg *rq, int code)
{
	if ((rq->REQ_METHOD==METHOD_INVITE || rq->REQ_METHOD==METHOD_ACK)
			&& code>=200 && code<300)
		return AAA_ACCT_START;

	if ((rq->REQ_METHOD==METHOD_BYE || rq->REQ_METHOD==METHOD_CANCEL))
		return AAA_ACCT_STOP;

	if (code>=200 && code <=300)
		return AAA_ACCT_EVENT;

	return -1;
}


int acc_diameter_send_request(sip_msg_t *req, acc_info_t *inf)
{
	int attr_cnt;
	int cnt;
	AAAMessage *send = NULL;
	AAA_AVP *avp;
	struct sip_uri puri;
	str *uri;
	int ret;
	int i;
	int status;
	char tmp[2];
	unsigned int mid;
	int m;
	int o;

	attr_cnt =  accb.get_core_attrs(req, inf->varr, inf->iarr, inf->tarr);
	/* last value is not used */
	attr_cnt--;

	if ( (send=AAAInMessage(ACCOUNTING_REQUEST, AAA_APP_NASREQ))==NULL) {
		LM_ERR("failed to create new AAA request\n");
		return -1;
	}

	m = 0;
	o = 0;
	/* AVP_ACCOUNTIG_RECORD_TYPE */
	if( (status = diam_status(req, inf->env->code))<0) {
		LM_ERR("status unknown\n");
		goto error;
	}
	tmp[0] = status+'0';
	tmp[1] = 0;
	if( (avp=AAACreateAVP(AVP_Accounting_Record_Type, 0, 0, tmp,
					1, AVP_DUPLICATE_DATA)) == 0) {
		LM_ERR("failed to create AVP:no more free memory!\n");
		goto error;
	}
	if( AAAAddAVPToMessage(send, avp, 0)!= AAA_ERR_SUCCESS) {
		LM_ERR("avp not added \n");
		AAAFreeAVP(&avp);
		goto error;
	}
	/* SIP_MSGID AVP */
	mid = req->id;
	if( (avp=AAACreateAVP(AVP_SIP_MSGID, 0, 0, (char*)(&mid),
					sizeof(mid), AVP_DUPLICATE_DATA)) == 0) {
		LM_ERR("failed to create AVP:no more free memory!\n");
		goto error;
	}
	if( AAAAddAVPToMessage(send, avp, 0)!= AAA_ERR_SUCCESS) {
		LM_ERR("avp not added \n");
		AAAFreeAVP(&avp);
		goto error;
	}

	/* SIP Service AVP */
	if( (avp=AAACreateAVP(AVP_Service_Type, 0, 0, SIP_ACCOUNTING,
					SERVICE_LEN, AVP_DUPLICATE_DATA)) == 0) {
		LM_ERR("failed to create AVP:no more free memory!\n");
		goto error;
	}
	if( AAAAddAVPToMessage(send, avp, 0)!= AAA_ERR_SUCCESS) {
		LM_ERR("avp not added \n");
		AAAFreeAVP(&avp);
		goto error;
	}

	/* also the extra attributes */
	o = accb.get_extra_attrs(diameter_extra, req, inf->varr+attr_cnt,
			inf->iarr+attr_cnt, inf->tarr+attr_cnt);
	attr_cnt += o;
	m = attr_cnt;

	/* add attributes */
	for(i=0; i<attr_cnt; i++) {
		if((avp=AAACreateAVP(diam_attrs[i], 0,0, inf->varr[i].s, inf->varr[i].len,
						AVP_DUPLICATE_DATA)) == 0) {
			LM_ERR("failed to create AVP: no more free memory!\n");
			goto error;
		}
		if( AAAAddAVPToMessage(send, avp, 0)!= AAA_ERR_SUCCESS) {
			LM_ERR("avp not added \n");
			AAAFreeAVP(&avp);
			goto error;
		}
	}

	/* and the leg attributes */
	if ( inf->leg_info ) {
		cnt = accb.get_leg_attrs(inf->leg_info,req,inf->varr,inf->iarr,inf->tarr,1);
		do {
			for (i=0; i<cnt; i++) {
				if((avp=AAACreateAVP(diam_attrs[attr_cnt+i], 0, 0,
								inf->varr[i].s, inf->varr[i].len, AVP_DUPLICATE_DATA)) == 0) {
					LM_ERR("failed to create AVP: no more free memory!\n");
					goto error;
				}
				if( AAAAddAVPToMessage(send, avp, 0)!= AAA_ERR_SUCCESS) {
					LM_ERR("avp not added \n");
					AAAFreeAVP(&avp);
					goto error;
				}
			}
		} while ( (attr_cnt=accb.get_leg_attrs(inf->leg_info,req,inf->varr,inf->iarr,
						inf->tarr, 0))!=0 );
	}

	if (get_uri(req, &uri) < 0) {
		LM_ERR("failed to get uri, From/To URI not found\n");
		goto error;
	}

	if (parse_uri(uri->s, uri->len, &puri) < 0) {
		LM_ERR("failed to parse From/To URI\n");
		goto error;
	}

	/* Destination-Realm AVP */
	if( (avp=AAACreateAVP(AVP_Destination_Realm, 0, 0, puri.host.s,
					puri.host.len, AVP_DUPLICATE_DATA)) == 0) {
		LM_ERR("failed to create AVP:no more free memory!\n");
		goto error;
	}

	if( AAAAddAVPToMessage(send, avp, 0)!= AAA_ERR_SUCCESS) {
		LM_ERR("avp not added \n");
		AAAFreeAVP(&avp);
		goto error;
	}

	/* prepare the message to be sent over the network */
	if(AAABuildMsgBuffer(send) != AAA_ERR_SUCCESS) {
		LM_ERR("message buffer not created\n");
		goto error;
	}

	if(sockfd==AAA_NO_CONNECTION) {
		sockfd = init_mytcp(diameter_client_host, diameter_client_port);
		if(sockfd==AAA_NO_CONNECTION) {
			LM_ERR("failed to reconnect to Diameter client\n");
			goto error;
		}
	}

	/* send the message to the DIAMETER client */
	ret = tcp_send_recv(sockfd, send->buf.s, send->buf.len, rb, req->id);
	if(ret == AAA_CONN_CLOSED) {
		LM_NOTICE("connection to Diameter client closed.It will be "
				"reopened by the next request\n");
		close(sockfd);
		sockfd = AAA_NO_CONNECTION;
		goto error;
	}

	if(ret != ACC_SUCCESS) {
		/* a transmission error occurred */
		LM_ERR("message sending to the DIAMETER backend authorization "
				"server failed\n");
		goto error;
	}

	AAAFreeMessage(&send);
	/* free memory allocated by extra2strar */
	free_strar_mem( &(inf->tarr[m-o]), &(inf->varr[m-o]), o, m);
	return 1;

error:
	AAAFreeMessage(&send);
	/* free memory allocated by extra2strar */
	free_strar_mem( &(inf->tarr[m-o]), &(inf->varr[m-o]), o, m);
	return -1;
}

int acc_diameter_init(acc_init_info_t *inf)
{
	if(acc_diam_init(inf->leg_info)<0) {
		LM_ERR("failed to init diameter acc\n");
		return -1;
	}
	return 0;
}

/*! \brief converts the name of the extra from str to integer
 * and stores it over str.len ; str.s is freed and made zero
 */
int extra2int(struct acc_extra *extra, int *attrs)
{
	unsigned int ui;
	int i;

	for( i=0 ; extra ; i++,extra=extra->next ) {
		if (str2int( &extra->name, &ui)!=0) {
			LM_ERR("<%s> is not a number\n", extra->name.s);
			return -1;
		}
		attrs[i] = (int)ui;
	}
	return i;
}

int extra2strar(struct acc_extra *extra, sip_msg_t *rq, str *val_arr,
		int *int_arr, char *type_arr)
{
	pv_value_t value;
	int n;
	int i;

	n = 0;
	i = 0;

	while (extra) {
		/* get the value */
		if (pv_get_spec_value( rq, &extra->spec, &value)!=0) {
			LM_ERR("failed to get '%.*s'\n", extra->name.len,extra->name.s);
		}

		/* check for overflow */
		if (n==MAX_ACC_EXTRA) {
			LM_WARN("array to short -> omitting extras for accounting\n");
			goto done;
		}

		if(value.flags&PV_VAL_NULL) {
			/* convert <null> to empty to have consistency */
			val_arr[n].s = 0;
			val_arr[n].len = 0;
			type_arr[n] = TYPE_NULL;
		} else {
			val_arr[n].s = (char *)pkg_malloc(value.rs.len);
			if (val_arr[n].s == NULL ) {
				PKG_MEM_ERROR;
				/* Cleanup already allocated memory and
				 * return that we didn't do anything */
				for (i = 0; i < n ; i++) {
					if (NULL != val_arr[i].s){
						pkg_free(val_arr[i].s);
						val_arr[i].s = NULL;
					}
				}
				n = 0;
				goto done;
			}
			memcpy(val_arr[n].s, value.rs.s, value.rs.len);
			val_arr[n].len = value.rs.len;
			if (value.flags&PV_VAL_INT) {
				int_arr[n] = value.ri;
				type_arr[n] = TYPE_INT;
			} else {
				type_arr[n] = TYPE_STR;
			}
		}
		n++;

		extra = extra->next;
	}

done:
	return n;
}

/**
 *
 */
static int w_acc_diameter_request(sip_msg_t *rq, char *comment, char *foo)
{
	return accb.exec(rq, &_acc_diameter_engine, (acc_param_t*)comment);
}
