/*-
 * Copyright (c) 2023 Baptiste Daroussin <bapt@FreeBSD.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer
 *    in this position and unchanged.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdbool.h>
#include <dirent.h>
#include <err.h>
#include <fcntl.h>
#include <string.h>
#include <sqlite3.h>
#include <locale.h>
#include <time.h>
#include <syslog.h>
#include <kcgi.h>

#ifndef NELEM
#define NELEM(array) (sizeof(array) / sizeof((array)[0]))
#endif

static locale_t l;
enum page {
	PAGE_INDEX,
	PAGE_RAW
};

static const char *const pages[] = {
	"mid",
	"midraw",
};

static void
send_error(struct kreq *r, enum khttp er, const char *msg)
{
	khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[er]);
	khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_TEXT_PLAIN]);
	khttp_body(r);
	khttp_puts(r, msg);
	khttp_free(r);
}

static enum khttp
sanitise(const struct kreq *r)
{
	if (r->page == NELEM(pages))
		return (KHTTP_400);
	else if (r->method != KMETHOD_GET)
		return (KHTTP_405);
	return KHTTP_200;
}

static void
redirect_to(struct kreq *r, const char *path)
{
	khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_303]);
	khttp_head(r, kresps[KRESP_LOCATION], "%s", path);
	khttp_body(r);
	khttp_free(r);
}

static void
usage(void)
{
	fprintf(stderr, "Usage: %s -d <archivedir> -m <msgid>\n",
	    getprogname());
	exit(1);
}

static char *
lookup_indir(const char *path, const char *msgid, const char *ml, bool raw, const char *prefix)
{
	sqlite3 *sqlite;
	sqlite3_stmt *stmt;

	if (sqlite3_open_v2(path, &sqlite, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK)
		return (NULL);

	if (sqlite3_prepare_v2(sqlite, "select idx, strftime('%s', date) from mails where msgid=?1", -1, &stmt, NULL) != SQLITE_OK) {
		syslog(LOG_ERR, "%s: Invalid sql: %s", path, sqlite3_errmsg(sqlite));
		warnx("%s: Invalid sql: %s", path, sqlite3_errmsg(sqlite));
		sqlite3_close(sqlite);
		return (NULL);
	}
	sqlite3_bind_text(stmt, 1, msgid, -1, SQLITE_STATIC);	
	if (sqlite3_step(stmt) == SQLITE_ROW) {
		time_t thismaildate = sqlite3_column_int64(stmt, 1);
		char dirname[1024];
		char *ret;
		strftime_l(dirname, sizeof(dirname), "%Y-%B", localtime(&thismaildate), l);
		if (asprintf(&ret, "%s%s/%s/%06lld.%s", prefix, ml, dirname, sqlite3_column_int64(stmt, 0) - 1, raw ? "txt" : "html") == -1) {
			sqlite3_finalize(stmt);
			sqlite3_close(sqlite);
			return (NULL);
		}
		sqlite3_finalize(stmt);
		sqlite3_close(sqlite);
		return (ret);
	}
	sqlite3_finalize(stmt);
	sqlite3_close(sqlite);
	return (NULL);
}

static char *
lookup(const char *dir, const char *msgid, bool raw, const char *prefix)
{
	DIR *dfp;
	struct dirent *d;
	char *res = NULL;
	char *path;

	if ((dfp = opendir(dir)) == NULL)
		errx(EXIT_FAILURE, "Invalid directory (%s)", dir);

	while ((d = readdir(dfp)) != NULL) {
		if (strcmp(d->d_name, ".") == 0 || strcmp(d->d_name, "..") == 0)
			continue;
		asprintf(&path, "%s/%s/.db", dir, d->d_name);
		if (path == NULL)
			continue;
		res = lookup_indir(path, msgid, d->d_name, raw, prefix);
		free(path);
		if (res != NULL)
			break;
	}
	closedir(dfp);
	return (res);
}

int
main(int argc, char **argv)
{
	int ch;
	const char *directory, *msgid;
	bool raw = false;
	char *res = NULL;
	const char *prefix;

	prefix = getenv("MLMMJ_ARCHIVES_PREFIX");
	if (prefix == NULL)
		prefix = "";
	directory = msgid = NULL;

	l = newlocale(LC_ALL_MASK, "C", NULL);
	if (l == NULL)
		err(EXIT_FAILURE, "newlocale");
	if (argc == 1) {
		struct kreq r;
		enum khttp er;
		directory = getenv("MLMMJ_ARCHIVES_DIRECTORY");
		if (directory == NULL) {
			syslog(LOG_ERR, "MLMMJ_ARCHIVES_DIRECTORY not defined");
			return (EXIT_FAILURE);
		}
		if (khttp_parse(&r, NULL, 0, pages, NELEM(pages), PAGE_INDEX))
			return (EXIT_FAILURE);
		switch (r.page) {
			case PAGE_RAW:
				raw = true;
				/* FALLTHROUGH */
			case PAGE_INDEX:
				if ((er = sanitise(&r)) != KHTTP_200) {
					send_error(&r, er, "Unsupported method");
					break;
				}
				if (r.path == NULL || r.path[0] == '\0' || r.fullpath[strlen(pages[r.page]) + 1] != '/')
					res = NULL;
				else
					res = lookup(directory, r.fullpath + strlen(pages[r.page]) + 2 , raw, prefix);
				if (res == NULL)
					send_error(&r, KHTTP_404, "Not found");
				else
					redirect_to(&r, res);
				free(res);
				break;
			default:
				send_error(&r, KHTTP_400, "invalid route");
				break;
		}
		freelocale(l);
		return (EXIT_SUCCESS);
	}
	while ((ch = getopt(argc, argv, "d:m:r")) != -1) {
		switch (ch) {
			case 'd':
				directory = optarg;
				break;
			case 'm':
				msgid = optarg;
				break;
			case 'r':
				raw = true;
				break;
			default:
				usage();
		}
	}
	argc -= optind;
	argv += optind;
	if (msgid == NULL || directory == NULL)
		errx(EXIT_FAILURE, "Requires both -d and -m arguments");

	res = lookup(directory, msgid, raw, prefix);
	if (res == NULL)
		return (EXIT_FAILURE);
	printf("%s\n", res);
	free(res);
	freelocale(l);

	return (EXIT_SUCCESS);
}
