/*
 *
 *  Wireless daemon for Linux
 *
 *  Copyright (C) 2015  Intel Corporation. All rights reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>

#include <ell/ell.h>
#include "src/dbus.h"
#include "src/agent.h"

static unsigned int next_request_id = 0;

enum agent_request_type {
	AGENT_REQUEST_TYPE_PASSPHRASE,
	AGENT_REQUEST_TYPE_USER_NAME_PASSWD,
};

/* Agent dbus request is done from iwd towards the agent */
struct agent_request {
	enum agent_request_type type;
	struct l_dbus_message *message;
	unsigned int id;
	void *user_data;
	void *user_callback;
	struct l_dbus_message *trigger;
	agent_request_destroy_func_t destroy;
};

struct agent {
	char *owner;
	char *path;
	unsigned int disconnect_watch;
	uint32_t pending_id;
	struct l_timeout *timeout;
	int timeout_secs;
	struct l_queue *requests;
};

static struct l_queue *agents;

/*
 * How long we wait for user to input things.
 * Return value is in seconds.
 *
 * This should probably be configurable by user via
 * config file/command line option/env variable.
 */
static unsigned int agent_timeout_input_request(void)
{
	return 120;
}

static void send_request(struct agent *agent, const char *request)
{
	struct l_dbus_message *message;

	l_debug("send %s request to %s %s", request, agent->owner,
							agent->path);

	message = l_dbus_message_new_method_call(dbus_get_bus(),
							agent->owner,
							agent->path,
							IWD_AGENT_INTERFACE,
							request);

	l_dbus_message_set_arguments(message, "");

	l_dbus_send(dbus_get_bus(), message);
}

static void send_cancel_request(void *user_data, int reason)
{
	struct agent *agent = user_data;
	struct l_dbus_message *message;
	const char *reasonstr;

	switch (reason) {
	case -ECANCELED:
		reasonstr = "user-canceled";
		break;
	case -ETIMEDOUT:
		reasonstr = "timed-out";
		break;
	case -ERANGE:
		reasonstr = "out-of-range";
		break;
	case -ESHUTDOWN:
		reasonstr = "shutdown";
		break;
	default:
		reasonstr = "unknown";
	}

	l_debug("send a Cancel(%s) to %s %s", reasonstr,
			agent->owner, agent->path);

	message = l_dbus_message_new_method_call(dbus_get_bus(),
							agent->owner,
							agent->path,
							IWD_AGENT_INTERFACE,
							"Cancel");

	l_dbus_message_set_arguments(message, "s", reasonstr);

	l_dbus_send(dbus_get_bus(), message);
}

static void agent_request_free(void *user_data)
{
	struct agent_request *request = user_data;

	l_dbus_message_unref(request->message);

	if (request->trigger)
		dbus_pending_reply(&request->trigger,
					dbus_error_aborted(request->trigger));

	if (request->destroy)
		request->destroy(request->user_data);

	l_free(request);
}

static void passphrase_reply(struct l_dbus_message *reply,
					struct agent_request *request)
{
	const char *error, *text;
	char *passphrase = NULL;
	enum agent_result result = AGENT_RESULT_FAILED;
	agent_request_passphrase_func_t user_callback = request->user_callback;

	if (l_dbus_message_get_error(reply, &error, &text))
		goto done;

	if (!l_dbus_message_get_arguments(reply, "s", &passphrase))
		goto done;

	result = AGENT_RESULT_OK;

done:
	user_callback(result, passphrase, request->trigger, request->user_data);
}

static void user_name_passwd_reply(struct l_dbus_message *reply,
					struct agent_request *request)
{
	const char *error, *text;
	char *username = NULL;
	char *passwd = NULL;
	enum agent_result result = AGENT_RESULT_FAILED;
	agent_request_user_name_passwd_func_t user_callback =
		request->user_callback;

	if (l_dbus_message_get_error(reply, &error, &text))
		goto done;

	if (!l_dbus_message_get_arguments(reply, "ss", &username, &passwd))
		goto done;

	result = AGENT_RESULT_OK;

done:
	user_callback(result, username, passwd,
			request->trigger, request->user_data);
}

static void agent_finalize_pending(struct agent *agent,
						struct l_dbus_message *reply)
{
	struct agent_request *pending;

