#include "log.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.export.h"
#include "ssb.h"
#include "ssb.import.h"
#include "task.h"
#include "taskstub.js.h"
#include "tests.h"
#include "util.js.h"

#include "ares.h"
#include "backtrace.h"
#include "sqlite3.h"
#include "unzip.h"

#include <getopt.h>
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>

#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__HAIKU__)
#include <signal.h>
#include <sys/resource.h>
#endif

#if defined(__linux__)
#include <sys/prctl.h>
#include <sys/stat.h>
#endif

#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif

#if !defined(_WIN32)
#include <unistd.h>
#endif

#if defined(__ANDROID__)
#include "jni.h"
#endif

struct backtrace_state* g_backtrace_state;

#if !TARGET_OS_IPHONE
static const char* _get_db_path()
{
	const char* k_db_path_default = "db.sqlite";
#if defined(__linux__) && !defined(__ANDROID__)
	if (stat(k_db_path_default, &(struct stat) { 0 }) == 0)
	{
		return tf_strdup(k_db_path_default);
	}
	else
	{
		char buffer[32];

		char* data_home = NULL;
		size_t size = sizeof(buffer);
		int r = uv_os_getenv("XDG_DATA_HOME", buffer, &size);
		if (r == 0 || r == UV_ENOBUFS)
		{
			size++;
			data_home = alloca(size);
			if (uv_os_getenv("XDG_DATA_HOME", data_home, &size) != 0)
			{
				data_home = NULL;
			}
		}

		if (!data_home)
		{
			size = sizeof(buffer);
			r = uv_os_getenv("HOME", buffer, &size);
			if (r == 0 || r == UV_ENOBUFS)
			{
				size++;
				char* home = alloca(size);
				r = uv_os_getenv("HOME", home, &size);
				if (r == 0)
				{
					size = snprintf(NULL, 0, "%s/.local/share", home) + 1;
					data_home = alloca(size);
					snprintf(data_home, size, "%s/.local/share", home);
				}
			}
		}

		if (data_home)
		{
			size = snprintf(NULL, 0, "%s/tildefriends/db.sqlite", data_home) + 1;
			char* path = alloca(size);
			snprintf(path, size, "%s/tildefriends/db.sqlite", data_home);
			return tf_strdup(path);
		}
	}
#endif
	return tf_strdup(k_db_path_default);
}

static void _create_directories_for_file(const char* path, int mode)
{
	if (stat(path, &(struct stat) { 0 }) == 0)
	{
		/* It already exists.  OK. */
		return;
	}

	size_t length = strlen(path) + 1;
	char* copy = alloca(length);
	memcpy(copy, path, length);
#if defined(_WIN32)
	for (char* c = copy; *c; c++)
	{
		if (*c == '\\')
		{
			*c = '/';
		}
	}
#endif
	char* slash = copy;
	while (slash)
	{
		slash = strchr(slash + 1, '/');
		if (slash)
		{
			*slash = '\0';
#if defined(_WIN32)
			if (mkdir(copy) == 0)
#else
			if (mkdir(copy, mode) == 0)
#endif
			{
				tf_printf("Created directory %s.\n", copy);
			}
			*slash = '/';
		}
	}
}
#endif

#if !TARGET_OS_IPHONE && !defined(__ANDROID__)
static int _tf_command_export(const char* file, int argc, char* argv[]);
static int _tf_command_import(const char* file, int argc, char* argv[]);
static int _tf_command_publish(const char* file, int argc, char* argv[]);
static int _tf_command_private(const char* file, int argc, char* argv[]);
static int _tf_command_run(const char* file, int argc, char* argv[]);
static int _tf_command_sandbox(const char* file, int argc, char* argv[]);
static int _tf_command_has_blob(const char* file, int argc, char* argv[]);
static int _tf_command_get_blob(const char* file, int argc, char* argv[]);
static int _tf_command_store_blob(const char* file, int argc, char* argv[]);
static int _tf_command_create_invite(const char* file, int argc, char* argv[]);
static int _tf_command_get_sequence(const char* file, int argc, char* argv[]);
static int _tf_command_get_identity(const char* file, int argc, char* argv[]);
static int _tf_command_get_profile(const char* file, int argc, char* argv[]);
static int _tf_command_get_contacts(const char* file, int argc, char* argv[]);
static int _tf_command_test(const char* file, int argc, char* argv[]);
static int _tf_command_verify(const char* file, int argc, char* argv[]);
static int _tf_command_usage(const char* file);

typedef struct _command_t
{
	const char* name;
	int (*callback)(const char* file, int argc, char* argv[]);
	const char* description;
} command_t;

const command_t k_commands[] = {
	{ "run", _tf_command_run, "Run tildefriends (default)." },
	{ "sandbox", _tf_command_sandbox, "Run a sandboxed tildefriends sandbox process (used internally)." },
	{ "import", _tf_command_import, "Import apps from file to the database." },
	{ "export", _tf_command_export, "Export apps from the database to file." },
	{ "publish", _tf_command_publish, "Append a message to a feed." },
	{ "private", _tf_command_private, "Append a private post message to a feed." },
	{ "create_invite", _tf_command_create_invite, "Create an invite." },
	{ "get_sequence", _tf_command_get_sequence, "Get the last sequence number for a feed." },
	{ "get_identity", _tf_command_get_identity, "Get the server account identity." },
	{ "get_profile", _tf_command_get_profile, "Get profile information for the given identity." },
	{ "get_contacts", _tf_command_get_contacts, "Get information about followed, blocked, and friend identities." },
	{ "has_blob", _tf_command_has_blob, "Check whether a blob is in the blob store." },
	{ "get_blob", _tf_command_get_blob, "Read a file from the blob store." },
	{ "store_blob", _tf_command_store_blob, "Write a file to the blob store." },
	{ "verify", _tf_command_verify, "Verify a feed." },
	{ "test", _tf_command_test, "Test SSB." },
};

static const char* _description(const char* name)
{
	for (int i = 0; i < tf_countof(k_commands); i++)
	{
		if (strcmp(name, k_commands[i].name) == 0)
		{
			return k_commands[i].description;
		}
	}
	return NULL;
}

