/*
 * Copyright (c) 2021 Matthias Schmidt <xhr@giessen.ccc.de>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/stat.h>
#include <sys/types.h>

#include <limits.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#ifdef __OpenBSD__
#include <readpassphrase.h>
#endif

#include "jrnlc.h"

static int debug    = 0;
static int backup 	= 0;
static struct config conf;
static char jrnlc_config[_POSIX_PATH_MAX];
static unsigned char nonce[crypto_secretbox_NONCEBYTES];
extern char *__progname;

static void
usage(void)
{
	log_fatal(1, "%s [-aBde] [-DIn number]\n", __progname);
}

int
main(int argc, char **argv)
{
	char *passbuf;
	int encrypt 	= 0;
	int decrypt 	= 0;
	int last 		= 0;
	int entry 		= 0;
	int to_delete 	= 0;
	int ch;

	conf.jrnlc_journal[0] = '\0';

	while ((ch = getopt(argc, argv, "aBdD:ef:I:n:vV")) != -1) {
		switch (ch) {
		case 'a':
			last = -1;
			break;
		case 'B':
			backup = 1;
			break;
		case 'd':
			decrypt = 1;
			break;
		case 'D':
			to_delete = get_number(optarg);
			break;
		case 'e':
			encrypt = 1;
			break;
		case 'I':
			entry = get_number(optarg);
			break;
		case 'n':
			last = get_number(optarg);
			break;
		case 'v':
			debug++;
			break;
		case 'V':
			printf("Version %s\n", VERSION);
			exit(0);
			break;
		default:
			usage();
		}
	}

	argc -= optind;
	argv += optind;

	if (decrypt && encrypt) {
		log_fatal(1, "Please specify either -d or -e, but not both\n");
	}

	if (entry > 0 && last > 0) {
		log_fatal(1, "Please specify either -I or -n, but not both\n");
	}

	/* Check if base directory exists, if not create it */
	setup_base_dir();

	sandbox();

	/* Load existing config file or create a new one with default values */
	(void)load_config();

	if (sodium_init() == -1)
		log_fatal(1, "Cannot initialize sodium crypto library\n");

	passbuf = calloc_wrapper(1, MAX_PASS_LEN);

	/* User supplied either -e or -d */
	if (encrypt && conf.encrypted) {
		log_fatal(1, "Journal is already encrypted\n");
	} else if (decrypt && !conf.encrypted) {
		log_fatal(1, "Journal is already decrypted\n");
	} else if (encrypt && !conf.encrypted) {
		/* User wants to encrypt and journal is unencrypted */
		printf("Encrypting journal...\n");

		int res = get_initial_passphrase(passbuf);
		if (res == 2) {
			sodium_memzero(passbuf, sizeof(passbuf));
			log_fatal(1, "No blank password allowed\n");
		} else if (res == 3) {
			sodium_memzero(passbuf, sizeof(passbuf));
			log_fatal(1, "Passwords do not match\n");
		}

		/* Generate a new random key and nonce ... */
		crypto_secretbox_keygen(conf.key);
		randombytes_buf(nonce, sizeof nonce);

		/* ... and save both in the key pass file */
		write_pass_file((unsigned char *)conf.key, passbuf);
		sodium_memzero(passbuf, sizeof(passbuf));
		free(passbuf);
		passbuf = NULL;

		load_journal_from_disk();

		/* Make journal as encrypted and save it to disk */
		conf.encrypted = 1;
		shutdown(0);
	} else if (decrypt && conf.encrypted) {
		/* User wants to decrypt and journal is encrypted */
		printf("Decrypting journal...\n");

		read_password("Enter Password: ", passbuf);

		/* Load the nonce first, otherwise decryption of the key fails */
		load_nonce(nonce);
		/* Load key from disk, encrypt it and save it to memory */
		memcpy(conf.key, load_pass_file(passbuf), crypto_secretbox_KEYBYTES);
		sodium_memzero(passbuf, sizeof(passbuf));
		free(passbuf);
		passbuf = NULL;

		load_journal_from_disk();

		/* Make journal as decrypted and save it to disk */
		conf.encrypted = 0;
		shutdown(0);
	}

	if (is_encrypted()) {
		read_password("Enter Password: ", passbuf);
		/* Load the nonce first, otherwise decryption of the key fails */
		load_nonce(nonce);
		/* Load key from disk, encrypt it and save it to memory */
		memcpy(conf.key, load_pass_file(passbuf), crypto_secretbox_KEYBYTES);
		sodium_memzero(passbuf, sizeof(passbuf));
		free(passbuf);
		passbuf = NULL;
	}

	/* Load the journal file into memory */
	load_journal_from_disk();

	if (to_delete > 0) {
		if (!delete_entry(to_delete)) {
			log_fatal(1, "Entry %d not found.  Nothing to delete\n", to_delete);
		}
		shutdown(0);
	} else if (entry > 0) {
		print_single_journal_entry(entry);
	} else if (last < 0) {
		/* Show all entries */
		print_journal_entries(-1);
	} else if (last > 0) {
		print_journal_entries(last);
	} else {
		create_new_journal_entry();
	}

	shutdown(0);

	return 0;
}

