/*
 *   This program is 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.
 *
 *   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.
 *
 *   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 St, Fifth Floor, Boston, MA 02110-1301, USA
 */

/**
 * $Id: 65b42474d2f655b72b8321c0683dbf5ca62a284a $
 * @file rlm_cache_memcached.c
 * @brief memcached based cache.
 *
 * @copyright 2014 The FreeRADIUS server project
 */
#include <libmemcached/memcached.h>

#include <freeradius-devel/radiusd.h>
#include <freeradius-devel/rad_assert.h>

#include "../../rlm_cache.h"
#include "../../serialize.h"

typedef struct rlm_cache_memcached_handle {
	memcached_st *handle;
} rlm_cache_memcached_handle_t;

typedef struct rlm_cache_memcached {
	char const 		*options;	//!< Connection options
	fr_connection_pool_t	*pool;
} rlm_cache_memcached_t;

static const CONF_PARSER driver_config[] = {
	{ "options", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_cache_memcached_t, options), "--SERVER=localhost" },

	{NULL, -1, 0, NULL, NULL}
};

/** Free a connection handle
 *
 * @param mandle to free.
 */
static int _mod_conn_free(rlm_cache_memcached_handle_t *mandle)
{
	if (mandle->handle) memcached_free(mandle->handle);

	return 0;
}

/** Create a new memcached handle
 *
 * @param ctx to allocate handle in.
 * @param instance data.
 */
static void *mod_conn_create(TALLOC_CTX *ctx, void *instance)
{
	rlm_cache_t			*inst = instance;
	rlm_cache_memcached_t		*driver = inst->driver;
	rlm_cache_memcached_handle_t	*mandle;

	memcached_st			*sandle;
	memcached_return_t		ret;

	sandle = memcached(driver->options, talloc_array_length(driver->options) -1);
	if (!sandle) {
		ERROR("rlm_cache_memcached: Failed creating memcached connection");

		return NULL;
	}

	ret = memcached_version(sandle);
	if (ret != MEMCACHED_SUCCESS) {
		ERROR("rlm_cache_memcached: Failed getting server info: %s: %s", memcached_strerror(sandle, ret),
		      memcached_last_error_message(sandle));
		memcached_free(sandle);
		return NULL;
	}

	mandle = talloc_zero(ctx, rlm_cache_memcached_handle_t);
	mandle->handle = sandle;
	talloc_set_destructor(mandle, _mod_conn_free);

	return mandle;
}

/** Cleanup a rlm_cache_memcached instance
 *
 * @param driver to free.
 * @return 0
 */
static int _mod_detach(rlm_cache_memcached_t *driver)
{
	fr_connection_pool_free(driver->pool);
	return 0;
}

/** Create a new rlm_cache_memcached instance
 *
 * @param conf memcached specific conf section.
 * @param inst main rlm_cache instance.
 * @return 0 on success, -1 on failure.
 */
static int mod_instantiate(CONF_SECTION *conf, rlm_cache_t *inst)
{
	rlm_cache_memcached_t *driver;
	memcached_return_t ret;

	char buffer[256];

	static bool version_done;

	buffer[0] = '\0';

	/*
	 *	Get version info from the libmemcached API.
	 */
	if (!version_done) {
		version_done = true;

		INFO("rlm_cache_memcached: libmemcached version: %s", memcached_lib_version());
	}

	driver = talloc_zero(inst, rlm_cache_memcached_t);
	talloc_set_destructor(driver, _mod_detach);

	if (cf_section_parse(conf, driver, driver_config) < 0) return -1;

	ret = libmemcached_check_configuration(driver->options, talloc_array_length(driver->options) -1,
					       buffer, sizeof(buffer));
	if (ret != MEMCACHED_SUCCESS) {
		ERROR("rlm_cache_memcached: Failed validating options string: %s", buffer);
		return -1;
	}

	inst->driver = driver;

	snprintf(buffer, sizeof(buffer), "rlm_cache (%s)", inst->name);

	driver->pool = fr_connection_pool_module_init(conf, inst, mod_conn_create, NULL, buffer);
	if (!driver->pool) return -1;

	if (inst->max_entries > 0) WARN("rlm_cache_memcached: max_entries is not supported by this driver");

	return 0;
}

static void cache_entry_free(rlm_cache_entry_t *c)
{
	talloc_free(c);
}

/** Locate a cache entry in memcached
 *
 * @param out Where to write the pointer to the cach entry.
 * @param inst main rlm_cache instance.
 * @param request The current request.
 * @param handle Pointer to memcached handle.
 * @param key to search for.
 * @return CACHE_OK on success CACHE_MISS if no entry found, CACHE_ERROR on error.
 */