static int _tf_command_test(const char* file, int argc, char* argv[])
{
#if !defined(__ANDROID__)
	const char* default_db_path = _get_db_path();
	tf_test_options_t test_options = {
		.exe_path = file,
		.db_path = default_db_path,
	};
	bool show_usage = false;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "tests", required_argument, NULL, 't' },
			{ "db-path", required_argument, NULL, 'd' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "t:d:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 't':
			test_options.tests = optarg;
			break;
		case 'd':
			test_options.db_path = optarg;
			break;
		}
	}

	for (int i = optind; i < argc; i++)
	{
		tf_printf("Unexpected argument: %s\n", argv[i]);
		show_usage = true;
	}

	if (show_usage)
	{
		tf_printf("\nUsage: %s test [options]\n\n", file);
		tf_printf("%s\n\n", _description("test"));
		tf_printf("options:\n");
		tf_printf("  -t, --tests tests      Comma-separated list of tests to run.  (default: all)\n");
		tf_printf("  -h, --help             Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	tf_tests(&test_options);
	tf_free((void*)default_db_path);
	return EXIT_SUCCESS;
#else
	return EXIT_FAILURE;
#endif
}

static int _tf_command_import(const char* file, int argc, char* argv[])
{
	const char* user = "import";
	const char* default_db_path = _get_db_path();
	const char* db_path = default_db_path;
	bool show_usage = false;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "user", required_argument, NULL, 'u' },
			{ "db-path", required_argument, NULL, 'd' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "u:d:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'u':
			user = optarg;
			break;
		case 'd':
			db_path = optarg;
			break;
		}
	}

	if (show_usage)
	{
		tf_printf("\nUsage: %s import [options] [paths...]\n\n", file);
		tf_printf("%s\n\n", _description("import"));
		tf_printf("options:\n");
		tf_printf("  -u, --user user          User into whose account apps will be imported (default: \"import\").\n");
		tf_printf("  -d, --db-path db_path    SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -h, --help               Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	_create_directories_for_file(db_path, 0700);
	tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
	tf_ssb_set_quiet(ssb, true);
	if (optind < argc)
	{
		for (int i = optind; i < argc; i++)
		{
			tf_printf("Importing %s...\n", argv[i]);
			tf_ssb_import(ssb, user, argv[i]);
		}
	}
	else
	{
		tf_printf("Importing %s...\n", "apps");
		tf_ssb_import(ssb, user, "apps");
	}
	tf_ssb_destroy(ssb);
	tf_free((void*)default_db_path);
	return EXIT_SUCCESS;
}

static int _tf_command_export(const char* file, int argc, char* argv[])
{
	const char* user = "core";
	const char* default_db_path = _get_db_path();
	const char* db_path = default_db_path;
	bool show_usage = false;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "user", required_argument, NULL, 'u' },
			{ "db-path", required_argument, NULL, 'd' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "u:d:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'u':
			user = optarg;
			break;
		case 'd':
			db_path = optarg;
			break;
		}
	}

	if (show_usage)
	{
		tf_printf("\nUsage: %s export [options] [paths...]\n\n", file);
		tf_printf("%s\n\n", _description("export"));
		tf_printf("options:\n");
		tf_printf("  -u, --user user          User from whose account apps will be exported (default: \"core\").\n");
		tf_printf("  -d, --db-path db_path    SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -h, --help               Show this usage information.\n");
		tf_printf("\n");
		tf_printf("paths                      Paths of apps to export (example: /~core/ssb /~user/app).\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
	tf_ssb_set_quiet(ssb, true);
	if (optind < argc)
	{
		for (int i = optind; i < argc; i++)
		{
			tf_printf("Exporting %s...\n", argv[i]);
			tf_ssb_export(ssb, argv[i]);
		}
	}
	else
	{
		const char* k_export[] = {
			"admin",
			"api",
			"apps",
			"appstore",
			"db",
			"docs",
			"follow",
			"ssb",
			"todo",
		};
		for (int i = 0; i < tf_countof(k_export); i++)
		{
			char buffer[256];
			snprintf(buffer, sizeof(buffer), "/~%s/%s", user, k_export[i]);
			tf_printf("Exporting %s...\n", buffer);
			tf_ssb_export(ssb, buffer);
		}
	}
	tf_ssb_destroy(ssb);
	tf_free((void*)default_db_path);
	return EXIT_SUCCESS;
}

static void _tf_published_callback(const char* id, bool verified, bool stored, void* user_data)
{
	if (verified)
	{
		if (stored)
		{
			tf_printf("Message %s stored.\n", id);
		}
		else
		{
			tf_printf("Unable to store the message.\n");
		}
	}
	else
	{
		tf_printf("Failed to verify the message.\n");
	}
	*(int*)user_data = stored ? EXIT_SUCCESS : EXIT_FAILURE;
}

static int _tf_command_publish(const char* file, int argc, char* argv[])
{
	const char* user = NULL;
	const char* identity = NULL;
	const char* default_db_path = _get_db_path();
	const char* db_path = default_db_path;
	const char* content = NULL;
	bool show_usage = false;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "user", required_argument, NULL, 'u' },
			{ "id", required_argument, NULL, 'i' },
			{ "db-path", required_argument, NULL, 'd' },
			{ "content", required_argument, NULL, 'c' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "u:i:d:c:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'u':
			user = optarg;
			break;
		case 'i':
			identity = optarg;
			break;
		case 'd':
			db_path = optarg;
			break;
		case 'c':
			content = optarg;
			break;
		}
	}

	if (show_usage || !identity || !content)
	{
		tf_printf("\nUsage: %s publish [options]\n\n", file);
		tf_printf("%s\n\n", _description("publish"));
		tf_printf("options:\n");
		tf_printf("  -u, --user user          User owning identity with which to publish.\n");
		tf_printf("  -i, --id identity        Identity with which to publish message.\n");
		tf_printf("  -d, --db-path db_path    SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -c, --content json       JSON content of message to publish.\n");
		tf_printf("  -h, --help               Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	int result = EXIT_FAILURE;
	tf_printf("Posting %s as account %s belonging to %s...\n", content, identity, user);
	_create_directories_for_file(db_path, 0700);
	tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
	tf_ssb_set_quiet(ssb, true);
	uint8_t private_key[512] = { 0 };

	bool free_user = false;
	if (!user)
	{
		user = tf_ssb_db_get_user_for_identity(ssb, identity);
		free_user = true;
	}

	if (tf_ssb_db_identity_get_private_key(ssb, user, identity, private_key, sizeof(private_key)))
	{
		JSContext* context = tf_ssb_get_context(ssb);
		int32_t sequence = 0;
		char previous[k_id_base64_len] = { 0 };
		sqlite3* db = tf_ssb_acquire_db_reader(ssb);
		tf_ssb_db_get_latest_message_by_author(db, NULL, identity, &sequence, previous, sizeof(previous));
		tf_ssb_release_db_reader(ssb, db);
		JSValue content_value = JS_ParseJSON(context, content, strlen(content), NULL);
		if (!JS_IsException(content_value))
		{
			JSValue message = tf_ssb_sign_message(ssb, identity, private_key, content_value, previous, sequence);
			JSValue message_value = JS_JSONStringify(context, message, JS_NULL, JS_NULL);
			const char* message_str = JS_ToCString(context, message_value);
			tf_printf("Posting: %s.\n", message_str);
			tf_ssb_verify_strip_and_store_message(ssb, message, _tf_published_callback, &result);
			JS_FreeCString(context, message_str);
			JS_FreeValue(context, message_value);
			JS_FreeValue(context, message);
		}
		else
		{
			tf_printf("Unable to parse content as JSON: ");
			tf_util_report_error(context, content_value);
		}
		JS_FreeValue(context, content_value);
	}
	else
	{
		tf_printf("Did not find private key for identity %s belonging to %s.\n", identity, user);
	}

	if (free_user)
	{
		tf_free((void*)user);
	}

	tf_ssb_destroy(ssb);
	tf_free((void*)default_db_path);
	return result;
}

static int _tf_command_private(const char* file, int argc, char* argv[])
{
	const char* user = NULL;
	const char* identity = NULL;
	const char* default_db_path = _get_db_path();
	const char* db_path = default_db_path;
	const char* text = NULL;
	const char* recipients = NULL;
	bool show_usage = false;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "user", required_argument, NULL, 'u' },
			{ "id", required_argument, NULL, 'i' },
			{ "recipients", required_argument, NULL, 'r' },
			{ "db-path", required_argument, NULL, 'd' },
			{ "text", required_argument, NULL, 't' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "u:i:d:t:r:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'u':
			user = optarg;
			break;
		case 'i':
			identity = optarg;
			break;
		case 'd':
			db_path = optarg;
			break;
		case 't':
			text = optarg;
			break;
		case 'r':
			recipients = optarg;
			break;
		}
	}

	if (show_usage || !identity || !recipients || !text)
	{
		tf_printf("\nUsage: %s private [options]\n\n", file);
		tf_printf("%s\n\n", _description("private"));
		tf_printf("options:\n");
		tf_printf("  -u, --user user              User owning identity with which to publish (optional).\n");
		tf_printf("  -i, --id identity            Identity with which to publish message.\n");
		tf_printf("  -r, --recipients recipients  Recipient identities.\n");
		tf_printf("  -d, --db-path db_path        SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -t, --text text              Private post text.\n");
		tf_printf("  -h, --help                   Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	int result = EXIT_FAILURE;
	tf_printf("Posting %s as account %s belonging to %s...\n", text, identity, user);
	_create_directories_for_file(db_path, 0700);
	tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
	tf_ssb_set_quiet(ssb, true);
	uint8_t private_key[512] = { 0 };
	const char* recipient_list[k_max_private_message_recipients] = { 0 };
	int recipient_count = 0;

	recipient_list[recipient_count++] = identity;

	bool free_user = false;
	if (!user)
	{
		user = tf_ssb_db_get_user_for_identity(ssb, identity);
		free_user = true;
	}

	if (tf_ssb_db_identity_get_private_key(ssb, user, identity, private_key, sizeof(private_key)))
	{
		char* copy = tf_strdup(recipients);
		char* next = NULL;
		const char* it = strtok_r(copy, ",", &next);
		while (it)
		{
			if (recipient_count == k_max_private_message_recipients)
			{
				tf_printf("Too many recipients (max %d).\n", k_max_private_message_recipients);
				goto done;
			}
			recipient_list[recipient_count++] = it;
			it = strtok_r(NULL, ",", &next);
		}

		JSContext* context = tf_ssb_get_context(ssb);
		JSValue message = JS_NewObject(context);
		JS_SetPropertyStr(context, message, "type", JS_NewString(context, "post"));
		JS_SetPropertyStr(context, message, "text", JS_NewString(context, text));
		JSValue recps = JS_NewArray(context);
		for (int i = 0; i < recipient_count; i++)
		{
			JS_SetPropertyUint32(context, recps, i, JS_NewString(context, recipient_list[i]));
		}
		JS_SetPropertyStr(context, message, "recps", recps);
		JSValue json = JS_JSONStringify(context, message, JS_NULL, JS_NULL);
		const char* message_str = JS_ToCString(context, json);
		char* encrypted = tf_ssb_private_message_encrypt(private_key, recipient_list, recipient_count, message_str, strlen(message_str));
		if (encrypted)
		{
			int32_t sequence = 0;
			char previous[k_id_base64_len] = { 0 };
			sqlite3* db = tf_ssb_acquire_db_reader(ssb);
			tf_ssb_db_get_latest_message_by_author(db, NULL, identity, &sequence, previous, sizeof(previous));
			tf_ssb_release_db_reader(ssb, db);

			JSValue content = JS_NewString(context, encrypted);
			JSValue to_publish = tf_ssb_sign_message(ssb, identity, private_key, content, previous, sequence);
			tf_ssb_verify_strip_and_store_message(ssb, to_publish, _tf_published_callback, &result);
			JS_FreeValue(context, to_publish);
			JS_FreeValue(context, content);
		}
		tf_free(encrypted);
		JS_FreeCString(context, message_str);
		JS_FreeValue(context, json);
		JS_FreeValue(context, message);
		tf_free(copy);
	}
	else
	{
		tf_printf("Did not find private key for identity %s belonging to %s.\n", identity, user);
	}

	if (free_user)
	{
		tf_free((void*)user);
	}
done:
	tf_ssb_destroy(ssb);
	tf_free((void*)default_db_path);
	return result;
}

static int _tf_command_store_blob(const char* file, int argc, char* argv[])
{
	const char* default_db_path = _get_db_path();
	const char* db_path = default_db_path;
	const char* file_path = NULL;
	bool show_usage = false;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "db-path", required_argument, NULL, 'd' },
			{ "file", required_argument, NULL, 'f' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "d:f:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'd':
			db_path = optarg;
			break;
		case 'f':
			file_path = optarg;
			break;
		}
	}

	if (show_usage || !file_path)
	{
		tf_printf("\nUsage: %s store_blob [options]\n\n", file);
		tf_printf("%s\n\n", _description("store_blob"));
		tf_printf("options:\n");
		tf_printf("  -d, --db-path db_path    SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -f, --file file_path     Path to file to add to the blob store.\n");
		tf_printf("  -h, --help               Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	char* data = NULL;
	size_t size = 0;
	FILE* blob_file = fopen(file_path, "rb");
	if (!blob_file)
	{
		tf_printf("Failed to open %s: %s.\n", file_path, strerror(errno));
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	char buffer[16 * 1024];
	while (true)
	{
		size_t bytes = fread(buffer, 1, sizeof(buffer), blob_file);
		if (bytes > 0)
		{
			data = tf_resize_vec(data, size + bytes);
			memcpy(data + size, buffer, bytes);
			size += bytes;
		}
		else
		{
			break;
		}
	}
	if (ferror(blob_file))
	{
		tf_printf("Failed to read %s: %s.\n", file_path, strerror(errno));
		fclose(blob_file);
		tf_free(data);
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}
	fclose(blob_file);

	char id[256];
	_create_directories_for_file(db_path, 0700);
	tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
	tf_ssb_set_quiet(ssb, true);
	if (tf_ssb_db_blob_store(ssb, (const uint8_t*)data, size, id, sizeof(id), NULL))
	{
		tf_printf("%s\n", id);
	}
	tf_ssb_destroy(ssb);
	tf_free(data);
	tf_free((void*)default_db_path);
	return EXIT_SUCCESS;
}

static int _tf_command_get_blob(const char* file, int argc, char* argv[])
{
	const char* default_db_path = _get_db_path();
	const char* db_path = default_db_path;
	const char* output = NULL;
	const char* blob_id = NULL;
	bool show_usage = false;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "db-path", required_argument, NULL, 'd' },
			{ "blob", required_argument, NULL, 'b' },
			{ "output", required_argument, NULL, 'o' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "d:b:o:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'd':
			db_path = optarg;
			break;
		case 'b':
			blob_id = optarg;
			break;
		case 'o':
			output = optarg;
			break;
		}
	}

	if (show_usage || !blob_id)
	{
		tf_printf("\nUsage: %s get_blob [options]\n\n", file);
		tf_printf("%s\n\n", _description("get_blob"));
		tf_printf("options:\n");
		tf_printf("  -d, --db-path db_path    SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -b, --blob blob_id       Blob identifier to retrieve.\n");
		tf_printf("  -o, --output file_path   Location to write the retrieved blob.\n");
		tf_printf("  -h, --help               Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	uint8_t* blob = NULL;
	size_t size = 0;

	_create_directories_for_file(db_path, 0700);
	tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
	tf_ssb_set_quiet(ssb, true);
	bool fetched = tf_ssb_db_blob_get(ssb, blob_id, &blob, &size);
	tf_ssb_destroy(ssb);

	if (!fetched)
	{
		tf_printf("Failed to fetch blob: %s.\n", blob_id);
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	if (output)
	{
		FILE* blob_file = fopen(output, "wb");
		if (!blob_file)
		{
			tf_printf("Failed to open %s: %s.\n", output, strerror(errno));
			tf_free((void*)default_db_path);
			tf_free(blob);
			return EXIT_FAILURE;
		}

		if (fwrite(blob, 1, size, blob_file) != size)
		{
			tf_printf("Failed to write %s: %s\n", output, strerror(errno));
			tf_free(blob);
			tf_free((void*)default_db_path);
			fclose(blob_file);
			return EXIT_FAILURE;
		}

		fclose(blob_file);
	}
	else
	{
		if (fwrite(blob, 1, size, stdout) != size)
		{
			tf_free(blob);
			tf_free((void*)default_db_path);
			return EXIT_FAILURE;
		}
	}

	tf_free(blob);
	tf_free((void*)default_db_path);
	return EXIT_SUCCESS;
}

static int _tf_command_has_blob(const char* file, int argc, char* argv[])
{
	const char* default_db_path = _get_db_path();
	const char* db_path = default_db_path;
	const char* blob_id = NULL;
	bool show_usage = false;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "db-path", required_argument, NULL, 'd' },
			{ "blob_id", required_argument, NULL, 'b' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "d:b:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'd':
			db_path = optarg;
			break;
		case 'b':
			blob_id = optarg;
			break;
		}
	}

	if (show_usage || !blob_id)
	{
		tf_printf("\nUsage: %s has_blob [options]\n\n", file);
		tf_printf("%s\n\n", _description("has_blob"));
		tf_printf("options:\n");
		tf_printf("  -d, --db-path db_path    SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -b, --blob_id blob_id    ID of blob to query.\n");
		tf_printf("  -h, --help               Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	sqlite3* db = NULL;
	sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, NULL);
	tf_ssb_db_init_reader(db);
	bool has = tf_ssb_db_blob_has(db, blob_id);
	sqlite3_close(db);
	tf_free((void*)default_db_path);
	tf_printf("%s\n", has ? "true" : "false");
	return has ? EXIT_SUCCESS : EXIT_FAILURE;
}

static int _tf_command_create_invite(const char* file, int argc, char* argv[])
{
	const char* default_db_path = _get_db_path();
	const char* db_path = default_db_path;
	const char* identity = NULL;
	int use_count = 1;
	int expires = 3600;
	bool show_usage = false;
	const char* host = NULL;
	int port = 0;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "db-path", required_argument, NULL, 'd' },
			{ "identity", required_argument, NULL, 'i' },
			{ "address", required_argument, NULL, 'a' },
			{ "port", required_argument, NULL, 'p' },
			{ "use_count", required_argument, NULL, 'u' },
			{ "expires", required_argument, NULL, 'e' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "d:i:a:p:u:e:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'd':
			db_path = optarg;
			break;
		case 'i':
			identity = optarg;
			break;
		case 'a':
			host = optarg;
			break;
		case 'p':
			port = atoi(optarg);
			break;
		case 'u':
			use_count = atoi(optarg);
			break;
		case 'e':
			expires = atoi(optarg);
			break;
		}
	}

	if (show_usage || !identity || !use_count || !expires || !host || !port)
	{
		tf_printf("\nUsage: %s create_invite [options]\n\n", file);
		tf_printf("%s\n\n", _description("create_invite"));
		tf_printf("options:\n");
		tf_printf("  -d, --db-path db_path    SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -i, --identity identity  Account from which to get latest sequence number.\n");
		tf_printf("  -a, --address address    Address to which the recipient will connect.\n");
		tf_printf("  -p, --port port          Port to which the recipient will connect.\n");
		tf_printf("  -u, --use_count count    Number of times this invite may be used (default: 1).\n");
		tf_printf("  -e, --expires seconds    How long this invite is valid in seconds (-1 for indefinitely, default: 1 hour).\n");
		tf_printf("  -h, --help               Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	int result = EXIT_FAILURE;
	tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
	tf_ssb_set_quiet(ssb, true);
	char invite[1024] = "";
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	if (tf_ssb_db_generate_invite(db, identity, host, port, use_count, expires, invite, sizeof(invite)))
	{
		tf_printf("%s\n", invite);
		result = EXIT_SUCCESS;
	}
	tf_ssb_release_db_writer(ssb, db);
	tf_ssb_destroy(ssb);
	tf_free((void*)default_db_path);
	return result;
}

static int _tf_command_get_sequence(const char* file, int argc, char* argv[])
{
	const char* default_db_path = _get_db_path();
	const char* db_path = default_db_path;
	const char* identity = NULL;
	bool show_usage = false;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "db-path", required_argument, NULL, 'd' },
			{ "id", required_argument, NULL, 'i' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "d:i:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'd':
			db_path = optarg;
			break;
		case 'i':
			identity = optarg;
			break;
		}
	}

	if (show_usage || !identity)
	{
		tf_printf("\nUsage: %s get_sequence [options]\n\n", file);
		tf_printf("%s\n\n", _description("get_sequence"));
		tf_printf("options:\n");
		tf_printf("  -d, --db-path db_path    SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -i, --identity identity  Account from which to get latest sequence number.\n");
		tf_printf("  -h, --help               Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
	tf_ssb_set_quiet(ssb, true);
	int32_t sequence = -1;

	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	int result = tf_ssb_db_get_latest_message_by_author(db, NULL, identity, &sequence, NULL, 0) ? EXIT_SUCCESS : EXIT_FAILURE;
	tf_ssb_release_db_reader(ssb, db);
	tf_printf("%d\n", sequence);
	tf_ssb_destroy(ssb);
	tf_free((void*)default_db_path);
	return result;
}

static int _tf_command_get_identity(const char* file, int argc, char* argv[])
{
	const char* default_db_path = _get_db_path();
	const char* db_path = default_db_path;
	bool show_usage = false;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "db-path", required_argument, NULL, 'd' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "d:i:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'd':
			db_path = optarg;
			break;
		}
	}

	if (show_usage)
	{
		tf_printf("\nUsage: %s get_identity [options]\n\n", file);
		tf_printf("%s\n\n", _description("get_identity"));
		tf_printf("options:\n");
		tf_printf("  -d, --db-path db_path    SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -h, --help               Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	char id[k_id_base64_len] = { 0 };
	tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
	tf_ssb_set_quiet(ssb, true);
	int result = tf_ssb_whoami(ssb, id, sizeof(id)) ? EXIT_SUCCESS : EXIT_FAILURE;
	tf_printf("%s\n", id);
	tf_ssb_destroy(ssb);
	tf_free((void*)default_db_path);
	return result;
}

static int _tf_command_get_profile(const char* file, int argc, char* argv[])
{
	const char* default_db_path = _get_db_path();
	const char* db_path = default_db_path;
	const char* identity = NULL;
	bool show_usage = false;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "db-path", required_argument, NULL, 'd' },
			{ "id", required_argument, NULL, 'i' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "d:i:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'd':
			db_path = optarg;
			break;
		case 'i':
			identity = optarg;
			break;
		}
	}

	if (show_usage || !identity)
	{
		tf_printf("\nUsage: %s get_profile [options]\n\n", file);
		tf_printf("%s\n\n", _description("get_profile"));
		tf_printf("options:\n");
		tf_printf("  -d, --db-path db_path    SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -i, --identity identity  Account for which to get profile information.\n");
		tf_printf("  -h, --help               Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	sqlite3* db = NULL;
	sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, NULL);
	tf_ssb_db_init_reader(db);
	const char* profile = tf_ssb_db_get_profile(db, identity);
	tf_printf("%s\n", profile);
	sqlite3_close(db);
	tf_free((void*)profile);
	tf_free((void*)default_db_path);
	return profile != NULL ? EXIT_SUCCESS : EXIT_FAILURE;
}

static int _tf_command_get_contacts(const char* file, int argc, char* argv[])
{
	const char* default_db_path = _get_db_path();
	const char* db_path = default_db_path;
	const char* identity = NULL;
	bool show_usage = false;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "db-path", required_argument, NULL, 'd' },
			{ "id", required_argument, NULL, 'i' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "d:i:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'd':
			db_path = optarg;
			break;
		case 'i':
			identity = optarg;
			break;
		}
	}

	if (show_usage || !identity)
	{
		tf_printf("\nUsage: %s get_contacts [options]\n\n", file);
		tf_printf("%s\n\n", _description("get_contacts"));
		tf_printf("options:\n");
		tf_printf("  -d, --db-path db_path    SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -i, --identity identity  Account from which to get contact information.\n");
		tf_printf("  -h, --help               Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
	tf_ssb_set_quiet(ssb, true);
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue contacts = JS_NewObject(context);
	JSValue follows = JS_NewObject(context);
	JSValue blocks = JS_NewObject(context);
	JSValue friends = JS_NewObject(context);
	tf_ssb_following_t* following = tf_ssb_db_following_deep(ssb, &identity, 1, 1, true);
	tf_ssb_following_t* following2 = tf_ssb_db_following_deep(ssb, &identity, 1, 2, false);
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	for (int i = 0; *following[i].id; i++)
	{
		if (following[i].followed_by_count)
		{
			const char* name = tf_ssb_db_get_profile_name(db, following[i].id);
			JS_SetPropertyStr(context, follows, following[i].id, name ? JS_NewString(context, name) : JS_NULL);
			tf_free((void*)name);
		}
		if (following[i].blocked_by_count)
		{
			const char* name = tf_ssb_db_get_profile_name(db, following[i].id);
			JS_SetPropertyStr(context, blocks, following[i].id, name ? JS_NewString(context, name) : JS_NULL);
			tf_free((void*)name);
		}
	}
	for (int i = 0; *following2[i].id; i++)
	{
		const char* name = tf_ssb_db_get_profile_name(db, following2[i].id);
		JS_SetPropertyStr(context, friends, following2[i].id, name ? JS_NewString(context, name) : JS_NULL);
		tf_free((void*)name);
	}
	tf_ssb_release_db_reader(ssb, db);
	JS_SetPropertyStr(context, contacts, "follows", follows);
	JS_SetPropertyStr(context, contacts, "blocks", blocks);
	JS_SetPropertyStr(context, contacts, "friends", friends);
	tf_free(following2);
	tf_free(following);
	JSValue json = JS_JSONStringify(context, contacts, JS_NULL, JS_NewInt32(context, 2));
	const char* json_str = JS_ToCString(context, json);
	tf_printf("%s\n", json_str);
	JS_FreeCString(context, json_str);
	JS_FreeValue(context, json);
	JS_FreeValue(context, contacts);
	tf_ssb_destroy(ssb);
	tf_free((void*)default_db_path);
	return EXIT_SUCCESS;
}

static int _tf_command_verify(const char* file, int argc, char* argv[])
{
	const char* identity = NULL;
	const char* default_db_path = _get_db_path();
	const char* db_path = default_db_path;
	int32_t sequence = 0;
	bool show_usage = false;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "id", required_argument, NULL, 'i' },
			{ "sequence", required_argument, NULL, 's' },
			{ "db-path", required_argument, NULL, 'd' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "i:s:d:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'i':
			identity = optarg;
			break;
		case 's':
			sequence = atoi(optarg);
			break;
		case 'd':
			db_path = optarg;
			break;
		}
	}

	if (show_usage)
	{
		tf_printf("\nUsage: %s verify [options]\n\n", file);
		tf_printf("%s\n\n", _description("verify"));
		tf_printf("options:\n");
		tf_printf("  -i, --identity identity  Identity to verify.\n");
		tf_printf("  -s, --sequence sequence  Sequence number to debug.\n");
		tf_printf("  -d, --db-path db_path    SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -h, --help               Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	bool verified = false;
	tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
	if (identity)
	{
		tf_printf("Verifying %s...\n", identity);
		verified = tf_ssb_db_verify(ssb, identity, sequence, true);
	}
	else
	{
		sqlite3* db = tf_ssb_acquire_db_reader(ssb);
		sqlite3_stmt* statement = NULL;
		if (sqlite3_prepare_v2(db, "SELECT DISTINCT author FROM messages ORDER BY author", -1, &statement, NULL) == SQLITE_OK)
		{
			verified = true;
			while (sqlite3_step(statement) == SQLITE_ROW)
			{
				const char* identity = (const char*)sqlite3_column_text(statement, 0);
				tf_printf("Verifying %s...", identity);
				if (tf_ssb_db_verify(ssb, identity, sequence, true))
				{
					tf_printf("success.\n");
				}
				else
				{
					tf_printf("fail.\n");
					verified = false;
				}
			}
			sqlite3_finalize(statement);
		}
		tf_ssb_release_db_reader(ssb, db);
	}
	tf_ssb_destroy(ssb);
	tf_free((void*)default_db_path);
	return verified ? EXIT_SUCCESS : EXIT_FAILURE;
}
#endif

typedef struct tf_run_args_t
{
	const char* script;
	const char* network_key;
	const char* db_path;
	int count;
	const char* args;
	const char* zip;
	bool one_proc;
	bool verbose;
	bool help;
} tf_run_args_t;

typedef struct _tf_run_thread_data_t
{
	tf_run_args_t args;
	int index;
	int result;
} tf_run_thread_data_t;

static int _tf_run_task(const tf_run_args_t* args, int index)
{
	int result = -1;
	tf_task_t* task = tf_task_create();
	tf_task_set_trusted(task, true);
	tf_printf("Zip path: %s\n", args->zip);
	tf_task_set_zip_path(task, args->zip);
	tf_task_set_ssb_network_key(task, args->network_key);
	tf_task_set_args(task, args->args);
	tf_task_set_one_proc(task, args->one_proc);
	const char* db_path = args->db_path;
	char db_path_buffer[256];
	if (index)
	{
		snprintf(db_path_buffer, sizeof(db_path_buffer), "%s.%d", args->db_path, index);
		db_path = db_path_buffer;
	}

	char* cwd = NULL;
	if (!args->zip)
	{
		size_t cwd_size = 1;
		uv_cwd((char[1]) { 0 }, &cwd_size);
		cwd = alloca(cwd_size);
		if (uv_cwd(cwd, &cwd_size) == 0)
		{
			size_t test_path_size = cwd_size + strlen("/core/index.html");
			char* test_path = alloca(test_path_size);

			uv_loop_t* loop = tf_task_get_loop(task);
			uv_fs_t req = { 0 };
			while (true)
			{
				snprintf(test_path, test_path_size, "%s/core/index.html", cwd);
				int r = uv_fs_access(loop, &req, test_path, 0000, NULL);
				uv_fs_req_cleanup(&req);
				if (r != UV_ENOENT)
				{
					break;
				}
				char* slash = strrchr(cwd, '/');
				if (slash)
				{
					*slash = '\0';
				}
				else
				{
					break;
				}
			}
			tf_printf("Root path: %s\n", cwd);
		}
		if (!*cwd)
		{
			cwd = NULL;
		}
	}

	tf_task_set_db_path(task, db_path);
	tf_task_activate(task);
	tf_ssb_set_verbose(tf_task_get_ssb(task), args->verbose);
	tf_ssb_start_periodic(tf_task_get_ssb(task));
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	int64_t http_port = 0;
	char out_http_port_file[512] = "";
	tf_ssb_db_get_global_setting_int64(db, "http_port", &http_port);
	tf_ssb_db_get_global_setting_string(db, "out_http_port_file", out_http_port_file, sizeof(out_http_port_file));
	tf_ssb_release_db_reader(ssb, db);
	if (http_port || *out_http_port_file)
	{
		if (args->zip)
		{
			tf_ssb_import_from_zip(tf_task_get_ssb(task), args->zip, "core", "apps");
		}
		else if (cwd)
		{
			size_t apps_path_size = strlen(cwd) + strlen("/apps") + 1;
			char* apps_path = alloca(apps_path_size);
			snprintf(apps_path, apps_path_size, "%s/apps", cwd);
			tf_ssb_import(tf_task_get_ssb(task), "core", apps_path);
		}
		else
		{
			tf_ssb_import(tf_task_get_ssb(task), "core", "apps");
		}
	}
	tf_ssb_set_main_thread(tf_task_get_ssb(task), true);
	const char* script = args->script;
	tf_task_set_root_path(task, cwd);
	if (!script || tf_task_execute(task, script))
	{
		tf_task_run(task);
		result = 0;
	}
	tf_task_destroy(task);
	tf_printf("_tf_run_task is done.  Goodbye.\n");
	return result;
}

static void _tf_run_task_thread(void* data)
{
	tf_run_thread_data_t* info = data;
	info->result = _tf_run_task(&info->args, info->index);
}

#if !TARGET_OS_IPHONE
static void _shed_privileges()
{
#if !defined(_WIN32) && !defined(__HAIKU__)
	struct rlimit zeroLimit;
	zeroLimit.rlim_cur = 0;
	zeroLimit.rlim_max = 0;

	// RLIMIT_AS
	// RLIMIT_CORE
	// RLIMIT_CPU
	// RLIMIT_DATA
	// RLIMIT_FSIZE
	// RLIMIT_RSS
	// RLIMIT_RTPRIO
	// RLIMIT_RTTIME
	// RLIMIT_SIGPENDING
	// RLIMIT_STACK

	if (setrlimit(RLIMIT_FSIZE, &zeroLimit) != 0)
	{
		perror("setrlimit(RLIMIT_FSIZE, {0, 0})");
		exit(EXIT_FAILURE);
	}
	if (setrlimit(RLIMIT_NOFILE, &zeroLimit) != 0)
	{
		perror("setrlimit(RLIMIT_NOFILE, {0, 0})");
		exit(EXIT_FAILURE);
	}
	if (setrlimit(RLIMIT_NPROC, &zeroLimit) != 0)
	{
		perror("setrlimit(RLIMIT_NPROC, {0, 0})");
		exit(EXIT_FAILURE);
	}
#if !defined(__MACH__) && !defined(__OpenBSD__)
	if (setrlimit(RLIMIT_LOCKS, &zeroLimit) != 0)
	{
		perror("setrlimit(RLIMIT_LOCKS, {0, 0})");
		exit(EXIT_FAILURE);
	}
	if (setrlimit(RLIMIT_MSGQUEUE, &zeroLimit) != 0)
	{
		perror("setrlimit(RLIMIT_MSGQUEUE, {0, 0})");
		exit(EXIT_FAILURE);
	}
#endif
#endif

#if defined(__OpenBSD__)
	/* How do I unveil nothing? */
	if (unveil("/dev/null", "r") || unveil(NULL, NULL))
	{
		perror("unveil");
		exit(EXIT_FAILURE);
	}
	if (pledge("stdio unveil", NULL))
	{
		perror("pledge");
		exit(EXIT_FAILURE);
	}
#endif
}

static int _tf_command_run(const char* file, int argc, char* argv[])
{
	const char* default_db_path = _get_db_path();
	tf_run_args_t args = {
		.count = 1,
		.db_path = default_db_path,
	};
	bool show_usage = false;

	/* Check if the executable has data attached. */
	unzFile zip = unzOpen(file);
	if (zip)
	{
		args.zip = file;
		unzClose(zip);
	}

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "script", required_argument, NULL, 's' },
			{ "ssb-network-key", required_argument, NULL, 'k' },
			{ "db-path", required_argument, NULL, 'd' },
			{ "count", required_argument, NULL, 'n' },
			{ "args", required_argument, NULL, 'a' },
			{ "one-proc", no_argument, NULL, 'o' },
			{ "zip", required_argument, NULL, 'z' },
			{ "verbose", no_argument, NULL, 'v' },
			{ "help", no_argument, NULL, 'h' },
		};
		int c = getopt_long(argc, argv, "s:k:d:n:a:oz:vh", k_options, NULL);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 's':
			args.script = optarg;
			break;
		case 'k':
			args.network_key = optarg;
			break;
		case 'd':
			args.db_path = optarg;
			break;
		case 'n':
			args.count = atoi(optarg);
			break;
		case 'a':
			args.args = optarg;
			break;
		case 'o':
			args.one_proc = true;
			break;
		case 'z':
			args.zip = optarg;
			break;
		case 'v':
			args.verbose = true;
			break;
		}
	}

	for (int i = optind; i < argc; i++)
	{
		tf_printf("Unexpected argument: %s\n", argv[i]);
		show_usage = true;
	}

	if (show_usage)
	{
		tf_printf("\nUsage: %s run [options]\n\n", file);
#if !defined(__ANDROID__)
		tf_printf("%s\n\n", _description("run"));
#endif
		tf_printf("options:\n");
		tf_printf("  -s, --script script        Script to run (default: none).\n");
		tf_printf("  -d, --db-path path         SQLite database path (default: %s).\n", default_db_path);
		tf_printf("  -k, --ssb-network-key key  SSB network key to use.\n");
		tf_printf("  -n, --count count          Number of instances to run.\n");
		tf_printf("  -a, --args args            Arguments of the format key=value,foo=bar,verbose=true (note: these are persisted to the database).\n");
		tf_util_document_settings("                                 ");
		tf_printf("  -o, --one-proc             Run everything in one process (unsafely!).\n");
		tf_printf("  -z, --zip path             Zip archive from which to load files.\n");
		tf_printf("  -v, --verbose              Log raw messages.\n");
		tf_printf("  -h, --help                 Show this usage information.\n");
		tf_free((void*)default_db_path);
		return EXIT_FAILURE;
	}

	int result = 0;
