/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
 *
 * 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 3 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, see <https://www.gnu.org/licenses/>.
 */

#include <ctype.h>
#include <err.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sysexits.h>
#include <time.h>
#include <tls.h>
#include <unistd.h>

#ifndef NO_READPASSPHRASE_H
#include <readpassphrase.h>
#endif

#if !defined(DIG_PATH) && !defined(DRILL_PATH)
#	ifdef __FreeBSD__
#		define DRILL_PATH "/usr/bin/drill"
#	else
#		define DIG_PATH "dig"
#	endif
#endif

typedef unsigned char byte;

static bool verbose;

int tlsRead(void *_tls, char *ptr, int len) {
	struct tls *tls = _tls;
	ssize_t ret;
	do {
		ret = tls_read(tls, ptr, len);
	} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
	if (ret < 0) errx(EX_IOERR, "tls_read: %s", tls_error(tls));
	if (verbose) fprintf(stderr, "%.*s", (int)ret, ptr);
	return ret;
}

int tlsWrite(void *_tls, const char *ptr, int len) {
	struct tls *tls = _tls;
	ssize_t ret;
	do {
		ret = tls_write(tls, ptr, len);
	} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
	if (ret < 0) errx(EX_IOERR, "tls_write: %s", tls_error(tls));
	if (verbose) fprintf(stderr, "%.*s", (int)ret, ptr);
	return ret;
}

int tlsClose(void *_tls) {
	struct tls *tls = _tls;
	int error = tls_close(tls);
	if (error) errx(EX_IOERR, "tls_close: %s", tls_error(tls));
	return error;
}

static void lookup(const char **host, const char **port, const char *domain) {
	static char buf[1024];
	snprintf(buf, sizeof(buf), "_imaps._tcp.%s", domain);

	int rw[2];
	int error = pipe(rw);
	if (error) err(EX_OSERR, "pipe");

	pid_t pid = fork();
	if (pid < 0) err(EX_OSERR, "fork");

	if (!pid) {
		close(rw[0]);
		dup2(rw[1], STDOUT_FILENO);
		dup2(rw[1], STDERR_FILENO);
		close(rw[1]);
#ifdef DRILL_PATH
		execlp(DRILL_PATH, DRILL_PATH, buf, "SRV", NULL);
		err(EX_CONFIG, "%s", DRILL_PATH);
#else
		execlp(DIG_PATH, DIG_PATH, "-t", "SRV", "-q", buf, "+short", NULL);
		err(EX_CONFIG, "%s", DIG_PATH);
#endif
	}

	int status;
	pid = wait(&status);
	if (pid < 0) err(EX_OSERR, "wait");

	close(rw[1]);
	FILE *pipe = fdopen(rw[0], "r");
	if (!pipe) err(EX_IOERR, "fdopen");

	fgets(buf, sizeof(buf), pipe);
	if (ferror(pipe)) err(EX_IOERR, "fgets");

	if (!WIFEXITED(status) || WEXITSTATUS(status)) {
		fprintf(stderr, "%s", buf);
		exit(WEXITSTATUS(status));
	}

	char *ptr = buf;
#ifdef DRILL_PATH
	for (;;) {
		char *line = fgets(buf, sizeof(buf), pipe);
		if (!line || !strcmp(line, ";; ANSWER SECTION:\n")) break;
	}
	fgets(buf, sizeof(buf), pipe);
	if (ferror(pipe)) err(EX_IOERR, "fgets");
	ptr = strrchr(buf, '\t');
	ptr = (ptr ? ptr + 1 : buf);
#endif
	fclose(pipe);

	char *dot = strrchr(ptr, '.');
	if (dot) *dot = '\0';
	strsep(&ptr, " \n"); // priority
	strsep(&ptr, " \n"); // weight
	*port = strsep(&ptr, " \n");
	*host = strsep(&ptr, " \n");
	if (!*host) {
		*host = domain;
		*port = "imaps";
	}
}

static const char *uuidGen(void) {
	byte uuid[16];
	arc4random_buf(uuid, sizeof(uuid));
	uuid[6] &= 0x0F;
	uuid[6] |= 0x40;
	uuid[8] &= 0x3F;
	uuid[8] |= 0x80;

	static char str[sizeof("00000000-0000-0000-0000-000000000000")];
	snprintf(
		str, sizeof(str),
		"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
		uuid[0], uuid[1], uuid[2], uuid[3],
		uuid[4], uuid[5], uuid[6], uuid[7],
		uuid[8], uuid[9], uuid[10], uuid[11],
		uuid[12], uuid[13], uuid[14], uuid[15]
	);
	return str;
}

static bool uuidCheck(const char *uuid) {
	if (strlen(uuid) != 36) return false;
	if (strspn(uuid, "0123456789abcdef-") != 36) return false;
	return true;
}