static cache_status_t cache_entry_find(rlm_cache_entry_t **out, UNUSED rlm_cache_t *inst, REQUEST *request,
				       rlm_cache_handle_t **handle, char const *key)
{
	rlm_cache_memcached_handle_t *mandle = *handle;

	memcached_return_t mret;
	size_t len;
	int ret;
	uint32_t flags;

	char *from_store;

	rlm_cache_entry_t *c;

	from_store = memcached_get(mandle->handle, key, strlen(key), &len, &flags, &mret);
	if (!from_store) {
		if (mret == MEMCACHED_NOTFOUND) return CACHE_MISS;

		RERROR("Failed retrieving entry for key \"%s\": %s: %s", key, memcached_strerror(mandle->handle, mret),
		       memcached_last_error_message(mandle->handle));

		return CACHE_ERROR;
	}
	RDEBUG2("Retrieved %zu bytes from memcached", len);
	RDEBUG2("%s", from_store);

	c = talloc_zero(NULL,  rlm_cache_entry_t);
	ret = cache_deserialize(c, from_store, len);
	free(from_store);
	if (ret < 0) {
		RERROR("%s", fr_strerror());
		talloc_free(c);
		return CACHE_ERROR;
	}
	c->key = talloc_strdup(c, key);
	*out = c;

	return CACHE_OK;
}

/** Insert a new entry into the data store
 *
 * @param inst main rlm_cache instance.
 * @param request The current request.
 * @param handle Pointer to memcached handle.
 * @param c entry to insert.
 * @return CACHE_OK on success else CACHE_ERROR on error.
 */
static cache_status_t cache_entry_insert(UNUSED rlm_cache_t *inst, REQUEST *request, rlm_cache_handle_t **handle,
					 rlm_cache_entry_t *c)
{
	rlm_cache_memcached_handle_t *mandle = *handle;

	memcached_return_t ret;

	TALLOC_CTX *pool;
	char *to_store;

	pool = talloc_pool(NULL, 1024);
	if (!pool) return CACHE_ERROR;

	if (cache_serialize(pool, &to_store, c) < 0) {
		talloc_free(pool);

		return CACHE_ERROR;
	}

	ret = memcached_set(mandle->handle, c->key, talloc_array_length(c->key) - 1,
		            to_store ? to_store : "",
		            to_store ? talloc_array_length(to_store) - 1 : 0, c->expires, 0);
	talloc_free(pool);
	if (ret != MEMCACHED_SUCCESS) {
		RERROR("Failed storing entry with key \"%s\": %s: %s", c->key,
		       memcached_strerror(mandle->handle, ret),
		       memcached_last_error_message(mandle->handle));

		return CACHE_ERROR;
	}

	return CACHE_OK;
}

/** Call delete the cache entry from memcached
 *
 * @param inst main rlm_cache instance.
 * @param request The current request.
 * @param handle Pointer to memcached handle.
 * @param c entry to expire.
 * @return CACHE_OK on success else CACHE_ERROR.
 */
static cache_status_t cache_entry_expire(UNUSED rlm_cache_t *inst, REQUEST *request, rlm_cache_handle_t **handle,
					 rlm_cache_entry_t *c)
{
	rlm_cache_memcached_handle_t *mandle = *handle;

	memcached_return_t ret;

	ret = memcached_delete(mandle->handle, c->key, talloc_array_length(c->key) - 1, 0);
	if (ret != MEMCACHED_SUCCESS) {
		RERROR("Failed deleting entry with key \"%s\": %s", c->key,
		       memcached_last_error_message(mandle->handle));

		return CACHE_ERROR;
	}

	return CACHE_OK;
}

/** Get a memcached handle
 *
 * @param out Where to write the handle.
 * @param inst rlm_cache instance.
 * @param request The current request.
 */
static int mod_conn_get(rlm_cache_handle_t **out, rlm_cache_t *inst, UNUSED REQUEST *request)
{
	rlm_cache_memcached_t *driver = inst->driver;
	rlm_cache_handle_t *mandle;

	*out = NULL;

	mandle = fr_connection_get(driver->pool);
	if (!mandle) {
		*out = NULL;
		return -1;
	}
	*out = mandle;

	return 0;
}

/** Release a socket
 *
 * @param inst main rlm_cache instance.
 * @param request The current request.
 * @param handle Pointer to the handle to release (will be set to NULL).
 */
static void mod_conn_release(rlm_cache_t *inst, UNUSED REQUEST *request, rlm_cache_handle_t **handle)
{
	rlm_cache_memcached_t *driver = inst->driver;

	fr_connection_release(driver->pool, *handle);
	*handle = NULL;
}

/** Reconnect a socket
 *
 * @param inst main rlm_cache instance.
 * @param request The current request.
 * @param handle Pointer to the handle to reconnect (will be set to NULL if reconnection fails).
 */
static int mod_conn_reconnect(rlm_cache_t *inst, UNUSED REQUEST *request, rlm_cache_handle_t **handle)
{
	rlm_cache_memcached_t *driver = inst->driver;
	rlm_cache_handle_t *mandle;

	mandle = fr_connection_reconnect(driver->pool, *handle);
	if (!mandle) {
		*handle = NULL;
		return -1;
	}
	*handle = mandle;

	return 0;
}

extern cache_module_t rlm_cache_memcached;
cache_module_t rlm_cache_memcached = {
	.name		= "rlm_cache_memcached",
	.instantiate	= mod_instantiate,
	.free		= cache_entry_free,

	.find		= cache_entry_find,
	.insert		= cache_entry_insert,
	.expire		= cache_entry_expire,

	.acquire	= mod_conn_get,
	.release	= mod_conn_release,
	.reconnect	= mod_conn_reconnect
};