#if !defined(_WIN32) && !defined(__MACH__)
	setpgid(0, 0);
#endif

	_create_directories_for_file(args.db_path, 0700);
	uv_thread_t* threads = tf_malloc(sizeof(uv_thread_t) * args.count);
	tf_run_thread_data_t* data = tf_malloc(sizeof(tf_run_thread_data_t) * args.count);
#if defined(__ANDROID__)
	if (args.count == 1)
	{
		result = _tf_run_task(&args, 0);
	}
	else
#endif
	{
		for (int i = 0; i < args.count; i++)
		{
			data[i] = (tf_run_thread_data_t) {
				.args = args,
				.index = i,
			};
			uv_thread_options_t options = {
				.flags = UV_THREAD_HAS_STACK_SIZE,
				.stack_size = 1024 * 1024,
			};
			uv_thread_create_ex(&threads[i], &options, _tf_run_task_thread, &data[i]);
		}
	}
	for (int i = 0; i < args.count; i++)
	{
		uv_thread_join(&threads[i]);
		if (data[i].result != 0)
		{
			result = data[i].result;
		}
	}
	tf_free(data);
	tf_free(threads);
	tf_free((void*)default_db_path);
	return result;
}

static int _tf_command_sandbox(const char* file, int argc, char* argv[])
{
	bool show_usage = false;
	int fd = STDIN_FILENO;

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "fd", required_argument, NULL, 'f' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "f:h", k_options, NULL);
		if (c == -1)
		{
			break;
		}
		switch (c)
		{
		case '?':
		case 'h':
		default:
			show_usage = true;
			break;
		case 'f':
			fd = atoi(optarg);
			break;
		}
	}

	if (show_usage)
	{
		tf_printf("\nUsage: %s sandbox [options]\n\n", file);
#if !defined(__ANDROID__)
		tf_printf("%s\n\n", _description("sandbox"));
#endif
		tf_printf("options:\n");
		tf_printf("  -h, --help    Show this usage information.\n");
		tf_printf("  -f, --fd      File descriptor with which to communicate with parent process.\n");
		return EXIT_FAILURE;
	}

