/*
 * Copyright (C) 2014-2016 Tobias Brunner
 * HSR Hochschule fuer Technik Rapperswil
 *
 * Copyright (C) 2014 Martin Willi
 * Copyright (C) 2014 revosec AG
 *
 * This program 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.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program 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.
 */

#include "vici_attribute.h"
#include "vici_builder.h"

#include <daemon.h>
#include <collections/hashtable.h>
#include <collections/array.h>
#include <threading/rwlock.h>
#include <attributes/mem_pool.h>

typedef struct private_vici_attribute_t private_vici_attribute_t;

/**
 * private data of vici_attribute
 */
struct private_vici_attribute_t {

	/**
	 * public functions
	 */
	vici_attribute_t public;

	/**
	 * vici connection dispatcher
	 */
	vici_dispatcher_t *dispatcher;

	/**
	 * Configured pools, as char* => pool_t
	 */
	hashtable_t *pools;

	/**
	 * rwlock to lock access to pools
	 */
	rwlock_t *lock;
};

/**
 * Single configuration attribute with type
 */
typedef struct {
	/** type of attribute */
	configuration_attribute_type_t type;
	/** attribute value */
	chunk_t value;
} attribute_t;

/**
 * Clean up an attribute
 */
static void attribute_destroy(attribute_t *attr)
{
	free(attr->value.ptr);
	free(attr);
}

/**
 * Pool instances with associated attributes
 */
typedef struct {
	/** in-memory virtual IP pool */
	mem_pool_t *vips;
	/** configuration attributes, as attribute_t */
	array_t *attrs;
} pool_t;

/**
 * Clean up a pool instance
 */
static void pool_destroy(pool_t *pool)
{
	DESTROY_IF(pool->vips);
	array_destroy_function(pool->attrs, (void*)attribute_destroy, NULL);
	free(pool);
}

/**
 * Find an existing or not yet existing lease
 */
static host_t *find_addr(private_vici_attribute_t *this, linked_list_t *pools,
						 identification_t *id, host_t *requested,
						 mem_pool_op_t op, host_t *peer)
{
	enumerator_t *enumerator;
	host_t *addr = NULL;
	pool_t *pool;
	char *name;

	enumerator = pools->create_enumerator(pools);
	while (enumerator->enumerate(enumerator, &name))
	{
		pool = this->pools->get(this->pools, name);
		if (pool)
		{
			addr = pool->vips->acquire_address(pool->vips, id, requested,
											   op, peer);
			if (addr)
			{
				break;
			}
		}
	}
	enumerator->destroy(enumerator);

	return addr;
}

METHOD(attribute_provider_t, acquire_address, host_t*,
	private_vici_attribute_t *this, linked_list_t *pools, ike_sa_t *ike_sa,
	host_t *requested)
{
	identification_t *id;
	host_t *addr, *peer;

	id = ike_sa->get_other_eap_id(ike_sa);
	peer = ike_sa->get_other_host(ike_sa);

	this->lock->read_lock(this->lock);

	addr = find_addr(this, pools, id, requested, MEM_POOL_EXISTING, peer);
	if (!addr)
	{
		addr = find_addr(this, pools, id, requested, MEM_POOL_NEW, peer);
		if (!addr)
		{
			addr = find_addr(this, pools, id, requested, MEM_POOL_REASSIGN, peer);
		}
	}

	this->lock->unlock(this->lock);

	return addr;
}

METHOD(attribute_provider_t, release_address, bool,
	private_vici_attribute_t *this, linked_list_t *pools, host_t *address,
	ike_sa_t *ike_sa)
{
	enumerator_t *enumerator;
	identification_t *id;
	bool found = FALSE;
	pool_t *pool;
	char *name;

	id = ike_sa->get_other_eap_id(ike_sa);

	this->lock->read_lock(this->lock);

	enumerator = pools->create_enumerator(pools);
	while (enumerator->enumerate(enumerator, &name))
	{
		pool = this->pools->get(this->pools, name);
		if (pool)
		{
			found = pool->vips->release_address(pool->vips, address, id);
			if (found)
			{
				break;
			}
		}
	}
	enumerator->destroy(enumerator);

	this->lock->unlock(this->lock);

	return found;
}

