#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 "backtrace.h"
#include "sqlite3.h"
#include "unzip.h"

#include <getopt.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>
#endif

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

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

#if !defined(_countof)
#define _countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
#endif

struct backtrace_state* g_backtrace_state;

const char* k_db_path_default = "db.sqlite";

#if !TARGET_OS_IPHONE && !defined(__ANDROID__)
static int _tf_command_test(const char* file, int argc, char* argv[]);
static int _tf_command_import(const char* file, int argc, char* argv[]);
static int _tf_command_export(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_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 to SSB." },
	{ "export", _tf_command_export, "Export apps from SSB." },
	{ "test", _tf_command_test, "Test SSB." },
};

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

	while (!show_usage)
	{
		static const struct option k_options[] = {
			{ "tests", required_argument, NULL, 't' },
			{ "help", no_argument, NULL, 'h' },
			{ 0 },
		};
		int c = getopt_long(argc, argv, "t: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;
		}
	}

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

	if (show_usage)
	{
		tf_printf("\n%s test [options]\n\n", file);
		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");
		return EXIT_FAILURE;
	}

	tf_tests(&test_options);
	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* db_path = k_db_path_default;
	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("\n%s import [options] [paths...]\n\n", file);
		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", k_db_path_default);
		tf_printf("  -h, --help               Show this usage information.\n");
		return EXIT_FAILURE;
	}

	tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
	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);
	return EXIT_SUCCESS;
}

static int _tf_command_export(const char* file, int argc, char* argv[])
{
	const char* user = "core";
	const char* db_path = k_db_path_default;
	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("\n%s export [options] [paths...]\n\n", file);
		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", k_db_path_default);
		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");
		return EXIT_FAILURE;
	}

	tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
	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 < (int)_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);
	return EXIT_SUCCESS;
}
#endif

typedef struct tf_run_args_t
{
	const char* script;
	const char* network_key;
	int ssb_port;
	int http_port;
	int https_port;
	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("setting zip path to %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_ssb_port(task, args->ssb_port ? args->ssb_port + index : 0);
	tf_task_set_http_port(task, args->http_port ? args->http_port + index : 0);
	tf_task_set_https_port(task, args->https_port ? args->https_port + index : 0);
	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;
	}
	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));
	if (args->http_port || args->https_port)
	{
		if (args->zip)
		{
			tf_ssb_import_from_zip(tf_task_get_ssb(task), args->zip, "core", "apps");
		}
		else
		{
			tf_ssb_import(tf_task_get_ssb(task), "core", "apps");
		}
	}
	if (tf_task_execute(task, args->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(-1);
	}
	if (setrlimit(RLIMIT_NOFILE, &zeroLimit) != 0)
	{
		perror("setrlimit(RLIMIT_NOFILE, {0, 0})");
		exit(-1);
	}
	if (setrlimit(RLIMIT_NPROC, &zeroLimit) != 0)
	{
		perror("setrlimit(RLIMIT_NPROC, {0, 0})");
		exit(-1);
	}
#if !defined(__MACH__) && !defined(__OpenBSD__)
	if (setrlimit(RLIMIT_LOCKS, &zeroLimit) != 0)
	{
		perror("setrlimit(RLIMIT_LOCKS, {0, 0})");
		exit(-1);
	}
	if (setrlimit(RLIMIT_MSGQUEUE, &zeroLimit) != 0)
	{
		perror("setrlimit(RLIMIT_MSGQUEUE, {0, 0})");
		exit(-1);
	}
#endif
#endif

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

static int _tf_command_run(const char* file, int argc, char* argv[])
{
	tf_run_args_t args = {
		.count = 1,
		.script = "core/core.js",
		.http_port = 12345,
		.https_port = 12346,
		.ssb_port = 8008,
		.db_path = k_db_path_default,
	};
	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-port", required_argument, NULL, 'b' },
			{ "ssb-network-key", required_argument, NULL, 'k' },
			{ "http-port", required_argument, NULL, 'p' },
			{ "https-port", required_argument, NULL, 'q' },
			{ "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:b:k:p:q: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 'b':
			args.ssb_port = atoi(optarg);
			break;
		case 'p':
			args.http_port = atoi(optarg);
			break;
		case 'q':
			args.https_port = atoi(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;
		}
	}

	if (show_usage)
	{
		tf_printf("\n%s run [options]\n\n", file);
		tf_printf("options\n");
		tf_printf("  -s, --script script        Script to run (default: core/core.js).\n");
		tf_printf("  -b, --ssb-port port        Port on which to run SSB (default: 8008, 0 disables).\n");
		tf_printf("  -p, --http-port port       Port on which to run Tilde Friends web server (default: 12345).\n");
		tf_printf("  -q, --https-port port      Port on which to run secure Tilde Friends web server (default: 12346).\n");
		tf_printf("  -d, --db-path path         SQLite database path (default: %s).\n", k_db_path_default);
		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.\n");
		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");
		return EXIT_FAILURE;
	}

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

	if (args.count == 1)
	{
		result = _tf_run_task(&args, 0);
	}
	if (args.count > 1)
	{
		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);
		for (int i = 0; i < args.count; i++)
		{
			data[i] = (tf_run_thread_data_t) {
				.args = args,
				.index = i,
			};
			uv_thread_create(&threads[i], _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);
	}
	return result;
}

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

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

	if (show_usage)
	{
		tf_printf("\nUsage: %s sandbox [options]\n\n", file);
		tf_printf("options:\n");
		tf_printf("  -h, --help    Show this usage information.\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, STDIN_FILENO);
	_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 < (int)_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(1);
}

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;
		}
	}

#if defined(__ANDROID__)
	setenv("UV_USE_IO_URING", "0", 1);
#endif

	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_tls_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__)
	use_error_handler = true;
#endif
	if (use_error_handler)
	{
		if (
#if !defined(_WIN32)
			signal(SIGSYS, _error_handler) == SIG_ERR ||
#endif
			signal(SIGSEGV, _error_handler) == SIG_ERR)
		{
			perror("signal");
		}
	}
}

#if defined(__ANDROID__)
int main(int argc, char* argv[])
{
	_startup(argc, argv);
	int result = -1;
	if (argc > 1)
	{
		if (strcmp(argv[1], "run") == 0)
		{
			result = _tf_command_run(argv[0], argc - 1, argv + 1);
		}
		else if (strcmp(argv[1], "sandbox") == 0)
		{
			result = _tf_command_sandbox(argv[0], argc - 1, argv + 1);
		}
	}
	tf_mem_shutdown();
	return result;
}
#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,
		.script = "core/core.js",
		.http_port = 12345,
		.https_port = 12346,
		.ssb_port = 8008,
		.db_path = k_db_path_default,
		.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[])
{
	_startup(argc, argv);

	int result = 0;
	if (argc >= 2)
	{
		for (int i = 0; i < (int)_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:
	tf_mem_shutdown();
	return result;
}
#endif