	if (agent->timeout) {
		l_timeout_remove(agent->timeout);
		agent->timeout = NULL;
	}

	pending = l_queue_pop_head(agent->requests);

	switch (pending->type) {
	case AGENT_REQUEST_TYPE_PASSPHRASE:
		passphrase_reply(reply, pending);
		break;
	case AGENT_REQUEST_TYPE_USER_NAME_PASSWD:
		user_name_passwd_reply(reply, pending);
		break;
	}

	if (pending->trigger) {
		l_dbus_message_unref(pending->trigger);
		pending->trigger = NULL;
	}

	agent_request_free(pending);
}

static void agent_free(void *data)
{
	struct agent *agent = data;

	l_debug("agent free %p", agent);

	if (agent->timeout)
		l_timeout_remove(agent->timeout);

	if (agent->pending_id)
		l_dbus_cancel(dbus_get_bus(), agent->pending_id);

	l_queue_destroy(agent->requests, agent_request_free);

	if (agent->disconnect_watch)
		l_dbus_remove_watch(dbus_get_bus(), agent->disconnect_watch);

	l_free(agent->owner);
	l_free(agent->path);
	l_free(agent);
}

static void agent_send_next_request(struct agent *agent);

static void request_timeout(struct l_timeout *timeout, void *user_data)
{
	struct agent *agent = user_data;

	l_dbus_cancel(dbus_get_bus(), agent->pending_id);

	send_cancel_request(agent, -ETIMEDOUT);

	agent_finalize_pending(agent, NULL);

	agent_send_next_request(agent);
}

static void agent_receive_reply(struct l_dbus_message *message,
							void *user_data)
{
	struct agent *agent = user_data;

	l_debug("agent %p request id %u", agent, agent->pending_id);

	agent->pending_id = 0;

	agent_finalize_pending(agent, message);

	if (!agent->pending_id)
		agent_send_next_request(agent);
}

static void agent_send_next_request(struct agent *agent)
{
	struct agent_request *pending;

	pending = l_queue_peek_head(agent->requests);
	if (!pending)
		return;

	agent->timeout = l_timeout_create(agent->timeout_secs,
						request_timeout,
						agent, NULL);

	l_debug("send request to %s %s", agent->owner, agent->path);

	agent->pending_id = l_dbus_send_with_reply(dbus_get_bus(),
							pending->message,
							agent_receive_reply,
							agent, NULL);

	pending->message = NULL;

	return;
}

static unsigned int agent_queue_request(struct agent *agent,
					enum agent_request_type type,
					struct l_dbus_message *message,
					int timeout, void *callback,
					struct l_dbus_message *trigger,
					void *user_data,
					agent_request_destroy_func_t destroy)
{
	struct agent_request *request;

	request = l_new(struct agent_request, 1);

	request->type = type;
	request->message = message;
	request->id = ++next_request_id;
	request->user_data = user_data;
	request->user_callback = callback;
	request->trigger = l_dbus_message_ref(trigger);
	request->destroy = destroy;

	agent->timeout_secs = timeout;

	l_queue_push_tail(agent->requests, request);

	if (l_queue_length(agent->requests) == 1)
		agent_send_next_request(agent);

	return request->id;
}

static struct agent *agent_lookup(const char *owner)
{
	const struct l_queue_entry *entry;

	if (!owner)
		return NULL;

	for (entry = l_queue_get_entries(agents); entry; entry = entry->next) {
		struct agent *agent = entry->data;

		if (strcmp(agent->owner, owner))
			continue;

		return agent;
	}

	return NULL;
}

static struct agent *get_agent(const char *owner)
{
	struct agent *agent = agent_lookup(owner);

	if (agent)
		return agent;

	return l_queue_peek_head(agents);
}

/**
 * agent_request_passphrase:
 * @path: object path related to this request (like network object path)
 * @callback: user callback called when the request is ready
 * @trigger: Message associated with (e.g. that triggered) this request
 * @user_data: user defined data
 * @destroy: callback to release @user_data when this request finishes
 *
 * Called when a passphrase information is needed from the user. Returns an
 * id that can be used to cancel the request.
 *
 * If @trigger is not NULL, then a reference is taken automatically.  If
 * agent_cancel_request is called subsequently, a dbus_aborted error is
 * automatically generated for @trigger.  Otherwise, after @callback is
 * called, the reference to @trigger is dropped.  It is assumed that the
 * caller will take ownership of @trigger in the callback if needed.
 */
