/*
 * File:   ro_timer.c
 * Author: Jason Penton
 *
 * Created on 06 April 2011, 1:37 PM
 */

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>

#include "../../core/mem/shm_mem.h"
#include "../ims_dialog/dlg_load.h"
#include "ro_timer.h"
#include "ro_session_hash.h"
#include "ims_ro.h"
#include "ro_db_handler.h"
#include "ims_charging_mod.h"
#include "ims_charging_stats.h"
#include "../../core/counters.h"

extern int interim_request_credits;
extern int ro_timer_buffer;
extern int ro_db_mode;
extern ims_dlg_api_t dlgb;
extern struct ims_charging_counters_h ims_charging_cnts_h;

/*! global dialog timer */
struct ro_timer *roi_timer = 0;
/*! global dialog timer handler */
ro_timer_handler timer_hdl = 0;

/*!
 * \brief Initialize the ro_session timer handler
 * Initialize the ro_session timer handler, allocate the lock and a global
 * timer in shared memory. The global timer handler will be set on success.
 * \param hdl ro_session timer handler
 * \return 0 on success, -1 on failure
 */
int init_ro_timer(ro_timer_handler hdl)
{
	roi_timer = (struct ro_timer *)shm_malloc(sizeof(struct ro_timer));
	if(roi_timer == 0) {
		LM_ERR("no more shm mem\n");
		return -1;
	}
	memset(roi_timer, 0, sizeof(struct ro_timer));

	roi_timer->first.next = roi_timer->first.prev = &(roi_timer->first);

	roi_timer->lock = lock_alloc();
	if(roi_timer->lock == 0) {
		LM_ERR("failed to alloc lock\n");
		goto error0;
	}

	if(lock_init(roi_timer->lock) == 0) {
		LM_ERR("failed to init lock\n");
		goto error1;
	}

	timer_hdl = hdl;
	return 0;
error1:
	lock_dealloc(roi_timer->lock);
error0:
	shm_free(roi_timer);
	roi_timer = 0;
	return -1;
}

/*!
 * \brief Destroy global ro_session timer
 */
void destroy_ro_timer(void)
{
	if(roi_timer == 0)
		return;

	lock_destroy(roi_timer->lock);
	lock_dealloc(roi_timer->lock);

	shm_free(roi_timer);
	roi_timer = 0;
}

/*!
 * \brief Helper function for insert_ro_session_timer
 * \see insert_ro_session_timer
 * \param tl ro_session timer list
 */
static inline void insert_ro_timer_unsafe(struct ro_tl *tl)
{
	struct ro_tl *ptr;

	/* insert in sorted order */
	for(ptr = roi_timer->first.prev; ptr != &roi_timer->first;
			ptr = ptr->prev) {
		if(ptr->timeout <= tl->timeout)
			break;
	}

	LM_DBG("inserting %p for %d\n", tl, tl->timeout);
	LM_DBG("BEFORE ptr [%p], ptr->next [%p], ptr->next->prev [%p]\n", ptr,
			ptr->next, ptr->next->prev);
	tl->prev = ptr;
	tl->next = ptr->next;
	tl->prev->next = tl;
	tl->next->prev = tl;
	LM_DBG("AFTER tl->prev [%p], tl->next [%p]\n", tl->prev, tl->next);
}

/*!
 * \brief Insert a ro_session timer to the list
 * \param tl ro_session timer list
 * \param interval timeout value in seconds
 * \return 0 on success, -1 when the input timer list is invalid
 */
int insert_ro_timer(struct ro_tl *tl, int interval)
{
	lock_get(roi_timer->lock);

	LM_DBG("inserting timer for interval [%i]\n", interval);
	if(tl->next != 0 || tl->prev != 0) {
		lock_release(roi_timer->lock);
		LM_CRIT("Trying to insert a bogus ro tl=%p tl->next=%p tl->prev=%p\n",
				tl, tl->next, tl->prev);
		return -1;
	}
	tl->timeout = get_ticks() + interval;
	insert_ro_timer_unsafe(tl);


	LM_DBG("TIMER inserted\n");
	lock_release(roi_timer->lock);

	return 0;
}

/*!
 * \brief Helper function for remove_ro_session_timer
 * \param tl ro_session timer list
 * \see remove_ro_session_timer
 */
static inline void remove_ro_timer_unsafe(struct ro_tl *tl)
{
	tl->prev->next = tl->next;
	tl->next->prev = tl->prev;
}

/*!
 * \brief Remove a ro_session timer from the list
 * \param tl ro_session timer that should be removed
 * \return 1 when the input timer is empty, 0 when the timer was removed,
 * -1 when the input timer list is invalid
 */
