/*
 * Handle ESI events
 *
 * Copyright (C) 2007 Olaf Kirch <olaf.kirch@oracle.com>
 */

#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "config.h"
#include <libisns/isns.h>
#include <libisns/attrs.h>
#include "objects.h"
#include <libisns/message.h>
#include "security.h"
#include <libisns/util.h>
#include "db.h"

#define ESI_RETRANS_TIMEOUT	60

typedef struct isns_esi isns_esi_t;
typedef struct isns_esi_portal isns_esi_portal_t;

struct isns_esi {
	isns_list_t		esi_list;
	isns_object_t *		esi_object;
	isns_list_t		esi_portals;

	unsigned int		esi_update : 1;
};

struct isns_esi_portal {
	isns_list_t		esp_list;
	isns_object_t *		esp_object;
	isns_portal_info_t	esp_portal;
	unsigned int		esp_interval;
	isns_portal_info_t	esp_dest;

	isns_socket_t *		esp_socket;
	unsigned int		esp_retries;
	unsigned int		esp_timeout;
	time_t			esp_start;
	time_t			esp_next_xmit;
	uint32_t		esp_xid;
};

int			isns_esi_enabled = 0;
static isns_server_t *	isns_esi_server = NULL;
static ISNS_LIST_DECLARE(isns_esi_list);

static void		isns_esi_transmit(void *);
static void		isns_esi_sendto(isns_esi_t *, isns_esi_portal_t *);
static void		isns_process_esi_response(uint32_t, int,
						  isns_simple_t *);
static void		isns_esi_disconnect(isns_esi_portal_t *);
static void		isns_esi_restart(isns_esi_portal_t *);
static void		isns_esi_drop_portal(isns_esi_portal_t *, isns_db_t *, int);
static void		isns_esi_drop_entity(isns_esi_t *, isns_db_t *, int);
static int		isns_esi_update(isns_esi_t *);
static void		isns_esi_schedule(int);
static void		isns_esi_callback(const isns_db_event_t *, void *);

void
isns_esi_init(isns_server_t *srv)
{
	if (isns_config.ic_esi_retries == 0) {
		isns_debug_esi("ESI disabled by administrator\n");
	} else {
		unsigned int	max_interval;

		isns_register_callback(isns_esi_callback, NULL);
		isns_esi_schedule(0);

		max_interval = isns_config.ic_registration_period / 2;
		if (isns_config.ic_esi_max_interval > max_interval) {
			isns_warning("Max ESI interval adjusted to %u sec "
					"to match registration period\n",
					max_interval);
			isns_config.ic_esi_max_interval = max_interval;
			if (isns_config.ic_esi_min_interval > max_interval)
				isns_config.ic_esi_min_interval = max_interval;
		}
		isns_esi_server = srv;
		isns_esi_enabled = 1;
	}
}

/*
 * Timer callback to send out ESI messages.
 */
void
isns_esi_transmit(__attribute__((unused))void *ptr)
{
	isns_db_t	*db = isns_esi_server->is_db;
	isns_list_t	*esi_pos, *esi_next;
	time_t		now;
	isns_object_t	*obj;
	time_t		next_timeout;

	now = time(NULL);
	next_timeout = now + 3600;

	isns_list_foreach(&isns_esi_list, esi_pos, esi_next) {
		isns_list_t	*esp_pos, *esp_next;
		isns_esi_t	*esi = isns_list_item(isns_esi_t, esi_list, esi_pos);

		if (esi->esi_update) {
			esi->esi_update = 0;
			if (!isns_esi_update(esi))
				continue;
		}

		isns_list_foreach(&esi->esi_portals, esp_pos, esp_next) {
			isns_esi_portal_t *esp = isns_list_item(isns_esi_portal_t,
							esp_list, esp_pos);

			/* Check whether the portal object still exist */
			obj = esp->esp_object;
			if (obj->ie_state != ISNS_OBJECT_STATE_MATURE) {
				isns_esi_drop_portal(esp, db, 0);
				continue;
			}

			if (esp->esp_next_xmit <= now) {
				if (esp->esp_retries == 0) {
					isns_debug_esi("No ESI response from %s - dropping\n",
							isns_portal_string(&esp->esp_dest));
					isns_esi_drop_portal(esp, db, 1);
					continue;
				}

				esp->esp_retries -= 1;
				esp->esp_next_xmit = now + esp->esp_timeout;
				isns_esi_sendto(esi, esp);
			}
			if (esp->esp_next_xmit < next_timeout)
				next_timeout = esp->esp_next_xmit;
		}

		if (isns_list_empty(&esi->esi_portals))
			isns_esi_drop_entity(esi, db, 1);
	}

	isns_debug_esi("Next ESI message in %d seconds\n", next_timeout - now);
	isns_esi_schedule(next_timeout - now);
}