CALLBACK(attr_filter, bool,
	void *data, enumerator_t *orig, va_list args)
{
	attribute_t *attr;
	configuration_attribute_type_t *type;
	chunk_t *value;

	VA_ARGS_VGET(args, type, value);

	if (orig->enumerate(orig, &attr))
	{
		*type = attr->type;
		*value = attr->value;
		return TRUE;
	}
	return FALSE;
}

/**
 * Create nested inner enumerator over pool attributes
 */
CALLBACK(create_nested, enumerator_t*,
	pool_t *pool, void *this)
{
	return enumerator_create_filter(array_create_enumerator(pool->attrs),
									attr_filter, NULL, NULL);
}

/**
 * Data associated to nested enumerator cleanup
 */
typedef struct {
	private_vici_attribute_t *this;
	linked_list_t *list;
} nested_data_t;

/**
 * Clean up nested enumerator data
 */
CALLBACK(nested_cleanup, void,
	nested_data_t *data)
{
	data->this->lock->unlock(data->this->lock);
	data->list->destroy(data->list);
	free(data);
}

/**
 * Check if any of vips is from pool
 */
static bool have_vips_from_pool(mem_pool_t *pool, linked_list_t *vips)
{
	enumerator_t *enumerator;
	host_t *host;
	chunk_t start, end, current;
	uint32_t size;
	bool found = FALSE;

	host = pool->get_base(pool);
	start = host->get_address(host);

	if (start.len >= sizeof(size))
	{
		end = chunk_clone(start);

		/* mem_pool is currenty limited to 2^31 addresses, so 32-bit
		 * calculations should be sufficient. */
		size = untoh32(start.ptr + start.len - sizeof(size));
		htoun32(end.ptr + end.len - sizeof(size), size + pool->get_size(pool));

		enumerator = vips->create_enumerator(vips);
		while (enumerator->enumerate(enumerator, &host))
		{
			current = host->get_address(host);
			if (chunk_compare(current, start) >= 0 &&
				chunk_compare(current, end) < 0)
			{
				found = TRUE;
				break;
			}
		}
		enumerator->destroy(enumerator);

		free(end.ptr);
	}
	return found;
}

METHOD(attribute_provider_t, create_attribute_enumerator, enumerator_t*,
	private_vici_attribute_t *this, linked_list_t *pools,
	ike_sa_t *ike_sa, linked_list_t *vips)
{
	enumerator_t *enumerator;
	nested_data_t *data;
	pool_t *pool;
	char *name;

	INIT(data,
		.this = this,
		.list = linked_list_create(),
	);

	this->lock->read_lock(this->lock);

	enumerator = pools->create_enumerator(pools);
	while (enumerator->enumerate(enumerator, &name))
	{
		pool = this->pools->get(this->pools, name);
		if (pool && have_vips_from_pool(pool->vips, vips))
		{
			data->list->insert_last(data->list, pool);
		}
	}
	enumerator->destroy(enumerator);

	return enumerator_create_nested(data->list->create_enumerator(data->list),
									create_nested, data, nested_cleanup);
}

/**
 * Merge a pool configuration with existing ones
 */
static bool merge_pool(private_vici_attribute_t *this, pool_t *new)
{
	mem_pool_t *tmp;
	host_t *base;
	pool_t *old;
	const char *name;
	u_int size;

	name = new->vips->get_name(new->vips);
	base = new->vips->get_base(new->vips);
	size = new->vips->get_size(new->vips);

	old = this->pools->remove(this->pools, name);
	if (!old)
	{
		this->pools->put(this->pools, name, new);
		DBG1(DBG_CFG, "added vici pool %s: %H, %u entries", name, base, size);
		return TRUE;
	}

	if (base->ip_equals(base, old->vips->get_base(old->vips)) &&
		size == old->vips->get_size(old->vips))
	{
		/* no changes in pool, so keep existing, but use new attributes */
		DBG1(DBG_CFG, "updated vici pool %s: %H, %u entries", name, base, size);
		tmp = new->vips;
		new->vips = old->vips;
		old->vips = tmp;
		this->pools->put(this->pools, new->vips->get_name(new->vips), new);
		pool_destroy(old);
		return TRUE;
	}
	if (old->vips->get_online(old->vips) == 0)
	{
		/* can replace old pool, no online leases */
		DBG1(DBG_CFG, "replaced vici pool %s: %H, %u entries", name, base, size);
		this->pools->put(this->pools, name, new);
		pool_destroy(old);
		return TRUE;
	}
	/* have online leases, unable to replace, TODO: migrate leases? */
	DBG1(DBG_CFG, "vici pool %s has %u online leases, unable to replace",
		 name, old->vips->get_online(old->vips));
	this->pools->put(this->pools, old->vips->get_name(old->vips), old);
	return FALSE;
}