int remove_ro_timer(struct ro_tl *tl)
{
	lock_get(roi_timer->lock);

	if(tl->prev == NULL && tl->timeout == 0) {
		lock_release(roi_timer->lock);
		return 1;
	}

	if(tl->prev == NULL || tl->next == NULL) {
		LM_CRIT("bogus tl=%p tl->prev=%p tl->next=%p\n", tl, tl->prev,
				tl->next);
		lock_release(roi_timer->lock);
		return -1;
	}
	LM_DBG("TIMER [%p] REMOVED\n", tl);
	remove_ro_timer_unsafe(tl);
	tl->next = NULL;
	tl->prev = NULL;
	tl->timeout = 0;

	lock_release(roi_timer->lock);
	return 0;
}

/*!
 * \brief Update a ro_session timer on the list
 * \param tl dialog timer
 * \param timeout new timeout value in seconds
 * \return 0 on success, -1 when the input list is invalid
 * \note the update is implemented as a remove, insert
 */
int update_ro_timer(struct ro_tl *tl, int timeout)
{
	lock_get(roi_timer->lock);

	LM_DBG("Updating ro timer [%p] with timeout [%d]\n", tl, timeout);
	if(tl->next) {
		if(tl->prev == 0) {
			lock_release(roi_timer->lock);
			return -1;
		}
		remove_ro_timer_unsafe(tl);
	}

	tl->timeout = get_ticks() + timeout;
	insert_ro_timer_unsafe(tl);

	lock_release(roi_timer->lock);
	return 0;
}

/*!
 * \brief Helper function for ro_timer_routine
 * \param time time for expiration check
 * \return list of expired credit reservations on sessions on success, 0 on failure
 */
static inline struct ro_tl *get_expired_ro_sessions(unsigned int time)
{
	struct ro_tl *tl, *end, *ret;

	lock_get(roi_timer->lock);

	LM_DBG("my ticks are [%d]\n", time);

	if(roi_timer->first.next == &(roi_timer->first)
			|| roi_timer->first.next->timeout > time) {
		lock_release(roi_timer->lock);
		return 0;
	}

	end = &roi_timer->first;
	tl = roi_timer->first.next;
	LM_DBG("start with tl=%p tl->prev=%p tl->next=%p (%d) at %d and end with "
		   "end=%p end->prev=%p end->next=%p\n",
			tl, tl->prev, tl->next, tl->timeout, time, end, end->prev,
			end->next);
	while(tl != end && tl->timeout <= time) {
		LM_DBG("getting tl=%p tl->prev=%p tl->next=%p with %d\n", tl, tl->prev,
				tl->next, tl->timeout);
		tl->prev = 0;
		tl->timeout = 0;
		tl = tl->next;
	}
	LM_DBG("end with tl=%p tl->prev=%p tl->next=%p and "
		   "d_timer->first.next->prev=%p\n",
			tl, tl->prev, tl->next, roi_timer->first.next->prev);

	if(tl == end && roi_timer->first.next->prev) {
		ret = 0;
	} else {
		ret = roi_timer->first.next;
		tl->prev->next = 0;
		roi_timer->first.next = tl;
		tl->prev = &roi_timer->first;
	}

	lock_release(roi_timer->lock);

	return ret;
}

/*!
 * \brief Timer routine for expiration of credit reservations
 * Timer handler for expiration of credit reservations on a session, runs the global timer handler on them.
 * \param time for expiration checks
 * \param attr unused
 */
void ro_timer_routine(unsigned int ticks, void *attr)
{

	struct ro_tl *tl, *ctl;
	LM_DBG("getting expired ro-sessions\n");

	tl = get_expired_ro_sessions(ticks);

	while(tl) {
		ctl = tl;
		tl = tl->next;
		ctl->next = NULL;
		LM_DBG("Ro Session Timer firing: tl=%p next=%p\n", ctl, tl);
		timer_hdl(ctl);
	}
}

void resume_ro_session_ontimeout(
		struct interim_ccr *i_req, int timeout_or_error)
{
	time_t now = get_current_time_micro();
	long used_secs;
	struct ro_session_entry *ro_session_entry = NULL;
	int call_terminated = 0;

	if(!i_req) {
		LM_ERR("This is so wrong: i_req is NULL\n");
		return;
	}

	ro_session_entry = &(ro_session_table->entries[i_req->ro_session->h_entry]);
	ro_session_lock(ro_session_table, ro_session_entry);
	LM_DBG("credit=%d credit_valid_for=%d\n", i_req->new_credit,
			i_req->credit_valid_for);

	used_secs = rint(
			(now
					- ((timeout_or_error == 1
							   && i_req->ro_session->last_event_timestamp_backup
										  > 0)
									? i_req->ro_session
											  ->last_event_timestamp_backup
									: i_req->ro_session->last_event_timestamp))
			/ (float)1000000);