#define ENUM_ATOM \
	X(Unknown, "") \
	X(Untagged, "*") \
	X(Ok, "OK") \
	X(No, "NO") \
	X(Bad, "BAD") \
	X(Bye, "BYE") \
	X(Login, "LOGIN") \
	X(Search, "SEARCH") \
	X(Fetch, "FETCH") \
	X(Append, "APPEND") \
	X(Next, "next")

enum Atom {
#define X(id, _) id,
	ENUM_ATOM
#undef X
	AtomsLen,
};

static const char *Atoms[AtomsLen] = {
#define X(id, str) [id] = str,
	ENUM_ATOM
#undef X
};

static enum Atom atom(const char *str) {
	if (!str) return Unknown;
	for (enum Atom i = 0; i < AtomsLen; ++i) {
		if (!strcmp(str, Atoms[i])) return i;
	}
	return Unknown;
}

#define DATE_FORMAT "%a, %e %b %Y %H:%M:%S %z"

static void append(
	FILE *imap, const char *mailbox,
	const char *from, const char *uuid, const char *path
) {
	FILE *note = fopen(path, "r");
	if (!note) err(EX_NOINPUT, "%s", path);

	struct stat status;
	int error = fstat(fileno(note), &status);
	if (error) err(EX_IOERR, "%s", path);

	char date[sizeof("Mon, 00 Jan 0000 00:00:00 -0000")];
	strftime(
		date, sizeof(date), DATE_FORMAT,
		localtime(&status.st_mtime)
	);

#define HEADERS \
	"From: <%s>\r\n" \
	"Subject: %s\r\n" \
	"Date: %s\r\n" \
	"X-Universally-Unique-Identifier: %s\r\n" \
	"X-Uniform-Type-Identifier: com.apple.mail-note\r\n" \
	"X-Mailer: notemap\r\n" \
	"MIME-Version: 1.0\r\n" \
	"Content-Type: text/plain; charset=\"utf-8\"\r\n" \
	"Content-Transfer-Encoding: quoted-printable\r\n" \
	"\r\n"

	size_t max = sizeof(HEADERS)
		+ strlen(from)
		+ strlen(path)
		+ strlen(date)
		+ strlen(uuid)
		+ 3 * status.st_size
		+ 3 * status.st_size / 76;
	char *buf = malloc(max);
	if (!buf) err(EX_OSERR, "malloc");

	FILE *msg = fmemopen(buf, max, "w");
	if (!msg) err(EX_OSERR, "fmemopen");
	fprintf(msg, HEADERS, from, path, date, uuid);
#undef HEADERS

	int ch;
	int len = 0;
	while (EOF != (ch = fgetc(note))) {
		if (len == 76 && ch != '\n') {
			fprintf(msg, "=\r\n");
			len = 0;
		}
		if (ch == '\n') {
			// TODO: Check if last character was space or tab.
			fprintf(msg, "\r\n");
			len = 0;
		} else if (ch == '\t' || (ch >= ' ' && ch <= '~' && ch != '=')) {
			fprintf(msg, "%c", ch);
			len++;
		} else {
			fprintf(msg, "=%02X", ch);
			len += 3;
		}
	}
	if (ferror(note)) err(EX_IOERR, "%s", path);
	fclose(note);
	fclose(msg);

	buf[max - 1] = '\0';
	fprintf(
		imap, "%s APPEND %s (\\Seen) {%zu}\r\n",
		Atoms[Append], mailbox, strlen(buf)
	);
	if (fgetc(imap) == '+') {
		ungetc('+', imap);
		fprintf(imap, "%s\r\n", buf);
	}
	free(buf);
}