void
shutdown(int prio)
{
	/* User requested a plain text backup with -B */
	if (backup)
		save_journal_to_disk(NULL, 1);

	save_journal_to_disk(get_jrnlc_journal(), 0);
	write_config();

	sodium_memzero(conf.key, crypto_secretbox_KEYBYTES);

	exit(prio);
}

void
setup_base_dir()
{
	struct stat sb;
	char *home, *xdg_home;
	int ret;

	if ((xdg_home = getenv("XDG_CONFIG_HOME")) != NULL) {
		ret = snprintf(conf.jrnlc_dir, sizeof(conf.jrnlc_dir), "%s/jrnlc", xdg_home);
		if (ret < 0 || (size_t)ret >= sizeof(conf.jrnlc_dir)) {
			log_fatal(1, "Path truncation happened.  Buffer to short to fit %s\n", conf.jrnlc_dir);
		}
	} else if ((home = getenv("HOME")) != NULL) {
		ret = snprintf(conf.jrnlc_dir, sizeof(conf.jrnlc_dir), "%s/.config/jrnlc", home);
		if (ret < 0 || (size_t)ret >= sizeof(conf.jrnlc_dir)) {
			log_fatal(1, "Path truncation happened.  Buffer to short to fit %s\n", conf.jrnlc_dir);
		}
	} else {
		log_fatal(1, "Neither $XDG_CONFIG_HOME nor $HOME is set!\n");
	}

	if (stat(conf.jrnlc_dir, &sb) == 0 && S_ISDIR(sb.st_mode)) {
		log_debug(1, "%s already exists\n", conf.jrnlc_dir);
	} else {
		log_debug(1, "%s does not exists.  Attempt to create it\n", conf.jrnlc_dir);
		if (mkdir(conf.jrnlc_dir, 0700) == -1) {
			log_fatal(1, "Cannot create %s directory\n", conf.jrnlc_dir);
		}
	}

	/* Set up path to the journal JSON file */
	ret = snprintf(conf.jrnlc_journal, _POSIX_PATH_MAX, "%s/journal.json", conf.jrnlc_dir);
	if (ret < 0 || (size_t)ret >= sizeof(conf.jrnlc_journal)) {
		log_fatal(1, "Path truncation happened.  Buffer to short to fit %s\n", conf.jrnlc_dir);
	}

	/* Set up path for config JSON file */
	ret = snprintf(jrnlc_config, _POSIX_PATH_MAX, "%s/config.json", conf.jrnlc_dir);
	if (ret < 0 || (size_t)ret >= sizeof(jrnlc_config)) {
		log_fatal(1, "Path truncation happened.  Buffer to short to fit %s\n", conf.jrnlc_dir);
	}

	/* Set up path for key JSON file */
	ret = snprintf(conf.key_path, _POSIX_PATH_MAX, "%s/key.json", conf.jrnlc_dir);
	if (ret < 0 || (size_t)ret >= sizeof(conf.key_path)) {
		log_fatal(1, "Path truncation happened.  Buffer to short to fit %s\n", conf.jrnlc_dir);
	}

}

const unsigned char *
get_nonce()
{
	return nonce;
}

const char*
get_jrnlc_journal()
{
	return conf.jrnlc_journal;
}

int
is_encrypted()
{
	return conf.encrypted;
}

struct config *
get_config()
{
	return &conf;
}

const char*
get_config_path()
{
	return jrnlc_config;
}

void
log_debug(int prio, const char *fmt, ...)
{
	va_list ap;

	if (debug == 0)
		return;

	if (debug < prio)
		return;

	va_start(ap, fmt);
	fprintf(stdout, "[*] ");
	vfprintf(stdout, fmt, ap);
	va_end(ap);
}

void
log_fatal(int prio, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	va_end(ap);

	exit(prio);
}

