/*
 * Copyright (C) 2012 Smile Communications, jason.penton@smilecoms.com
 * Copyright (C) 2012 Smile Communications, richard.good@smilecoms.com
 *
 * Jason Penton (jason(dot)penton(at)smilecoms.com and
 * Richard Good (richard(dot)good(at)smilecoms.com) as part of an
 * effort to add full IMS support to Kamailio/SR using a new and
 * improved architecture
 *
 * NB: A lot of this code was originally part of OpenIMSCore,
 * FhG Fokus.
 * Copyright (C) 2004-2006 FhG Fokus
 * Thanks for great work! This is an effort to
 * break apart the various CSCF functions into logically separate
 * components. We hope this will drive wider use. We also feel
 * that in this way the architecture is more complete and thereby easier
 * to manage in the Kamailio/SR environment
 *
 * 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 "routing.h"
#include "config.h"
#include "peermanager.h"
#include "diameter_api.h"
#include "math.h"

#define LB_MAX_PEERS \
	20 /**< maximum peers that can be loadbalanced accross i.e. same metric */

extern dp_config *config; /**< Configuration for this diameter peer 	*/
int gcount = 0;


/**
 * Returns if the peer advertised support for an Application ID
 * @param p - the peer to check
 * @param app_id - the application id to look for
 * @param vendor_id - the vendor id to look for, 0 if not vendor specific
 * @returns 0 if not found, 1 if found
 */
int peer_handles_application(peer *p, int app_id, int vendor_id)
{
	int i;
	LM_DBG("Checking if peer %.*s handles application %d for vendord %d\n",
			p->fqdn.len, p->fqdn.s, app_id, vendor_id);
	if(!p || !p->applications || !p->applications_cnt)
		return 0;
	for(i = 0; i < p->applications_cnt; i++)
		if(p->applications[i].id == app_id
				&& p->applications[i].vendor == vendor_id)
			return 1;
	return 0;
}

/**
 * Get the first peer that is connected from the list of routing entries.
 * @param r - the list of routing entries to look into
 * @returns - the peer or null if none connected
 */