/**
 * Create a (error) reply message
 */
static vici_message_t* create_reply(char *fmt, ...)
{
	vici_builder_t *builder;
	va_list args;

	builder = vici_builder_create();
	builder->add_kv(builder, "success", fmt ? "no" : "yes");
	if (fmt)
	{
		va_start(args, fmt);
		builder->vadd_kv(builder, "errmsg", fmt, args);
		va_end(args);
	}
	return builder->finalize(builder);
}

/**
 * Parse a range definition of an address pool
 */
static mem_pool_t *create_pool_range(char *name, char *buf)
{
	mem_pool_t *pool;
	host_t *from, *to;

	if (!host_create_from_range(buf, &from, &to))
	{
		return NULL;
	}
	pool = mem_pool_create_range(name, from, to);
	from->destroy(from);
	to->destroy(to);
	return pool;
}

/**
 * Parse callback data, passed to each callback
 */
typedef struct {
	private_vici_attribute_t *this;
	vici_message_t *reply;
} request_data_t;

/**
 * Data associated to a pool load
 */
typedef struct {
	request_data_t *request;
	char *name;
	pool_t *pool;
} load_data_t;

CALLBACK(pool_li, bool,
	load_data_t *data, vici_message_t *message, char *name, chunk_t value)
{
	struct {
		char *name;
		configuration_attribute_type_t v4;
		configuration_attribute_type_t v6;
	} keys[] = {
		{"address",			INTERNAL_IP4_ADDRESS,	INTERNAL_IP6_ADDRESS	},
		{"dns",				INTERNAL_IP4_DNS,		INTERNAL_IP6_DNS		},
		{"nbns",			INTERNAL_IP4_NBNS,		INTERNAL_IP6_NBNS		},
		{"dhcp",			INTERNAL_IP4_DHCP,		INTERNAL_IP6_DHCP		},
		{"netmask",			INTERNAL_IP4_NETMASK,	INTERNAL_IP6_NETMASK	},
		{"server",			INTERNAL_IP4_SERVER,	INTERNAL_IP6_SERVER		},
		{"subnet",			INTERNAL_IP4_SUBNET,	INTERNAL_IP6_SUBNET		},
		{"split_include",	UNITY_SPLIT_INCLUDE,	UNITY_SPLIT_INCLUDE		},
		{"split_exclude",	UNITY_LOCAL_LAN,		UNITY_LOCAL_LAN			},
	};
	char buf[256];
	int i, index = -1, mask = -1, type = 0;
	chunk_t encoding;
	attribute_t *attr;
	host_t *host = NULL;

	for (i = 0; i < countof(keys); i++)
	{
		if (streq(name, keys[i].name))
		{
			index = i;
			break;
		}
	}
	if (index == -1)
	{
		type = atoi(name);
		if (!type)
		{
			data->request->reply = create_reply("invalid attribute: %s", name);
			return FALSE;
		}
	}

	if (vici_stringify(value, buf, sizeof(buf)))
	{
		if (strchr(buf, '/'))
		{
			host = host_create_from_subnet(buf, &mask);
		}
		else
		{
			host = host_create_from_string(buf, 0);
		}
	}
	if (host)
	{
		if (index != -1)
		{
			switch (host->get_family(host))
			{
				case AF_INET:
					type = keys[index].v4;
					break;
				case AF_INET6:
				default:
					type = keys[index].v6;
					break;
			}
		}
		if (mask == -1)
		{
			encoding = chunk_clone(host->get_address(host));
		}
		else
		{
			if (host->get_family(host) == AF_INET)
			{	/* IPv4 attributes contain a subnet mask */
				uint32_t netmask = 0;

				if (mask)
				{	/* shifting uint32_t by 32 or more is undefined */
					mask = 32 - mask;
					netmask = htonl((0xFFFFFFFF >> mask) << mask);
				}
				encoding = chunk_cat("cc", host->get_address(host),
									 chunk_from_thing(netmask));
			}
			else
			{	/* IPv6 addresses the prefix only */
				encoding = chunk_cat("cc", host->get_address(host),
									 chunk_from_chars(mask));
			}
		}
		host->destroy(host);
	}
	else
	{
		if (index != -1)
		{
			data->request->reply = create_reply("invalid attribute value "
												"for %s", name);
			return FALSE;
		}
		/* use raw binary data for numbered attributes */
		encoding = chunk_clone(value);
	}
	INIT(attr,
		.type = type,
		.value = encoding,
	);
	array_insert_create(&data->pool->attrs, ARRAY_TAIL, attr);
	return TRUE;
}