unsigned int agent_request_passphrase(const char *path,
				agent_request_passphrase_func_t callback,
				struct l_dbus_message *trigger,
				void *user_data,
				agent_request_destroy_func_t destroy)
{
	struct agent *agent = get_agent(l_dbus_message_get_sender(trigger));
	struct l_dbus_message *message;

	if (!agent || !callback)
		return 0;

	l_debug("agent %p owner %s path %s", agent, agent->owner, agent->path);

	message = l_dbus_message_new_method_call(dbus_get_bus(),
							agent->owner,
							agent->path,
							IWD_AGENT_INTERFACE,
							"RequestPassphrase");

	l_dbus_message_set_arguments(message, "o", path);

	return agent_queue_request(agent, AGENT_REQUEST_TYPE_PASSPHRASE,
					message, agent_timeout_input_request(),
					callback, trigger, user_data, destroy);
}

unsigned int agent_request_pkey_passphrase(const char *path,
				agent_request_passphrase_func_t callback,
				struct l_dbus_message *trigger,
				void *user_data,
				agent_request_destroy_func_t destroy)
{
	struct agent *agent = get_agent(l_dbus_message_get_sender(trigger));
	struct l_dbus_message *message;

	if (!agent || !callback)
		return 0;

	l_debug("agent %p owner %s path %s", agent, agent->owner, agent->path);

	message = l_dbus_message_new_method_call(dbus_get_bus(),
						agent->owner, agent->path,
						IWD_AGENT_INTERFACE,
						"RequestPrivateKeyPassphrase");

	l_dbus_message_set_arguments(message, "o", path);

	return agent_queue_request(agent, AGENT_REQUEST_TYPE_PASSPHRASE,
					message, agent_timeout_input_request(),
					callback, trigger, user_data, destroy);
}

unsigned int agent_request_user_name_password(const char *path,
				agent_request_user_name_passwd_func_t callback,
				struct l_dbus_message *trigger,
				void *user_data,
				agent_request_destroy_func_t destroy)
{
	struct agent *agent = get_agent(l_dbus_message_get_sender(trigger));
	struct l_dbus_message *message;

	if (!agent || !callback)
		return 0;

	l_debug("agent %p owner %s path %s", agent, agent->owner, agent->path);

	message = l_dbus_message_new_method_call(dbus_get_bus(),
						agent->owner, agent->path,
						IWD_AGENT_INTERFACE,
						"RequestUserNameAndPassword");

	l_dbus_message_set_arguments(message, "o", path);

	return agent_queue_request(agent, AGENT_REQUEST_TYPE_USER_NAME_PASSWD,
					message, agent_timeout_input_request(),
					callback, trigger, user_data, destroy);
}

unsigned int agent_request_user_password(const char *path, const char *user,
				agent_request_passphrase_func_t callback,
				struct l_dbus_message *trigger, void *user_data,
				agent_request_destroy_func_t destroy)
{
	struct agent *agent = get_agent(l_dbus_message_get_sender(trigger));
	struct l_dbus_message *message;

	if (!agent || !callback)
		return 0;

	l_debug("agent %p owner %s path %s", agent, agent->owner, agent->path);

	message = l_dbus_message_new_method_call(dbus_get_bus(),
						agent->owner, agent->path,
						IWD_AGENT_INTERFACE,
						"RequestUserPassword");

	l_dbus_message_set_arguments(message, "os", path, user ?: "");

	return agent_queue_request(agent, AGENT_REQUEST_TYPE_PASSPHRASE,
					message, agent_timeout_input_request(),
					callback, trigger, user_data, destroy);
}

static bool find_request(const void *a, const void *b)
{
	const struct agent_request *request = a;
	unsigned int id = L_PTR_TO_UINT(b);

	return request->id == id;
}