peer *get_first_connected_route(
		cdp_session_t *cdp_session, routing_entry *r, int app_id, int vendor_id)
{
	peer *peers[LB_MAX_PEERS];
	int peer_count = 0;
	int prev_metric = 0;
	routing_entry *i;
	peer *p;
	int j;
	time_t least_recent_time;
	struct timespec time_spec;

	if(cdp_session) {
		/*try and find an already used peer for this session - sticky*/
		if((cdp_session->sticky_peer_fqdn.len > 0)
				&& cdp_session->sticky_peer_fqdn.s) {
			//we have an old sticky peer. let's make sure it's up and connected before we use it.
			AAASessionsUnlock(
					cdp_session
							->hash); /*V1.1 - Don't attempt to hold two locks at same time */
			p = get_peer_by_fqdn(&cdp_session->sticky_peer_fqdn);
			AAASessionsLock(
					cdp_session
							->hash); /*V1.1 - As we were...no call seems to pass cdp_session unlocked */
			if(p && !p->disabled && (p->state == I_Open || p->state == R_Open)
					&& (peer_handles_application(p, app_id, vendor_id)
							|| peer_handles_application(p, RELAY_APP_ID, 0))) {
				p->last_selected = time(NULL);
				LM_DBG("Found a sticky peer [%.*s] for this session - "
					   "re-using\n",
						p->fqdn.len, p->fqdn.s);
				return p;
			}
		}
	}

	for(i = r; i; i = i->next) {
		if(peer_count >= LB_MAX_PEERS)
			break;
		p = get_peer_by_fqdn(&(i->fqdn));
		if(!p)
			LM_DBG("The peer %.*s does not seem to be connected or "
				   "configured\n",
					i->fqdn.len, i->fqdn.s);
		else
			LM_DBG("The peer %.*s state is %s\n", i->fqdn.len, i->fqdn.s,
					(p->state == I_Open || p->state == R_Open) ? "opened"
															   : "closed");
		if(p && !p->disabled && (p->state == I_Open || p->state == R_Open)
				&& (peer_handles_application(p, app_id, vendor_id)
						|| peer_handles_application(p, RELAY_APP_ID, 0))) {
			LM_DBG("The peer %.*s matches - will forward there\n", i->fqdn.len,
					i->fqdn.s);
			if(peer_count != 0) { //check the metric
				if(i->metric != prev_metric)
					break;
				//metric must be the same
				peers[peer_count++] = p;
			} else { //we're first
				prev_metric = i->metric;
				peers[peer_count++] = p;
			}
		}
	}

	if(peer_count == 0) {
		return 0;
	}

	least_recent_time = peers[0]->last_selected;
	LM_DBG("peer [%.*s] was last used @ %" TIME_T_FMT "\n", peers[0]->fqdn.len,
			peers[0]->fqdn.s, TIME_T_CAST(peers[0]->last_selected));
	p = peers[0];
	for(j = 1; j < peer_count; j++) {
		LM_DBG("Peer [%.*s] was last used at [%" TIME_T_FMT "]\n",
				peers[j]->fqdn.len, peers[j]->fqdn.s,
				TIME_T_CAST(peers[j]->last_selected));
		if(peers[j]->last_selected < least_recent_time) {
			least_recent_time = peers[j]->last_selected;
			p = peers[j];
		}
	}

	ksr_clock_gettime(&time_spec);

	p->last_selected = (time_spec.tv_sec * 1000000)
					   + round(time_spec.tv_nsec
							   / 1.0e3); // Convert nanoseconds to microseconds
	LM_DBG("chosen peer [%.*s]\n", p->fqdn.len, p->fqdn.s);

	if(cdp_session) {
		if(cdp_session->sticky_peer_fqdn_buflen <= p->fqdn.len) {
			LM_DBG("not enough storage for sticky peer - allocating more\n");
			if(cdp_session->sticky_peer_fqdn.s)
				shm_free(cdp_session->sticky_peer_fqdn.s);

			cdp_session->sticky_peer_fqdn.s =
					(char *)shm_malloc(p->fqdn.len + 1);
			if(!cdp_session->sticky_peer_fqdn.s) {
				LM_ERR("no more shm memory\n");
				return 0;
			}
			cdp_session->sticky_peer_fqdn_buflen = p->fqdn.len + 1;
			memset(cdp_session->sticky_peer_fqdn.s, 0, p->fqdn.len + 1);
		}
		cdp_session->sticky_peer_fqdn.len = p->fqdn.len;
		memcpy(cdp_session->sticky_peer_fqdn.s, p->fqdn.s, p->fqdn.len);
	}

	return p;
}


/**
 * Sub-function to route a message based on the application id and vendor id.
 * This allows the main routing to try multiple times, e.g. in case it finds
 * multiple app_id/vendor_id combinations in the message.
 */
peer *get_routing_peer_by_app_id(cdp_session_t *cdp_session,
		str destination_realm, int app_id, int vendor_id)
{
	peer *p = 0;
	routing_realm *rr = 0;

	if(!config->r_table) {
		LM_ERR("Empty routing table.\n");
		return 0;
	}

	// 1. Try the Realm-based routing table, if Destination-Realm is present
	if(destination_realm.len) {
		/* first search for the destination realm */
		for(rr = config->r_table->realms; rr; rr = rr->next)
			if(rr->realm.len == destination_realm.len
					&& strncasecmp(rr->realm.s, destination_realm.s,
							   destination_realm.len)
							   == 0)
				break;
		if(rr) {
			p = get_first_connected_route(
					cdp_session, rr->routes, app_id, vendor_id);
			if(p)
				return p;
			else
				LM_WARN("No connected Route peer "
						"found for Realm <%.*s>. Trying DefaultRoutes "
						"next...\n",
						destination_realm.len, destination_realm.s);
		}
	}

	// 2. Try the DefaultRoute table
	LM_DBG("no routing peer found, trying default route\n");
	p = get_first_connected_route(
			cdp_session, config->r_table->routes, app_id, vendor_id);
	if(!p) {
		LM_WARN("No connected DefaultRoute peer "
				"found for app_id %d and vendor_id %d.\n",
				app_id, vendor_id);
	}
	return p;
}

