/*
 * Off-the-Record Messaging (OTR) modules for IRC
 *
 * Copyright (C) 2008 - Uli Meis <a.sporto+bee@gmail.com>
 *               2012 - David Goulet <dgoulet@ev0ke.net>
 *
 * 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.
 *
 * 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 Street, Fifth Floor, Boston, MA 02110-1301,USA
 */

#define _GNU_SOURCE
#include <glib.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/poll.h>
#include <signal.h>
#include <unistd.h>

#include "key.h"

#include "levels.h"
#include "network.h"
#include "pidwait.h"
#include "printtext.h"

#include "irssi-otr.h"
#include "otr-formats.h"

/*
 * Status of key generation.
 */
enum key_gen_status {
	KEY_GEN_IDLE		= 0,
	KEY_GEN_STARTED		= 1,
	KEY_GEN_RUNNING		= 2,
	KEY_GEN_FINISHED	= 3,
	KEY_GEN_ERROR		= 4,
};

/*
 * Data of the state of key generation.
 */
struct key_gen_data {
	struct otr_user_state *ustate;
	char *account_name;
	char *key_file_path;
	enum key_gen_status status;
	gcry_error_t gcry_error;
};

/*
 * Event from the key generation process.
 */
struct key_gen_event {
	enum key_gen_status status;
	gcry_error_t error;
};

/*
 * Key generation process.
 */
struct key_gen_worker {
	int tag;
	GIOChannel *pipes[2];
};

/*
 * Key generation data for the thread in charge of creating the key.
 */
static struct key_gen_data key_gen_state = {
	.status = KEY_GEN_IDLE,
	.gcry_error = GPG_ERR_NO_ERROR,
};

/*
 * Build file path concatenate to the irssi config dir.
 */
static char *file_path_build(const char *path)
{
	g_return_val_if_fail(path != NULL, NULL);

	/* Either NULL or the filename is returned here which is valid. */
	return g_strdup_printf("%s/%s", get_irssi_dir(), path);
}

/*
 * Emit a key generation status event.
 */
static void emit_event(GIOChannel *pipe, enum key_gen_status status, gcry_error_t error)
{
	struct key_gen_event event;

	g_return_if_fail(pipe != NULL);

	event.status = status;
	event.error = error;

	g_io_channel_write_block(pipe, &event, sizeof(event));
}

/*
 * Reset key generation state and status is IDLE.
 */
static void reset_key_gen_state(void)
{
	/* Safety. */
	g_free(key_gen_state.key_file_path);
	g_free(key_gen_state.account_name);

	/* Nullify everything. */
	memset(&key_gen_state, 0, sizeof(key_gen_state));
	key_gen_state.status = KEY_GEN_IDLE;
	key_gen_state.gcry_error = GPG_ERR_NO_ERROR;
}

/*
 * Read status event from key generation worker.
 */
static void read_key_gen_status(struct key_gen_worker *worker, GIOChannel *pipe)
{
	struct key_gen_event event;
	gcry_error_t err;

	g_return_if_fail(worker != NULL);

	fcntl(g_io_channel_unix_get_fd(pipe), F_SETFL, O_NONBLOCK);

	if (g_io_channel_read_block(pipe, &event, sizeof(event)) == -1) {
		printformat(NULL, NULL, MSGLEVEL_CLIENTERROR,
				TXT_OTR_KEYGEN_FAILED,
				key_gen_state.account_name,
				g_strerror(errno));
		return;
	}

	key_gen_state.status = event.status;
	key_gen_state.gcry_error = event.error;

	if (event.status == KEY_GEN_FINISHED || event.status == KEY_GEN_ERROR) {
		/* Worker is done. */
		g_source_remove(worker->tag);

		g_io_channel_shutdown(worker->pipes[0], TRUE, NULL);
		g_io_channel_unref(worker->pipes[0]);

		g_io_channel_shutdown(worker->pipes[1], TRUE, NULL);
		g_io_channel_unref(worker->pipes[1]);

		g_free(worker);

		if (event.status == KEY_GEN_ERROR) {
			printformat(NULL, NULL, MSGLEVEL_CLIENTERROR,
					TXT_OTR_KEYGEN_FAILED,
					key_gen_state.account_name,
					gcry_strerror(key_gen_state.gcry_error));
			reset_key_gen_state();
			return;
		}

		err = otrl_privkey_read(key_gen_state.ustate->otr_state, key_gen_state.key_file_path);

		if (err != GPG_ERR_NO_ERROR) {
			printformat(NULL, NULL, MSGLEVEL_CLIENTERROR,
					TXT_OTR_KEYGEN_FAILED,
					key_gen_state.account_name,
					gcry_strerror(key_gen_state.gcry_error));
		} else {
			printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE,
					TXT_OTR_KEYGEN_COMPLETED,
					key_gen_state.account_name);
		}

		reset_key_gen_state();
	}
}