int main(int argc, char *argv[]) {
	const char *path = ".notemap";
	bool add = false;
	bool force = false;

	const char *user = NULL;
	const char *host = NULL;
	const char *port = "imaps";
	const char *mailbox = "Notes";
	int rppFlags = 0;

	int opt;
	while (0 < (opt = getopt(argc, argv, "M:afh:m:p:u:vw"))) {
		switch (opt) {
			break; case 'M': mailbox = optarg;
			break; case 'a': add = true;
			break; case 'f': force = true;
			break; case 'h': host = optarg;
			break; case 'm': path = optarg;
			break; case 'p': port = optarg;
			break; case 'u': user = optarg;
			break; case 'v': verbose = true;
			break; case 'w': rppFlags |= RPP_STDIN;
			break; default:  return EX_USAGE;
		}
	}
	if (!user) errx(EX_USAGE, "username required");
	argv += optind;
	argc -= optind;
	
	if (!host) {
		const char *domain = strchr(user, '@');
		if (!domain) errx(EX_USAGE, "no domain in username");
		lookup(&host, &port, &domain[1]);
	}

	char buf[1024];
	char *pass = readpassphrase(
		(rppFlags & RPP_STDIN ? "" : "Password: "),
		buf, sizeof(buf), rppFlags
	);
	if (!pass) err(EX_UNAVAILABLE, "readpassphrase");

	if (add) {
		FILE *map = fopen(path, "a");
		if (!map) err(EX_CANTCREAT, "%s", path);
		for (int i = 0; i < argc; ++i) {
			if (access(argv[i], R_OK)) err(EX_NOINPUT, "%s", argv[i]);
			fprintf(map, "%s %s\n", uuidGen(), argv[i]);
			if (ferror(map)) err(EX_IOERR, "%s", path);
		}
		int error = fclose(map);
		if (error) err(EX_IOERR, "%s", path);
	}

	FILE *map = fopen(path, "r");
	if (!map) err(EX_NOINPUT, "%s", path);

	struct tls *client = tls_client();
	if (!client) errx(EX_SOFTWARE, "tls_client");

	struct tls_config *config = tls_config_new();
	if (!config) errx(EX_SOFTWARE, "tls_config_new");

	int error = tls_configure(client, config);
	if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client));
	tls_config_free(config);

	error = tls_connect(client, host, port);
	if (error) errx(EX_NOHOST, "tls_connect: %s", tls_error(client));

	FILE *imap = funopen(client, tlsRead, tlsWrite, NULL, tlsClose);
	if (!imap) err(EX_SOFTWARE, "funopen");
	setlinebuf(imap);

	char *line = NULL;
	size_t lineCap = 0;

	char *entry = NULL;
	size_t entryCap = 0;
	char *uuid = NULL;
	char *note = NULL;
	int seq = 0;

	bool login = false;
	while (0 < getline(&line, &lineCap, imap)) {
		char *cr = strchr(line, '\r');
		if (cr) *cr = '\0';

		char *rest = line;
		enum Atom tag = atom(strsep(&rest, " "));
		if (rest && isdigit(rest[0])) {
			strsep(&rest, " ");
		}
		enum Atom resp = atom(strsep(&rest, " "));

		if (resp == No || resp == Bad || resp == Bye) {
			errx(
				EX_CONFIG, "%s: %s %s",
				Atoms[tag], Atoms[resp], (rest ? rest : "")
			);
		}

		switch (tag) {
			break; case Untagged: {
				if (login) break;
				fprintf(
					imap, "%s LOGIN \"%s\" \"%s\"\r\n",
					Atoms[Login], user, pass
				);
				login = true;
			}

			break; case Login: {
				fprintf(imap, "%s SELECT \"%s\"\r\n", Atoms[Next], mailbox);
			}

			break; case Next: Next: {
				ssize_t len;
				len = getline(&entry, &entryCap, map);
				if (ferror(map)) err(EX_IOERR, "%s", path);
				if (len < 1) goto done;
				if (entry[len - 1] == '\n') entry[len - 1] = '\0';

				note = entry;
				uuid = strsep(&note, " ");
				if (!note || !uuid || !uuidCheck(uuid)) {
					errx(EX_CONFIG, "invalid map entry: %s", entry);
				}

				if (argc) {
					int i;
					for (i = 0; i < argc; ++i) {
						if (!argv[i]) continue;
						if (strcmp(argv[i], note)) continue;
						argv[i] = NULL;
						break;
					}
					if (i == argc) goto Next;
				}

				fprintf(
					imap,
					"%s SEARCH HEADER X-Universally-Unique-Identifier %s\r\n",
					Atoms[Search], uuid
				);
			}

			break; case Search: {
				if (!seq) goto Fetch;
				fprintf(imap, "%s FETCH %d ENVELOPE\r\n", Atoms[Fetch], seq);
			}

			break; case Fetch: Fetch: {
				append(imap, mailbox, user, uuid, note);
			}

			break; case Append: {
				printf("%c %s\n", (seq ? '~' : '+'), note);
				if (!seq) goto Next;
				fprintf(
					imap, "%s STORE %d +FLAGS.SILENT (\\Deleted)\r\n",
					Atoms[Next], seq
				);
			}

			break; default:;
		}

		if (resp == Search) {
			if (rest) {
				seq = strtol(rest, &rest, 10);
				if (*rest) {
					errx(EX_CONFIG, "multiple messages matching %s", uuid);
				}
			} else {
				seq = 0;
			}
		}

		if (resp == Fetch) {
			if (strncmp(rest, "(ENVELOPE", 9)) continue;

			struct tm date = {0};
			rest = strptime(
				rest, "(ENVELOPE (\"" DATE_FORMAT "\"", &date
			);
			if (!rest) errx(EX_PROTOCOL, "invalid envelope date");

			struct stat status;
			int error = stat(note, &status);
			if (error) err(EX_NOINPUT, "%s", note);

			if (!force && status.st_mtime < mktime(&date)) {
				errx(
					EX_TEMPFAIL,
					"%s: note modified in mailbox; use -f to overwrite",
					note
				);
			} else if (status.st_mtime == mktime(&date)) {
				goto Next;
			}
		}
	}

done:
	fprintf(imap, "ayy LOGOUT\r\n");
	fclose(imap);

	int ret = EX_OK;
	for (int i = 0; i < argc; ++i) {
		if (!argv[i]) continue;
		warnx("%s: unmapped note; use -a to add", argv[i]);
		ret = EX_CONFIG;
	}
	return ret;
}