CALLBACK(pool_kv, bool,
	load_data_t *data, vici_message_t *message, char *name, chunk_t value)
{
	if (streq(name, "addrs"))
	{
		char buf[128];
		mem_pool_t *pool;
		host_t *base = NULL;
		int bits;

		if (data->pool->vips)
		{
			data->request->reply = create_reply("multiple addrs defined");
			return FALSE;
		}
		if (!vici_stringify(value, buf, sizeof(buf)))
		{
			data->request->reply = create_reply("invalid addrs value");
			return FALSE;
		}
		pool = create_pool_range(data->name, buf);
		if (!pool)
		{
			base = host_create_from_subnet(buf, &bits);
			if (base)
			{
				pool = mem_pool_create(data->name, base, bits);
				base->destroy(base);
			}
		}
		if (!pool)
		{
			data->request->reply = create_reply("invalid addrs value: %s", buf);
			return FALSE;
		}
		data->pool->vips = pool;
		return TRUE;
	}
	data->request->reply = create_reply("invalid attribute: %s", name);
	return FALSE;
}

CALLBACK(pool_sn, bool,
	request_data_t *request, vici_message_t *message,
	vici_parse_context_t *ctx, char *name)
{
	load_data_t data = {
		.request = request,
		.name = name,
	};
	bool merged;

	INIT(data.pool);

	if (!message->parse(message, ctx, NULL, pool_kv, pool_li, &data))
	{
		pool_destroy(data.pool);
		return FALSE;
	}

	if (!data.pool->vips)
	{
		request->reply = create_reply("missing addrs for pool '%s'", name);
		pool_destroy(data.pool);
		return FALSE;
	}

	request->this->lock->write_lock(request->this->lock);
	merged = merge_pool(request->this, data.pool);
	request->this->lock->unlock(request->this->lock);

	if (!merged)
	{
		request->reply = create_reply("vici pool %s has online leases, "
									  "unable to replace", name);
		pool_destroy(data.pool);
	}
	return merged;
}

CALLBACK(load_pool, vici_message_t*,
	private_vici_attribute_t *this, char *name, u_int id,
	vici_message_t *message)
{
	request_data_t request = {
		.this = this,
	};

	if (!message->parse(message, NULL, pool_sn, NULL, NULL, &request))
	{
		if (request.reply)
		{
			return request.reply;
		}
		return create_reply("parsing request failed");
	}
	return create_reply(NULL);
}

CALLBACK(unload_pool, vici_message_t*,
	private_vici_attribute_t *this, char *name, u_int id,
	vici_message_t *message)
{
	vici_message_t *reply;
	u_int online;
	pool_t *pool;

	name = message->get_str(message, NULL, "name");
	if (!name)
	{
		return create_reply("missing pool name to unload");
	}

	this->lock->write_lock(this->lock);

	pool = this->pools->remove(this->pools, name);
	if (pool)
	{
		online = pool->vips->get_online(pool->vips);
		if (online)
		{
			DBG1(DBG_CFG, "vici pool %s has %u online leases, unable to unload",
				 name, online);
			reply = create_reply("%s has online leases, unable to unload", name);
			this->pools->put(this->pools, pool->vips->get_name(pool->vips), pool);
		}
		else
		{
			DBG1(DBG_CFG, "unloaded vici pool %s", name);
			reply = create_reply(NULL);
			pool_destroy(pool);
		}
	}
	else
	{
		reply = create_reply("%s not found", name);
	}

	this->lock->unlock(this->lock);

	return reply;
}