/*
 * Run key generation in a seperate process (takes ages). The other process
 * will rewrite the key file, we shouldn't change anything till it's done and
 * we've reloaded the keys.
 */
void key_gen_run(struct otr_user_state *ustate, const char *account_name)
{
	struct key_gen_worker *worker;
	int fd[2];
	gcry_error_t err;
	pid_t pid;

	g_return_if_fail(ustate != NULL);
	g_return_if_fail(account_name != NULL);

	if (key_gen_state.status != KEY_GEN_IDLE) {
		printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, TXT_OTR_KEYGEN_RUNNING, key_gen_state.account_name);
		return;
	}

	/* Make sure the pointer does not go away during the proess. */
	key_gen_state.account_name = strdup(account_name);
	key_gen_state.ustate = ustate;
	key_gen_state.status = KEY_GEN_STARTED;

	/* Creating key file path. */
	key_gen_state.key_file_path = file_path_build(OTR_KEYFILE);
	if (key_gen_state.key_file_path == NULL) {
		printformat(NULL, NULL, MSGLEVEL_CLIENTERROR,
				TXT_OTR_KEYGEN_FAILED,
				key_gen_state.account_name,
				g_strerror(errno));
		reset_key_gen_state();
		return;
	}

	printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, TXT_OTR_KEYGEN_STARTED, key_gen_state.account_name);

	if (pipe(fd) != 0) {
		printformat(NULL, NULL, MSGLEVEL_CLIENTERROR,
				TXT_OTR_KEYGEN_FAILED,
				key_gen_state.account_name,
				g_strerror(errno));
		reset_key_gen_state();
		return;
	}

	worker = g_new0(struct key_gen_worker, 1);

	if (worker == NULL) {
		printformat(NULL, NULL, MSGLEVEL_CLIENTERROR,
				TXT_OTR_KEYGEN_FAILED,
				key_gen_state.account_name,
				g_strerror(errno));
		reset_key_gen_state();
		return;
	}

	worker->pipes[0] = g_io_channel_new(fd[0]);
	worker->pipes[1] = g_io_channel_new(fd[1]);

	pid = fork();

	if (pid > 0) {
		/* Parent process */
		pidwait_add(pid);
		worker->tag = g_input_add(worker->pipes[0], G_INPUT_READ, (GInputFunction)read_key_gen_status, worker);
		return;
	}

	if (pid != 0) {
		/* error */
		g_warning("Key generation failed: %s", g_strerror(errno));

		g_source_remove(worker->tag);

		g_io_channel_shutdown(worker->pipes[0], TRUE, NULL);
		g_io_channel_unref(worker->pipes[0]);

		g_io_channel_shutdown(worker->pipes[1], TRUE, NULL);
		g_io_channel_unref(worker->pipes[1]);

		g_free(worker);

		return;
	}

	/* Child process */
	key_gen_state.status = KEY_GEN_RUNNING;
	emit_event(worker->pipes[1], KEY_GEN_RUNNING, GPG_ERR_NO_ERROR);

	err = otrl_privkey_generate(key_gen_state.ustate->otr_state, key_gen_state.key_file_path, key_gen_state.account_name, OTR_PROTOCOL_ID);

	if (err != GPG_ERR_NO_ERROR) {
		emit_event(worker->pipes[1], KEY_GEN_ERROR, err);
		_exit(99);
		return;
	}

	emit_event(worker->pipes[1], KEY_GEN_FINISHED, GPG_ERR_NO_ERROR);

	_exit(99);
}