/*
 * Send an ESI message
 */
void
isns_esi_sendto(isns_esi_t *esi, isns_esi_portal_t *esp)
{
	isns_attr_list_t attrs = ISNS_ATTR_LIST_INIT;
	isns_socket_t	*sock;
	isns_simple_t	*msg;

	/* For TCP portals, kill the TCP socket every time. */
	if (esp->esp_dest.proto == IPPROTO_TCP)
		isns_esi_disconnect(esp);

	if (esp->esp_socket == NULL) {
		sock = isns_connect_to_portal(&esp->esp_dest);
		if (sock == NULL)
			return;

		isns_socket_set_security_ctx(sock,
			isns_default_security_context(0));
		/* sock->is_disconnect_fatal = 1; */
		esp->esp_socket = sock;
	}

	isns_attr_list_append_uint64(&attrs,
				ISNS_TAG_TIMESTAMP,
				time(NULL));
	/* The following will extract the ENTITY IDENTIFIER */
	isns_object_extract_keys(esi->esi_object, &attrs);
	isns_portal_to_attr_list(&esp->esp_portal,
				ISNS_TAG_PORTAL_IP_ADDRESS,
				ISNS_TAG_PORTAL_TCP_UDP_PORT,
				&attrs);

	msg = isns_simple_create(ISNS_ENTITY_STATUS_INQUIRY,
					NULL, &attrs);
	if (msg == NULL)
		return;

	isns_debug_esi("*** Sending ESI message to %s (xid=0x%x); %u retries left\n",
			isns_portal_string(&esp->esp_dest),
			msg->is_xid, esp->esp_retries);
	isns_simple_transmit(esp->esp_socket, msg,
			NULL, esp->esp_timeout - 1,
			isns_process_esi_response);
	esp->esp_xid = msg->is_xid;
	isns_simple_free(msg);
}

/*
 * A new entity was added. See if it uses ESI, and create
 * portals and such.
 */
static void
isns_esi_add_entity(isns_object_t *obj)
{
	isns_esi_t	*esi;

	isns_debug_esi("Enable ESI monitoring for entity %u\n", obj->ie_index);
	esi = isns_calloc(1, sizeof(*esi));
	esi->esi_object = isns_object_get(obj);
	esi->esi_update = 1;
	isns_list_init(&esi->esi_list);
	isns_list_init(&esi->esi_portals);

	isns_list_append(&isns_esi_list, &esi->esi_list);
}

/*
 * Given an entity, see if we can find ESI state for it.
 */
static isns_esi_t *
isns_esi_find(isns_object_t *obj)
{
	isns_list_t	*pos, *next;

	isns_list_foreach(&isns_esi_list, pos, next) {
		isns_esi_t	*esi = isns_list_item(isns_esi_t, esi_list, pos);

		if (esi->esi_object == obj)
			return esi;
	}
	return NULL;
}

/*
 * Update the ESI state after an entity has changed
 */
static int
isns_esi_update(isns_esi_t *esi)
{
	isns_object_t	*entity = esi->esi_object;
	ISNS_LIST_DECLARE(hold);
	isns_esi_portal_t *esp;
	unsigned int	i;

	isns_debug_esi("Updating ESI state for entity %u\n", entity->ie_index);

	isns_list_move(&hold, &esi->esi_portals);
	for (i = 0; i < entity->ie_children.iol_count; ++i) {
		isns_object_t	*child = entity->ie_children.iol_data[i];
		isns_portal_info_t esi_portal, portal_info;
		uint32_t	esi_interval;
		isns_list_t	*pos, *next;
		int		changed = 0;

		if (!ISNS_IS_PORTAL(child))
			continue;

		if (!isns_portal_from_object(&portal_info,
					ISNS_TAG_PORTAL_IP_ADDRESS,
					ISNS_TAG_PORTAL_TCP_UDP_PORT,
					child)
		 || !isns_portal_from_object(&esi_portal,
					ISNS_TAG_PORTAL_IP_ADDRESS,
					ISNS_TAG_ESI_PORT,
					child)
		 || !isns_object_get_uint32(child,
					ISNS_TAG_ESI_INTERVAL,
					&esi_interval))
			continue;

		isns_list_foreach(&hold, pos, next) {
			esp = isns_list_item(isns_esi_portal_t, esp_list, pos);

			if (esp->esp_object == child) {
				isns_debug_esi("Updating ESI state for %s\n",
						isns_portal_string(&portal_info));
				isns_list_del(&esp->esp_list);
				goto update;
			}
		}

		isns_debug_esi("Creating ESI state for %s\n",
				isns_portal_string(&portal_info));
		esp = isns_calloc(1, sizeof(*esp));
		esp->esp_object = isns_object_get(child);
		isns_list_init(&esp->esp_list);
		changed = 1;

update:
		if (!isns_portal_equal(&esp->esp_portal, &portal_info)) {
			esp->esp_portal = portal_info;
			changed++;
		}
		if (!isns_portal_equal(&esp->esp_dest, &esi_portal)) {
			isns_esi_disconnect(esp);
			esp->esp_dest = esi_portal;
			changed++;
		}
		if (esp->esp_interval != esi_interval) {
			esp->esp_interval = esi_interval;
			changed++;
		}

		isns_esi_restart(esp);

		isns_list_append(&esi->esi_portals, &esp->esp_list);
	}

	/* Destroy any old ESI portals */
	while (!isns_list_empty(&hold)) {
		esp = isns_list_item(isns_esi_portal_t, esp_list, hold.next);

		isns_esi_drop_portal(esp, NULL, 0);
	}

	/* If the client explicitly unregistered all ESI portals,
	 * stop monitoring it but *without* destroying the entity. */
	if (isns_list_empty(&esi->esi_portals)) {
		isns_esi_drop_entity(esi, NULL, 0);
		return 0;
	}

	return 1;
}