#if defined(__linux__)
	prctl(PR_SET_PDEATHSIG, SIGHUP);
#endif
	tf_task_t* task = tf_task_create();
	tf_task_configure_from_fd(task, fd);
	_shed_privileges();
	/* The caller will trigger tf_task_activate with a message. */
	tf_task_run(task);
	tf_task_destroy(task);
	return EXIT_SUCCESS;
}

#if !defined(__ANDROID__)
static int _tf_command_usage(const char* file)
{
	tf_printf("Usage: %s command [command-options]\n", file);
	tf_printf("commands:\n");
	for (int i = 0; i < tf_countof(k_commands); i++)
	{
		tf_printf("  %s - %s\n", k_commands[i].name, k_commands[i].description);
	}
	return -1;
}
#endif
#endif

static void _backtrace_error(void* data, const char* message, int errnum)
{
	tf_printf("libbacktrace error %d: %s\n", errnum, message);
}

static void _error_handler(int sig)
{
	const char* stack = tf_util_backtrace_string();
	tf_printf("ERROR:\n%s\n", stack);
	tf_free((void*)stack);
	_exit(EXIT_FAILURE);
}

#if defined(_WIN32)
static LONG WINAPI _win32_exception_handler(EXCEPTION_POINTERS* info)
{
	if (info->ExceptionRecord->ExceptionCode == STATUS_ACCESS_VIOLATION || info->ExceptionRecord->ExceptionCode == STATUS_ILLEGAL_INSTRUCTION ||
		info->ExceptionRecord->ExceptionCode == STATUS_STACK_OVERFLOW || info->ExceptionRecord->ExceptionCode == STATUS_HEAP_CORRUPTION)
	{
		const char* stack = tf_util_backtrace_string();
		tf_printf("ERROR:\n%s\n", stack);
		tf_free((void*)stack);
		_exit(EXIT_FAILURE);
	}
	return EXCEPTION_CONTINUE_SEARCH;
}
#endif