/**
 * Get the first connect peer that matches the routing mechanisms.
 * - First the Destination-Host AVP value is tried if connected (the peer does not have to
 * be in the routing table at all).
 * - Then we look for a connected peer in the specific realm for the Destination-Realm AVP
 * - Then we look for the first connected peer in the default routes
 * @param m - the Diameter message to find the destination peer for
 * @returns - the connected peer or null if none connected found
 */
peer *get_routing_peer(cdp_session_t *cdp_session, AAAMessage *m)
{
	str destination_realm = {0, 0}, destination_host = {0, 0};
	AAA_AVP *avp, *avp_vendor, *avp2;
	AAA_AVP_LIST group;
	peer *p;
	int app_id = 0, vendor_id = 0;

	// 1. Destination-Host routing to adjacent peer
	avp = AAAFindMatchingAVP(m, 0, AVP_Destination_Host, 0, AAA_FORWARD_SEARCH);
	if(avp)
		destination_host = avp->data;

	if(destination_host.len) {
		/* There is a destination host present in the message try and route directly there */
		p = get_peer_by_fqdn(&destination_host);
		if(p && (p->state == I_Open || p->state == R_Open)
				&& peer_handles_application(p, app_id, vendor_id)) {
			p->last_selected = time(NULL);
			return p;
		}
		/* the destination host peer is not connected at the moment, try a normal route then */
	}

	if(!config->r_table) {
		LM_ERR("Empty routing table.\n");
		return 0;
	}

	LM_DBG("getting diameter routing peer for realm: [%.*s]\n",
			m->dest_realm->data.len, m->dest_realm->data.s);
	avp = AAAFindMatchingAVP(
			m, 0, AVP_Destination_Realm, 0, AAA_FORWARD_SEARCH);
	if(avp)
		destination_realm = avp->data;

	// 2. Try using the Vendor-Specific-Application-Id
	app_id = m->applicationId;
	avp = AAAFindMatchingAVP(
			m, 0, AVP_Vendor_Specific_Application_Id, 0, AAA_FORWARD_SEARCH);
	if(avp) {
		group = AAAUngroupAVPS(avp->data);
		avp_vendor =
				AAAFindMatchingAVPList(group, group.head, AVP_Vendor_Id, 0, 0);
		if(avp_vendor) {
			vendor_id = get_4bytes(avp_vendor->data.s);
		}
		// 2.1. Authorization Application-Id
		avp2 = AAAFindMatchingAVPList(
				group, group.head, AVP_Auth_Application_Id, 0, 0);
		if(avp2) {
			app_id = get_4bytes(avp2->data.s);
			p = get_routing_peer_by_app_id(
					cdp_session, destination_realm, app_id, vendor_id);
			if(p) {
				AAAFreeAVPList(&group);
				return p;
			}
		}
		// 2.2. Accounting Application-Id
		avp2 = AAAFindMatchingAVPList(
				group, group.head, AVP_Acct_Application_Id, 0, 0);
		if(avp2) {
			app_id = get_4bytes(avp2->data.s);
			p = get_routing_peer_by_app_id(
					cdp_session, destination_realm, app_id, vendor_id);
			if(p) {
				AAAFreeAVPList(&group);
				return p;
			}
		}
		AAAFreeAVPList(&group);
	}

	// 3. Try with Vendor-Id (opt) and Auth/Acct Application-Id at top level
	avp_vendor = AAAFindMatchingAVP(m, 0, AVP_Vendor_Id, 0, AAA_FORWARD_SEARCH);
	if(avp_vendor) {
		vendor_id = get_4bytes(avp_vendor->data.s);
	}
	// 3.1. Authorization Application-Id
	avp = AAAFindMatchingAVP(
			m, 0, AVP_Auth_Application_Id, 0, AAA_FORWARD_SEARCH);
	if(avp) {
		app_id = get_4bytes(avp->data.s);
		p = get_routing_peer_by_app_id(
				cdp_session, destination_realm, app_id, vendor_id);
		if(p)
			return p;
	}
	// 3.2. Accounting Application-Id
	avp = AAAFindMatchingAVP(
			m, 0, AVP_Acct_Application_Id, 0, AAA_FORWARD_SEARCH);
	if(avp) {
		app_id = get_4bytes(avp->data.s);
		p = get_routing_peer_by_app_id(
				cdp_session, destination_realm, app_id, vendor_id);
		if(p)
			return p;
	}

	// Give up

	return 0;
}