void
isns_esi_restart(isns_esi_portal_t *esp)
{
	unsigned int	timeo;

	isns_esi_disconnect(esp);

	esp->esp_start = time(NULL);
	esp->esp_retries = isns_config.ic_esi_retries;
	esp->esp_next_xmit = esp->esp_start + esp->esp_interval;
	esp->esp_xid = 0;

	timeo = esp->esp_interval / esp->esp_retries;
	if (timeo == 0)
		timeo = 1;
	else if (timeo > ESI_RETRANS_TIMEOUT)
		timeo = ESI_RETRANS_TIMEOUT;
	esp->esp_timeout = timeo;
}

void
isns_esi_disconnect(isns_esi_portal_t *esp)
{
	if (esp->esp_socket)
		isns_socket_free(esp->esp_socket);
	esp->esp_socket = NULL;
}

/*
 * Generic wrapper to dropping an object
 */
static inline void
__isns_esi_drop_object(isns_db_t *db, isns_object_t *obj, unsigned int dead)
{
	if (db && obj && obj->ie_state == ISNS_OBJECT_STATE_MATURE && dead)
		isns_db_remove(db, obj);
	isns_object_release(obj);
}

/*
 * Portal did not respond in time. Drop it
 */
void
isns_esi_drop_portal(isns_esi_portal_t *esp, isns_db_t *db, int dead)
{

	isns_debug_esi("ESI: dropping portal %s\n",
			isns_portal_string(&esp->esp_portal));

	isns_list_del(&esp->esp_list);
	isns_esi_disconnect(esp);
	__isns_esi_drop_object(db, esp->esp_object, dead);
	isns_free(esp);
}

/*
 * We ran out of ESI portals for this entity.
 */
void
isns_esi_drop_entity(isns_esi_t *esi, isns_db_t *db, int dead)
{
	isns_debug_esi("ESI: dropping entity %u\n",
			esi->esi_object->ie_index);

	isns_list_del(&esi->esi_list);
	__isns_esi_drop_object(db, esi->esi_object, dead);

	while (!isns_list_empty(&esi->esi_portals)) {
		isns_esi_portal_t *esp;

		esp = isns_list_item(isns_esi_portal_t, esp_list,
				esi->esi_portals.next);
		isns_esi_drop_portal(esp, db, dead);
	}
	isns_free(esi);
}

/*
 * When receiving an ESI response, find the portal we sent the
 * original message to.
 */
static isns_esi_portal_t *
isns_esi_get_msg_portal(uint32_t xid, isns_esi_t **esip)
{
	isns_list_t	*esi_pos, *esi_next;

	isns_list_foreach(&isns_esi_list, esi_pos, esi_next) {
		isns_esi_t	*esi = isns_list_item(isns_esi_t, esi_list, esi_pos);
		isns_list_t	*esp_pos, *esp_next;

		isns_list_foreach(&esi->esi_portals, esp_pos, esp_next) {
			isns_esi_portal_t *esp = isns_list_item(isns_esi_portal_t,
							esp_list, esp_pos);

			if (esp->esp_xid == xid) {
				*esip = esi;
				return esp;
			}
		}
	}

	return NULL;
}

/*
 * Handle incoming ESI request
 */