static void _startup(int argc, char* argv[])
{
	char buffer[8] = { 0 };
	size_t buffer_size = sizeof(buffer);
	bool tracking = uv_os_getenv("TF_MEM_TRACKING", buffer, &buffer_size) == 0 && strcmp(buffer, "1") == 0;
	for (int i = 1; i < argc; i++)
	{
		if (strcmp(argv[i], "sandbox") == 0)
		{
			tracking = false;
		}
	}

	tf_mem_startup(tracking);
	g_backtrace_state = backtrace_create_state(argv[0], 0, _backtrace_error, NULL);

#if defined(__linux__)
	prctl(PR_SET_PDEATHSIG, SIGKILL);
#endif
	tf_mem_replace_uv_allocator();
	tf_mem_replace_sqlite_allocator();
	uv_setup_args(argc, argv);
	tf_taskstub_startup();

#if !defined(_WIN32)
	if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
	{
		perror("signal");
	}
#endif

	bool use_error_handler = false;
#if defined(__ANDROID__) || defined(_WIN32)
	use_error_handler = true;
#endif
	if (use_error_handler)
	{
		if (
#if !defined(_WIN32)
			signal(SIGSYS, _error_handler) == SIG_ERR ||
#endif
			signal(SIGABRT, _error_handler) == SIG_ERR || signal(SIGSEGV, _error_handler) == SIG_ERR)
		{
			perror("signal");
		}
	}

#if defined(_WIN32)
	AddVectoredExceptionHandler(0, _win32_exception_handler);
#endif
}