	/* check to make sure diameter server is giving us sane values */
	if(i_req->credit_valid_for != 0
			&& i_req->new_credit > i_req->credit_valid_for) {
		LM_WARN("That's weird, Diameter server gave us credit with a lower "
				"validity period :D. Setting reserved time to validity period "
				"instead \n");
		i_req->new_credit = i_req->credit_valid_for;
	}

	if(i_req->new_credit > 0) {
		//now insert the new timer
		i_req->ro_session->last_event_timestamp = get_current_time_micro();
		i_req->ro_session->event_type = answered;
		i_req->ro_session->valid_for = i_req->credit_valid_for;

		int ret = 0;
		if(i_req->is_final_allocation) {
			LM_DBG("This is a final allocation and call will end in %i "
				   "seconds\n",
					i_req->new_credit);
			i_req->ro_session->event_type = no_more_credit;
			ret = insert_ro_timer(&i_req->ro_session->ro_tl, i_req->new_credit);
		} else {
			int timer_timeout = i_req->new_credit;

			if(i_req->new_credit
					> i_req->ro_session->ro_timer_buffer /*TIMEOUTBUFFER*/) {

				// We haven't finished using our 1st block of units, and we need to set the timer to
				// (new_credit - i_req->ro_session->ro_timer_buffer[5 secs]) to ensure we get new credit before our previous
				// reservation is exhausted. This will only be done the first time, because the timer
				// will always be fired 5 seconds before we run out of time thanks to this operation
				timer_timeout =
						i_req->new_credit - i_req->ro_session->ro_timer_buffer;
			}

			ret = insert_ro_timer(&i_req->ro_session->ro_tl, timer_timeout);
		}

		// update to the new block of units we got
		i_req->ro_session->reserved_secs = i_req->new_credit;

		if(ret != 0) {
			LM_CRIT("unable to insert timer for Ro Session [%.*s]\n",
					i_req->ro_session->ro_session_id.len,
					i_req->ro_session->ro_session_id.s);
		} else {
			ref_ro_session(i_req->ro_session, 1, 0);
		}

		i_req->ro_session->flags |= RO_SESSION_FLAG_CHANGED;
		if(ro_db_mode == DB_MODE_REALTIME) {
			if(update_ro_dbinfo_unsafe(i_req->ro_session) != 0) {
				LM_ERR("Failed to update Ro session in DB... continuing\n");
			}
		}
	} else {
		/* just put the timer back in with however many seconds are left (if any!!! in which case we need to kill */
		/* also update the event type to no_more_credit to save on processing the next time we get here */
		i_req->ro_session->event_type = no_more_credit;
		if(!timeout_or_error)
			i_req->ro_session->last_event_timestamp = get_current_time_micro();

		int whatsleft = i_req->ro_session->reserved_secs - used_secs;
		if(whatsleft <= 0) {
			// TODO we need to handle this situation more precisely.
			// in case CCR times out, we get a call shutdown but the error message assumes it was due to a lack of credit.
			//
			LM_WARN("Immediately killing call due to no more credit *OR* no "
					"CCA received (timeout) after reservation request\n");

			//
			// we need to unlock the session or else we might get a deadlock on dlg_terminated() dialog callback.
			// Do not unref the session because it will be made inside the dlg_terminated() function.
			//

			//unref_ro_session_unsafe(i_req->ro_session, 1, ro_session_entry);
			ro_session_unlock(ro_session_table, ro_session_entry);

			dlgb.lookup_terminate_dlg(i_req->ro_session->dlg_h_entry,
					i_req->ro_session->dlg_h_id, NULL);
			call_terminated = 1;
		} else {
			LM_DBG("No more credit for user - letting call run out of money in "
				   "[%i] seconds\n",
					whatsleft);
			int ret = insert_ro_timer(&i_req->ro_session->ro_tl, whatsleft);
			if(ret != 0) {
				LM_CRIT("unable to insert timer for Ro Session [%.*s]\n",
						i_req->ro_session->ro_session_id.len,
						i_req->ro_session->ro_session_id.s);
			} else {
				ref_ro_session(i_req->ro_session, 1, 0);
			}
		}
	}

	//
	// if call was forcefully terminated, the lock was released before dlgb.lookup_terminate_dlg() function call.
	//
	if(!call_terminated) {
		unref_ro_session(i_req->ro_session, 1,
				0); //unref from the initial timer that fired this event.
		ro_session_unlock(ro_session_table, ro_session_entry);
	}

	shm_free(i_req);
	LM_DBG("Exiting async ccr interim nicely\n");
}

/* this is the function called when a we need to request more funds/credit. We need to try and reserve more credit.
 * If we can't we need to put a new timer to kill the call at the appropriate time
 */