CALLBACK(get_pools, vici_message_t*,
	private_vici_attribute_t *this, char *name, u_int id,
	vici_message_t *message)
{
	vici_builder_t *builder;
	enumerator_t *enumerator, *leases;
	mem_pool_t *vips;
	pool_t *pool;
	identification_t *uid;
	host_t *lease;
	bool list_leases, on;
	char buf[32], *filter;
	int i;

	list_leases = message->get_bool(message, FALSE, "leases");
	filter = message->get_str(message, NULL, "name");

	builder = vici_builder_create();

	this->lock->read_lock(this->lock);
	enumerator = this->pools->create_enumerator(this->pools);
	while (enumerator->enumerate(enumerator, &name, &pool))
	{
		if (filter && !streq(name, filter))
		{
			continue;
		}

		vips = pool->vips;

		builder->begin_section(builder, name);

		builder->add_kv(builder, "base", "%H", vips->get_base(vips));
		builder->add_kv(builder, "size", "%u", vips->get_size(vips));
		builder->add_kv(builder, "online", "%u", vips->get_online(vips));
		builder->add_kv(builder, "offline", "%u", vips->get_offline(vips));

		if (list_leases)
		{
			i = 0;
			builder->begin_section(builder, "leases");
			leases = vips->create_lease_enumerator(vips);
			while (leases && leases->enumerate(leases, &uid, &lease, &on))
			{
				snprintf(buf, sizeof(buf), "%d", i++);
				builder->begin_section(builder, buf);
				builder->add_kv(builder, "address", "%H", lease);
				builder->add_kv(builder, "identity", "%Y", uid);
				builder->add_kv(builder, "status", on ? "online" : "offline");
				builder->end_section(builder);
			}
			leases->destroy(leases);
			builder->end_section(builder);
		}
		builder->end_section(builder);
	}
	enumerator->destroy(enumerator);
	this->lock->unlock(this->lock);

	return builder->finalize(builder);
}

static void manage_command(private_vici_attribute_t *this,
						   char *name, vici_command_cb_t cb, bool reg)
{
	this->dispatcher->manage_command(this->dispatcher, name,
									 reg ? cb : NULL, this);
}

/**
 * (Un-)register dispatcher functions
 */
static void manage_commands(private_vici_attribute_t *this, bool reg)
{
	manage_command(this, "load-pool", load_pool, reg);
	manage_command(this, "unload-pool", unload_pool, reg);
	manage_command(this, "get-pools", get_pools, reg);
}

METHOD(vici_attribute_t, destroy, void,
	private_vici_attribute_t *this)
{
	enumerator_t *enumerator;
	pool_t *pool;

	manage_commands(this, FALSE);

	enumerator = this->pools->create_enumerator(this->pools);
	while (enumerator->enumerate(enumerator, NULL, &pool))
	{
		pool_destroy(pool);
	}
	enumerator->destroy(enumerator);
	this->pools->destroy(this->pools);
	this->lock->destroy(this->lock);
	free(this);
}

/*
 * see header file
 */
vici_attribute_t *vici_attribute_create(vici_dispatcher_t *dispatcher)
{
	private_vici_attribute_t *this;

	INIT(this,
		.public = {
			.provider = {
				.acquire_address = _acquire_address,
				.release_address = _release_address,
				.create_attribute_enumerator = _create_attribute_enumerator,
			},
			.destroy = _destroy,
		},
		.dispatcher = dispatcher,
		.pools = hashtable_create(hashtable_hash_str, hashtable_equals_str, 4),
		.lock = rwlock_create(RWLOCK_TYPE_DEFAULT),
	);

	manage_commands(this, TRUE);

	return &this->public;
}