#if defined(__ANDROID__)
static JNIEnv* s_jni_env;

static void _tf_service_start(int pipe_fd)
{
	tf_printf("_tf_service_start\n");
	jclass c = (*s_jni_env)->FindClass(s_jni_env, "com/unprompted/tildefriends/TildeFriendsActivity");
	jmethodID start_sandbox = (*s_jni_env)->GetStaticMethodID(s_jni_env, c, "start_sandbox", "(I)V");
	(*s_jni_env)->CallStaticVoidMethod(s_jni_env, c, start_sandbox, pipe_fd);
}

static void _tf_service_stop()
{
	tf_printf("_tf_service_stop\n");
	jclass c = (*s_jni_env)->FindClass(s_jni_env, "com/unprompted/tildefriends/TildeFriendsActivity");
	jmethodID stop_sandbox = (*s_jni_env)->GetStaticMethodID(s_jni_env, c, "stop_sandbox", "()V");
	(*s_jni_env)->CallStaticVoidMethod(s_jni_env, c, stop_sandbox);
}

static jint _tf_server_main(JNIEnv* env, jobject this_object, jstring files_dir, jstring apk_path, jstring out_port_file_path, jobject connectivity_manager)
{
	s_jni_env = env;

	tf_printf("This is tf_server_main main.\n");
	_startup(0, (char*[]) { NULL });
	tf_printf("That was startup.\n");

	ares_library_init_android(connectivity_manager);
	ares_library_init(0);

	const char* files = (*env)->GetStringUTFChars(env, files_dir, NULL);
	const char* apk = (*env)->GetStringUTFChars(env, apk_path, NULL);
	const char* out_port_file = (*env)->GetStringUTFChars(env, out_port_file_path, NULL);

	tf_printf("FILES = %s\n", files);
	tf_printf("APK = %s\n", apk);
	tf_printf("OUT_PORT = %s\n", out_port_file);

	int result = uv_chdir(files);
	if (result)
	{
		char buffer[256];
		tf_printf("uv_chdir: %s\n", uv_strerror_r(result, buffer, sizeof(buffer)));
	}

	size_t args_length = strlen(out_port_file) + strlen("--args=http_port=0,out_http_port_file=") + 1;
	char* args_buffer = alloca(args_length);
	snprintf(args_buffer, args_length, "--args=http_port=0,out_http_port_file=%s", out_port_file);

	const char* args[] = {
		"run",
		"-z",
		apk,
		args_buffer,
	};

	tf_task_set_android_service_callbacks(_tf_service_start, _tf_service_stop);
	result = _tf_command_run(apk, tf_countof(args), (char**)args);
	tf_task_set_android_service_callbacks(NULL, NULL);

	(*env)->ReleaseStringUTFChars(env, files_dir, files);
	(*env)->ReleaseStringUTFChars(env, apk_path, apk);
	(*env)->ReleaseStringUTFChars(env, out_port_file_path, out_port_file);

	ares_library_cleanup();
	tf_mem_shutdown();
	tf_printf("tf_server_main finished with %d.", result);

	s_jni_env = NULL;
	return result;
}