/*
 * Write fingerprints to file.
 */
void key_write_fingerprints(struct otr_user_state *ustate)
{
	gcry_error_t err;
	char *filename;

	g_return_if_fail(ustate != NULL);

	filename = file_path_build(OTR_FINGERPRINTS_FILE);
	g_return_if_fail(filename != NULL);

	err = otrl_privkey_write_fingerprints(ustate->otr_state, filename);
	if (err == GPG_ERR_NO_ERROR) {
		IRSSI_OTR_DEBUG("Fingerprints saved to %9%s%9", filename);
	} else {
		IRSSI_OTR_DEBUG("Error writing fingerprints: %d (%d)",
				gcry_strerror(err), gcry_strsource(err));
	}

	g_free(filename);
}

/*
 * Write instance tags to file.
 */
void key_write_instags(struct otr_user_state *ustate)
{
	gcry_error_t err;
	char *filename;

	g_return_if_fail(ustate != NULL);

	filename = file_path_build(OTR_INSTAG_FILE);
	g_return_if_fail(filename != NULL);

	err = otrl_instag_write(ustate->otr_state, filename);
	if (err == GPG_ERR_NO_ERROR) {
		IRSSI_OTR_DEBUG("Instance tags saved in %9%s%9", filename);
	} else {
		IRSSI_OTR_DEBUG("Error saving instance tags: %d (%d)",
				gcry_strerror(err), gcry_strsource(err));
	}

	g_free(filename);
}

/*
 * Load private keys.
 */
void key_load(struct otr_user_state *ustate)
{
	int ret;
	gcry_error_t err;
	char *filename;

	g_return_if_fail(ustate != NULL);

	filename = file_path_build(OTR_KEYFILE);
	g_return_if_fail(filename != NULL);

	ret = access(filename, F_OK);
	if (ret < 0) {
		IRSSI_OTR_DEBUG("No private keys found in %9%s%9", filename);
		g_free(filename);
		return;
	}

	err = otrl_privkey_read(ustate->otr_state, filename);
	if (err == GPG_ERR_NO_ERROR) {
		IRSSI_OTR_DEBUG("Private keys loaded from %9%s%9", filename);
	} else {
		IRSSI_OTR_DEBUG("Error loading private keys: %d (%d)",
				gcry_strerror(err), gcry_strsource(err));
	}

	g_free(filename);
}

/*
 * Load fingerprints.
 */
void key_load_fingerprints(struct otr_user_state *ustate)
{
	int ret;
	gcry_error_t err;
	char *filename;

	g_return_if_fail(ustate != NULL);

	filename = file_path_build(OTR_FINGERPRINTS_FILE);
	g_return_if_fail(filename != NULL);

	ret = access(filename, F_OK);
	if (ret < 0) {
		IRSSI_OTR_DEBUG("No fingerprints found in %9%s%9", filename);
		g_free(filename);
		return;
	}

	err = otrl_privkey_read_fingerprints(ustate->otr_state, filename, NULL,
			NULL);
	if (err == GPG_ERR_NO_ERROR) {
		IRSSI_OTR_DEBUG("Fingerprints loaded from %9%s%9", filename);
	} else {
		IRSSI_OTR_DEBUG("Error loading fingerprints: %d (%d)",
				gcry_strerror(err), gcry_strsource(err));
	}

	g_free(filename);
}