bool agent_request_cancel(unsigned int req_id, int reason)
{
	struct agent_request *request = NULL;
	struct agent *agent;
	const struct l_queue_entry *entry;

	for (entry = l_queue_get_entries(agents); entry; entry = entry->next) {
		agent = entry->data;

		request = l_queue_remove_if(agent->requests, find_request,
							L_UINT_TO_PTR(req_id));
		if (request)
			break;
	}

	if (!request)
		return false;

	if (!request->message) {
		send_cancel_request(agent, reason);

		l_dbus_cancel(dbus_get_bus(), agent->pending_id);

		agent->pending_id = 0;

		if (agent->timeout) {
			l_timeout_remove(agent->timeout);
			agent->timeout = NULL;
		}

		agent_send_next_request(agent);
	}

	agent_request_free(request);

	return true;
}

static void agent_disconnect(struct l_dbus *dbus, void *user_data)
{
	struct agent *agent = user_data;

	l_debug("agent %s disconnected", agent->owner);

	l_queue_remove(agents, agent);

	l_idle_oneshot(agent_free, agent, NULL);
}

static struct agent *agent_create(struct l_dbus *dbus, const char *name,
							const char *path)
{
	struct agent *agent;

	agent = l_new(struct agent, 1);

	agent->owner = l_strdup(name);
	agent->path = l_strdup(path);
	agent->requests = l_queue_new();
	agent->disconnect_watch = l_dbus_add_disconnect_watch(dbus, name,
							agent_disconnect,
							agent, NULL);
	return agent;
}

static struct l_dbus_message *agent_register(struct l_dbus *dbus,
						struct l_dbus_message *message,
						void *user_data)
{
	struct agent *agent = agent_lookup(l_dbus_message_get_sender(message));
	struct l_dbus_message *reply;
	const char *path;

	if (agent)
		return dbus_error_already_exists(message);

	l_debug("agent register called");

	if (!l_dbus_message_get_arguments(message, "o", &path))
		return dbus_error_invalid_args(message);

	agent = agent_create(dbus, l_dbus_message_get_sender(message), path);
	if (!agent)
		return dbus_error_failed(message);

	l_queue_push_tail(agents, agent);

	l_debug("agent %s path %s", agent->owner, agent->path);

	reply = l_dbus_message_new_method_return(message);

	l_dbus_message_set_arguments(reply, "");

	return reply;
}

static struct l_dbus_message *agent_unregister(struct l_dbus *dbus,
						struct l_dbus_message *message,
						void *user_data)
{
	struct agent *agent = agent_lookup(l_dbus_message_get_sender(message));
	struct l_dbus_message *reply;

	l_debug("agent unregister");

	if (!agent)
		return dbus_error_not_found(message);

	l_queue_remove(agents, agent);

	agent_free(agent);

	reply = l_dbus_message_new_method_return(message);

	l_dbus_message_set_arguments(reply, "");

	return reply;
}

static void setup_agent_interface(struct l_dbus_interface *interface)
{
	l_dbus_interface_method(interface, "RegisterAgent", 0,
				agent_register,
				"", "o", "path");
	l_dbus_interface_method(interface, "UnregisterAgent", 0,
				agent_unregister,
				"", "o", "path");
}

static bool release_agent(void *data, void *user_data)
{
	struct agent *agent = data;

	send_request(agent, "Release");

	agent_free(agent);

	return true;
}

bool agent_init(struct l_dbus *dbus)
{
	agents = l_queue_new();

	if (!l_dbus_register_interface(dbus, IWD_AGENT_MANAGER_INTERFACE,
						setup_agent_interface,
						NULL, false)) {
		l_info("Unable to register %s interface",
				IWD_AGENT_MANAGER_INTERFACE);
		return false;
	}

	if (!l_dbus_object_add_interface(dbus, IWD_AGENT_MANAGER_PATH,
						IWD_AGENT_MANAGER_INTERFACE,
						NULL)) {
		l_info("Unable to register the agent manager object on '%s'",
				IWD_AGENT_MANAGER_PATH);
		l_dbus_unregister_interface(dbus, IWD_AGENT_MANAGER_INTERFACE);
		return false;
	}

	return true;
}

bool agent_exit(struct l_dbus *dbus)
{
	l_dbus_unregister_object(dbus, IWD_AGENT_MANAGER_PATH);
	l_dbus_unregister_interface(dbus, IWD_AGENT_MANAGER_INTERFACE);

	l_queue_destroy(agents, agent_free);
	agents = NULL;

	return true;
}

void agent_shutdown(void)
{
	l_queue_foreach_remove(agents, release_agent, NULL);
}