static jint _tf_sandbox_main(JNIEnv* env, jobject this_object, int pipe_fd)
{
	s_jni_env = env;

	tf_printf("This is tf_sandbox_main main (fd=%d).\n", pipe_fd);
	_startup(0, (char*[]) { NULL });
	tf_printf("That was startup.\n");

	char fd[32] = { 0 };
	snprintf(fd, sizeof(fd), "%d", pipe_fd);
	const char* args[] = {
		"sandbox",
		"-f",
		fd,
	};

	int result = _tf_command_sandbox(NULL, tf_countof(args), (char**)args);

	tf_mem_shutdown();
	tf_printf("tf_sandbox_main finished with %d.", result);

	s_jni_env = NULL;
	return result;
}

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
	tf_printf("JNI_Onload called.\n");
	JNIEnv* env;
	if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK)
	{
		tf_printf("Failed to get JNI environment.\n");
		return JNI_ERR;
	}

	tf_printf("Finding class.\n");
	jclass c = (*env)->FindClass(env, "com/unprompted/tildefriends/TildeFriendsActivity");
	if (!c)
	{
		tf_printf("Failed to find TildeFriendsActivity class.\n");
		return JNI_ERR;
	}

	tf_printf("Registering method.\n");
	static const JNINativeMethod methods[] = {
		{ "tf_server_main", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Landroid/net/ConnectivityManager;)I", _tf_server_main },
		{ "tf_sandbox_main", "(I)I", _tf_sandbox_main },
	};
	int result = (*env)->RegisterNatives(env, c, methods, tf_countof(methods));
	if (result != JNI_OK)
	{
		return result;
	}

	ares_library_init_jvm(vm);

	tf_printf("Done.\n");
	return JNI_VERSION_1_6;
}