int
isns_process_esi(isns_server_t *srv, isns_simple_t *call, isns_simple_t **reply)
{
	const isns_attr_list_t *attrs = &call->is_message_attrs;
	isns_object_t	*portal = NULL;

	/* We just echo back the attributes sent to us by the server,
	 * without further checking. */
	*reply = isns_simple_create(ISNS_ENTITY_STATUS_INQUIRY,
				srv->is_source, attrs);

	/* Look up the portal and update its mtime.
	 * This can help the application find out if a portal has
	 * seen ESIs recently, and react.
	 */
	if (srv->is_db && attrs->ial_count == 4) {
		const isns_attr_t	*addr_attr, *port_attr;

		addr_attr = attrs->ial_data[2];
		port_attr = attrs->ial_data[3];
		if (addr_attr->ia_tag_id == ISNS_TAG_PORTAL_IP_ADDRESS
		 && port_attr->ia_tag_id == ISNS_TAG_PORTAL_TCP_UDP_PORT) {
			isns_attr_list_t key;

			key.ial_count = 2;
			key.ial_data = attrs->ial_data + 2;
			portal = isns_db_lookup(srv->is_db,
					&isns_portal_template,
					&key);
		}

		if (portal)
			portal->ie_mtime = time(NULL);
	}
	return ISNS_SUCCESS;
}

void
isns_process_esi_response(uint32_t xid,
		__attribute__((unused))int status,
		isns_simple_t *msg)
{
	isns_portal_info_t	portal_info;
	isns_esi_portal_t	*esp;
	isns_esi_t		*esi;

	if (msg == NULL) {
		isns_debug_esi("ESI call 0x%x timed out\n", xid);
		return;
	}

	/* FIXME: As a matter of security, we should probably
	 * verify that the ESI response originated from the
	 * portal we sent it to; or at least that it was authenticated
	 * by the client we think we're talking to. */

	/* Get the portal */
	if (!isns_portal_from_attr_list(&portal_info,
				ISNS_TAG_PORTAL_IP_ADDRESS,
				ISNS_TAG_PORTAL_TCP_UDP_PORT,
				&msg->is_message_attrs)) {
		isns_debug_esi("Ignoring unintelligible ESI response\n");
		return;
	}

	if (!(esp = isns_esi_get_msg_portal(xid, &esi))) {
		isns_debug_esi("Ignoring unmatched ESI reply\n");
		return;
	}

	if (!isns_portal_equal(&esp->esp_portal, &portal_info)) {
		isns_warning("Faked ESI response for portal %s\n",
				isns_portal_string(&portal_info));
		return;
	}

	isns_debug_esi("Good ESI response from %s\n",
			isns_portal_string(&portal_info));
	isns_esi_restart(esp);

	/* Refresh the entity's registration timestamp */
	isns_object_set_uint64(esi->esi_object,
			ISNS_TAG_TIMESTAMP,
			time(NULL));
	isns_db_sync(isns_esi_server->is_db);
}

/*
 * Helper function to schedule the next timeout
 */
static void
isns_esi_schedule(int timeout)
{
	isns_cancel_timer(isns_esi_transmit, NULL);
	isns_add_oneshot_timer(timeout, isns_esi_transmit, NULL);
}

/*
 * Register an entity for ESI monitoring.
 * This is called when reloading the database.
 */
void
isns_esi_register(isns_object_t *obj)
{
	if (!isns_esi_find(obj))
		isns_esi_add_entity(obj);
	/* We do not call esi_schedule(0) here; that happens in
	 * isns_esi_init already. */
}

/*
 * This callback is invoked whenever an object is added/removed/modified.
 * We use this to keep track of ESI portals and such.
 */
void
isns_esi_callback(const isns_db_event_t *ev,
		__attribute__((unused))void *ptr)
{
	isns_object_t	*obj, *entity;
	isns_esi_t	*esi;
	uint32_t	event;

	obj = ev->ie_object;
	event = ev->ie_bits;

	if (obj->ie_flags & ISNS_OBJECT_PRIVATE)
		return;

	isns_debug_esi("isns_esi_callback(%p, 0x%x)\n", obj, event);

	if (ISNS_IS_ENTITY(obj)
	 && (event & ISNS_SCN_OBJECT_ADDED_MASK)) {
		if (!isns_esi_find(obj))
			isns_esi_add_entity(obj);
		/* Schedule an immediate ESI timer run */
		isns_esi_schedule(0);
		return;
	}

	if (!(entity = isns_object_get_entity(obj)))
		return;

	esi = isns_esi_find(entity);
	if (esi != NULL)
		esi->esi_update = 1;

	/* Schedule an immediate ESI timer run */
	isns_esi_schedule(0);
}