void ro_session_ontimeout(struct ro_tl *tl)
{
	time_t now, call_time;
	long used_secs;
	int adjustment;
	str default_out_of_credit_hdrs = {"Reason: outofcredit\r\n", 21};

	LM_DBG("We have a fired timer [p=%p] and tl=[%i].\n", tl, tl->timeout);

	/* find the session id for this timer*/
	struct ro_session *ro_session = ((struct ro_session
					*)((char *)(tl)
					   - (unsigned long)(&((struct ro_session *)0)->ro_tl)));
	LM_DBG("offset for ro_tl is [%lu] and ro_session id is [%.*s]\n",
			(unsigned long)(&((struct ro_session *)0)->ro_tl),
			ro_session->ro_session_id.len, ro_session->ro_session_id.s);

	if(!ro_session) {
		LM_ERR("Can't find a session. This is bad\n");
		return;
	}

	LM_DBG("event-type=%d\n", ro_session->event_type);

	//	if (!ro_session->active) {
	//		LM_ALERT("Looks like this session was terminated while requesting more units\n");
	//		goto exit;
	//		return;
	//	}


	if(ro_session->is_final_allocation) {
		now = get_current_time_micro();
		used_secs = now - ro_session->last_event_timestamp;
		if((ro_session->reserved_secs - used_secs) > 0) {
			update_ro_timer(&ro_session->ro_tl,
					(ro_session->reserved_secs - used_secs));
			return;
		} else {
			ro_session->event_type = no_more_credit;
		}
	}

	switch(ro_session->event_type) {
		case answered:
			now = get_current_time_micro();
			used_secs = rint(
					(now - ro_session->last_event_timestamp) / (float)1000000);
			call_time = rint((now - ro_session->start_time) / (float)1000000);

			if((used_secs + ro_session->billed) < (call_time)) {
				adjustment = call_time - (used_secs + ro_session->billed);
				LM_DBG("Making adjustment for Ro interim timer by adding %d "
					   "seconds\n",
						adjustment);
				used_secs += adjustment;
			}

			counter_add(ims_charging_cnts_h.billed_secs, used_secs);

			if(ro_session->callid.s != NULL
					&& ro_session->ro_session_id.s != NULL) {
				LM_DBG("Found a session to re-apply for timing [%.*s] and user "
					   "is [%.*s]\n",
						ro_session->ro_session_id.len,
						ro_session->ro_session_id.s,
						ro_session->asserted_identity.len,
						ro_session->asserted_identity.s);

				LM_DBG("Call session has been active for %i seconds. The last "
					   "reserved secs was [%i] and the last event was [%i "
					   "seconds] ago\n",
						(unsigned int)call_time,
						(unsigned int)ro_session->reserved_secs,
						(unsigned int)used_secs);

				LM_DBG("Call session [p=%p]: we will now make a request for "
					   "another [%i] of credit with a usage of [%i] seconds "
					   "from the last bundle.\n",
						ro_session,
						interim_request_credits /* new reservation request amount */
						,
						(unsigned int)
								used_secs /* charged seconds from previous reservation */);

				// Apply for more credit.
				//
				// The function call will return immediately and we will receive the reply asynchronously via a callback
				ro_session->billed += used_secs;
				send_ccr_interim(ro_session, (unsigned int)used_secs,
						interim_request_credits);
				return;
			} else {
				LM_ERR("Hmmm, the session we have either doesn't have all the "
					   "data or something else has gone wrong.\n");
				/* put the timer back so the call will be killed according to previous timeout. */
				ro_session->event_type = unknown_error;
				int ret = insert_ro_timer(&ro_session->ro_tl,
						(ro_session->reserved_secs - used_secs) / 1000000);
				if(ret != 0) {
					LM_CRIT("unable to insert timer for Ro Session [%.*s]\n",
							ro_session->ro_session_id.len,
							ro_session->ro_session_id.s);
				} else {
					ref_ro_session(ro_session, 1, 0);
					return;
				}
				LM_ERR("Immediately killing call due to unknown error\n");
			}
			break;
		case delayed_delete:
			destroy_ro_session(ro_session);
			return;
		case pending:
			/* call is not answered yet. No point asking more credit. Just wait for dialog to progress somehow */
			return;
		default:
			LM_ERR("Diameter call session - event [%d]\n",
					ro_session->event_type);

			if(ro_session->event_type == no_more_credit)
				LM_INFO("Call/session must be ended - no more funds.\n");
			else if(ro_session->event_type == unknown_error)
				LM_ERR("last event caused an error. We will now tear down this "
					   "session.\n");
	}

	counter_inc(ims_charging_cnts_h.killed_calls);

	dlgb.lookup_terminate_dlg(ro_session->dlg_h_entry, ro_session->dlg_h_id,
			&default_out_of_credit_hdrs);
	return;
}