int main(int argc, char* argv[])
{
	tf_printf("Welcome to Tilde Friends.  This is not the way to run on Android.\n");
	return EXIT_FAILURE;
}
#elif TARGET_OS_IPHONE
void tf_run_thread_start(const char* zip_path)
{
	_startup(0, NULL);
	uv_thread_t* thread = tf_malloc(sizeof(uv_thread_t));
	tf_run_thread_data_t* data = tf_malloc(sizeof(tf_run_thread_data_t));
	tf_run_args_t args = {
		.count = 1,
		.db_path = "db.sqlite",
		.one_proc = true,
		.zip = zip_path,
	};
	*data = (tf_run_thread_data_t) {
		.args = args,
	};
	uv_thread_create(thread, _tf_run_task_thread, data);
}
#else
int main(int argc, char* argv[])
{
#if defined(__linux__)
	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0)
	{
		tf_printf("Unable to set no new privileges flag: %s\n", strerror(errno));
		return EXIT_FAILURE;
	}
#endif
	setvbuf(stdout, NULL, _IONBF, 0);
	_startup(argc, argv);
	ares_library_init(0);

	int result = 0;
	if (argc >= 2)
	{
		for (int i = 0; i < tf_countof(k_commands); i++)
		{
			const command_t* command = &k_commands[i];
			if (strcmp(argv[1], command->name) == 0)
			{
				result = command->callback(argv[0], argc - 1, argv + 1);
				goto done;
			}
		}
		result = _tf_command_usage(argv[0]);
	}
	else
	{
		result = _tf_command_run(argv[0], argc, argv);
	}
done:
	ares_library_cleanup();
	tf_mem_shutdown();
	return result;
}
#endif
