#include "httpd.js.h"

#include "http.h"
#include "log.h"
#include "mem.h"
#include "sha1.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "taskstub.js.h"
#include "util.js.h"

#include "picohttpparser.h"
#include "uv.h"

#include <stdlib.h>

#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif

JSClassID s_app_handler_class_id;
JSClassID s_app_class_id;

static void _httpd_app_finalizer(JSRuntime* runtime, JSValue value);

typedef struct _app_blob_t
{
	tf_http_request_t* request;
	bool found;
	bool not_modified;
	bool use_handler;
	bool use_static;
	void* data;
	size_t size;
	char app_blob_id[k_blob_id_len];
	char notify_want_blob_id[k_id_base64_len];
	const char* file;
	tf_httpd_user_app_t* user_app;
	char etag[256];
} app_blob_t;

static void _httpd_endpoint_app_blob_work(tf_ssb_t* ssb, void* user_data)
{
	app_blob_t* data = user_data;
	tf_http_request_t* request = data->request;

	char id[k_id_base64_len] = { 0 };

	if (request->path[0] == '/' && request->path[1] == '~')
	{
		const char* last_slash = strchr(request->path + 1, '/');
		if (last_slash)
		{
			last_slash = strchr(last_slash + 1, '/');
		}
		data->user_app = last_slash ? tf_httpd_parse_user_app_from_path(request->path, last_slash) : NULL;
		if (data->user_app)
		{
			size_t path_length = strlen("path:") + strlen(data->user_app->app) + 1;
			char* app_path = tf_malloc(path_length);
			snprintf(app_path, path_length, "path:%s", data->user_app->app);
			const char* value = tf_ssb_db_get_property(ssb, data->user_app->user, app_path);
			tf_string_set(id, sizeof(id), value);
			tf_free(app_path);
			tf_free((void*)value);
			data->file = last_slash + 1;
		}
	}
	else if (request->path[0] == '/' && (request->path[1] == '&' || request->path[1] == '%'))
	{
		const char* end = strstr(request->path, ".sha256/");
		if (end)
		{
			tf_http_decode_uri(id, sizeof(id), request->path + 1, (int)(end + strlen(".sha256") - request->path - 1));
			data->file = end + strlen(".sha256/");
		}
	}

	if (*id == '%')
	{
		sqlite3* db = tf_ssb_acquire_db_reader(ssb);
		tf_ssb_db_get_app_blob_from_message(db, id, data->app_blob_id, sizeof(data->app_blob_id));
		tf_ssb_release_db_reader(ssb, db);
	}
	else
	{
		tf_string_set(data->app_blob_id, sizeof(data->app_blob_id), id);
	}

	char* app_blob = NULL;
	size_t app_blob_size = 0;
	if (*data->app_blob_id && tf_ssb_db_blob_get(ssb, data->app_blob_id, (uint8_t**)&app_blob, &app_blob_size))
	{
		JSMallocFunctions funcs = { 0 };
		tf_get_js_malloc_functions(&funcs);
		JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
		JSContext* context = JS_NewContext(runtime);

		JSValue app_object = JS_ParseJSON(context, app_blob, app_blob_size, NULL);
		JSValue files = JS_GetPropertyStr(context, app_object, "files");
		JSValue blob_id = JS_GetPropertyStr(context, files, data->file);
		if (JS_IsUndefined(blob_id))
		{
			blob_id = JS_GetPropertyStr(context, files, "handler.js");
			if (!JS_IsUndefined(blob_id))
			{
				data->use_handler = true;
			}
		}
		else
		{
			const char* blob_id_str = JS_ToCString(context, blob_id);
			if (blob_id_str)
			{
				snprintf(data->etag, sizeof(data->etag), "\"%s\"", blob_id_str);
				const char* match = tf_http_request_get_header(data->request, "if-none-match");
				if (match && strcmp(match, data->etag) == 0)
				{
					data->not_modified = true;
				}
				else
				{
					data->found = tf_ssb_db_blob_get(ssb, blob_id_str, (uint8_t**)&data->data, &data->size);
					if (!data->found)
					{
						sqlite3* db = tf_ssb_acquire_db_writer(ssb);
						tf_ssb_db_add_blob_wants(db, blob_id_str);
						tf_ssb_release_db_writer(ssb, db);
						tf_string_set(data->notify_want_blob_id, sizeof(data->notify_want_blob_id), blob_id_str);
					}
				}
			}
			JS_FreeCString(context, blob_id_str);
		}
		JS_FreeValue(context, blob_id);
		JS_FreeValue(context, files);
		JS_FreeValue(context, app_object);

		JS_FreeContext(context);
		JS_FreeRuntime(runtime);
		tf_free(app_blob);
	}
}

typedef struct _app_handler_t
{
	JSContext* context;
	JSValue credentials;
	tf_http_request_t* request;
	const char* user;
	const char* settings;
	tf_httpd_user_app_t* user_app;
	const char* app_blob_id;
	JSValue process;
} app_handler_t;

static void _httpd_app_handler_get_settings_work(tf_ssb_t* ssb, void* user_data)
{
	app_handler_t* work = user_data;
	work->settings = tf_ssb_db_get_property(ssb, "core", "settings");
}

static void _httpd_process_kill_task(JSContext* context, JSValue process)
{
	if (JS_IsObject(process))
	{
		JSValue kill = JS_GetPropertyStr(context, process, "kill");
		if (!JS_IsUndefined(kill))
		{
			JSValue result = JS_Call(context, kill, process, 0, NULL);
			tf_util_report_error(context, result);
			JS_FreeValue(context, result);
			JS_FreeValue(context, kill);
		}
	}
}

static JSValue _httpd_app_handler_respond(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* func_data)
{
	app_handler_t* work = JS_GetOpaque(func_data[0], s_app_handler_class_id);
	JSValue answer = argv[0];
	JSValue answer_data = JS_GetPropertyStr(context, answer, "data");
	if (JS_IsString(answer_data))
	{
		size_t size = 0;
		const char* data = JS_ToCStringLen(context, &size, answer_data);
		char size_buf[32];
		snprintf(size_buf, sizeof(size_buf), "%zd", size);

		int status_code = 200;
		JSValue status_value = JS_GetPropertyStr(context, answer, "status_code");
		if (JS_IsNumber(status_value))
		{
			JS_ToInt32(context, &status_code, status_value);
		}
		JS_FreeValue(context, status_value);

		JSValue content_type_value = JS_GetPropertyStr(context, answer, "content_type");
		const char* content_type = JS_IsString(content_type_value) ? JS_ToCString(context, content_type_value) : js_strdup(context, "text/plain");

		const char* headers[] = {
			"Content-Type",
			content_type,
			"Content-Length",
			size_buf,
			"Access-Control-Allow-Origin",
			"*",
			"Content-Security-Policy",
			"sandbox allow-downloads allow-top-navigation-by-user-activation",
		};
		tf_http_respond(work->request, status_code, headers, tf_countof(headers) / 2, data, size);

		JS_FreeCString(context, content_type);
		JS_FreeValue(context, content_type_value);
		JS_FreeCString(context, data);
	}
	JSValue imports = JS_GetPropertyStr(context, work->process, "imports");
	JS_SetPropertyStr(context, imports, "respond", JS_UNDEFINED);
	JS_FreeValue(context, imports);
	_httpd_process_kill_task(work->context, work->process);
	tf_http_request_unref(work->request);
	work->request = NULL;
	JS_FreeValue(context, answer_data);

	return JS_UNDEFINED;
}

static void _httpd_app_handler_finalizer(JSRuntime* runtime, JSValue value)
{
	app_handler_t* work = JS_GetOpaque(value, s_app_handler_class_id);
	if (work->request)
	{
		const char* headers[] = {
			"Content-Type",
			"text/plain",
		};
		const char* k_payload = tf_http_status_text(500);
		tf_http_respond(work->request, 500, headers, tf_countof(headers) / 2, k_payload, strlen(k_payload));
		tf_http_request_unref(work->request);
		work->request = NULL;
	}
	_httpd_process_kill_task(work->context, work->process);
	JS_FreeCString(work->context, work->user);
	JS_FreeValue(work->context, work->credentials);
	JS_FreeValue(work->context, work->process);
	tf_free(work->user_app);
	tf_free((void*)work->app_blob_id);
	tf_free(work);
}

static JSValue _httpd_app_handler_on_process_start(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* func_data)
{
	app_handler_t* work = JS_GetOpaque(func_data[0], s_app_handler_class_id);
	if (!work->request)
	{
		_httpd_process_kill_task(work->context, argv[0]);
	}
	else
	{
		work->process = JS_DupValue(context, argv[0]);
	}
	return JS_UNDEFINED;
}

static void _httpd_app_handler_get_settings_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	app_handler_t* work = user_data;
	JSContext* context = work->context;
	if (work->settings)
	{
		JSValue settings_value = work->settings ? JS_ParseJSON(context, work->settings, strlen(work->settings), NULL) : JS_UNDEFINED;
		JSValue permissions = !JS_IsUndefined(settings_value) ? JS_GetPropertyStr(context, settings_value, "permissions") : JS_UNDEFINED;
		JSValue user_permissions = !JS_IsUndefined(permissions) ? JS_GetPropertyStr(context, permissions, work->user) : JS_UNDEFINED;
		JSValue out_permissions = JS_NewObject(context);
		JS_SetPropertyStr(context, work->credentials, "permissions", out_permissions);
		int length = !JS_IsUndefined(user_permissions) ? tf_util_get_length(context, user_permissions) : 0;
		for (int i = 0; i < length; i++)
		{
			JSValue permission = JS_GetPropertyUint32(context, user_permissions, i);
			const char* permission_string = JS_ToCString(context, permission);
			JS_SetPropertyStr(context, out_permissions, permission_string, JS_TRUE);
			JS_FreeCString(context, permission_string);
			JS_FreeValue(context, permission);
		}
		JS_FreeValue(context, user_permissions);
		JS_FreeValue(context, permissions);
		JS_FreeValue(context, settings_value);
		tf_free((void*)work->settings);
	}

	JSValue options = JS_NewObject(context);
	JS_SetPropertyStr(context, options, "script", JS_NewString(context, "handler.js"));
	JS_SetPropertyStr(context, options, "credentials", work->credentials);
	work->credentials = JS_UNDEFINED;
	JS_SetPropertyStr(context, options, "packageOwner", JS_NewString(context, work->user_app->user));
	JS_SetPropertyStr(context, options, "packageName", JS_NewString(context, work->user_app->app));
	JSValue imports = JS_NewObject(context);
	JS_SetPropertyStr(context, options, "imports", imports);

	JSValue request_object = JS_NewObject(context);
	JS_SetPropertyStr(context, request_object, "path", JS_NewString(context, work->request->path));
	if (work->request->query)
	{
		JS_SetPropertyStr(context, request_object, "query", JS_NewString(context, work->request->query));
	}
	JS_SetPropertyStr(context, imports, "request", request_object);

	if (!s_app_handler_class_id)
	{
		JS_NewClassID(&s_app_handler_class_id);
		JSClassDef def = {
			.class_name = "AppHandler",
			.finalizer = _httpd_app_handler_finalizer,
		};
		if (JS_NewClass(JS_GetRuntime(context), s_app_handler_class_id, &def) != 0)
		{
			tf_printf("Failed to register %s.", def.class_name);
		}
	}

	JSValue data = JS_NewObjectClass(context, s_app_handler_class_id);
	JS_SetOpaque(data, work);
	JS_SetPropertyStr(context, imports, "respond", JS_NewCFunctionData(context, _httpd_app_handler_respond, 1, 0, 1, &data));

	JSValue args[] = {
		JS_NewString(context, work->app_blob_id),
		options,
	};
	JSValue global = JS_GetGlobalObject(context);
	JSValue exports = JS_GetPropertyStr(context, global, "exports");
	JSValue get_process_blob = JS_GetPropertyStr(context, exports, "getProcessBlob");
	JSValue result = JS_Call(context, get_process_blob, JS_UNDEFINED, tf_countof(args), args);
	tf_util_report_error(context, result);
	for (int i = 0; i < tf_countof(args); i++)
	{
		JS_FreeValue(context, args[i]);
	}

	JSValue promise_then = JS_GetPropertyStr(context, result, "then");
	JSValue then = JS_NewCFunctionData(context, _httpd_app_handler_on_process_start, 1, 0, 1, &data);
	JSValue promise = JS_Call(context, promise_then, result, 1, &then);
	tf_util_report_error(context, promise);
	JS_FreeValue(context, promise);
	JS_FreeValue(context, then);
	JS_FreeValue(context, promise_then);
	JS_FreeValue(context, data);
	JS_FreeValue(context, result);
	JS_FreeValue(context, get_process_blob);
	JS_FreeValue(context, exports);
	JS_FreeValue(context, global);
}

static void _httpd_call_app_handler(tf_ssb_t* ssb, tf_http_request_t* request, const char* app_blob_id, const char* path, tf_httpd_user_app_t* user_app)
{
	JSContext* context = tf_ssb_get_context(ssb);

	app_handler_t* work = tf_malloc(sizeof(app_handler_t));
	*work = (app_handler_t) {
		.context = context,
		.credentials = JS_NewObject(context),
		.user_app = user_app,
		.app_blob_id = tf_strdup(app_blob_id),
		.request = request,
	};

	const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
	JSValue jwt = tf_httpd_authenticate_jwt(ssb, context, session);
	JS_SetPropertyStr(context, work->credentials, "session", jwt);
	work->user = tf_util_get_property_as_string(context, jwt, "name");
	tf_free((void*)session);

	if (work->user)
	{
		tf_ssb_run_work(ssb, _httpd_app_handler_get_settings_work, _httpd_app_handler_get_settings_after_work, work);
	}
	else
	{
		_httpd_app_handler_get_settings_after_work(ssb, 0, work);
	}

	tf_http_request_ref(request);
}

static void _httpd_endpoint_app_blob_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	app_blob_t* data = user_data;
	if (data->not_modified)
	{
		const char* headers[] = {
			"Access-Control-Allow-Origin",
			"*",
			"Content-Security-Policy",
			"sandbox allow-downloads allow-top-navigation-by-user-activation",
		};
		tf_http_respond(data->request, 304, headers, tf_countof(headers) / 2, NULL, 0);
	}
	else if (data->use_static)
	{
		tf_httpd_endpoint_static(data->request);
	}
	else if (data->use_handler)
	{
		_httpd_call_app_handler(ssb, data->request, data->app_blob_id, data->file, data->user_app);
		data->user_app = NULL;
	}
	else if (data->found)
	{
		const char* mime_type = tf_httpd_ext_to_content_type(strrchr(data->request->path, '.'), false);
		if (!mime_type)
		{
			mime_type = tf_httpd_magic_bytes_to_content_type(data->data, data->size);
		}
		const char* headers[] = {
			"Access-Control-Allow-Origin",
			"*",
			"Content-Security-Policy",
			"sandbox allow-downloads allow-top-navigation-by-user-activation",
			"Content-Type",
			mime_type ? mime_type : "application/binary",
			"etag",
			data->etag,
		};
		tf_http_respond(data->request, 200, headers, tf_countof(headers) / 2, data->data, data->size);
	}
	else
	{
		const char* headers[] = {
			"Access-Control-Allow-Origin",
			"*",
		};
		tf_http_respond(data->request, 404, headers, tf_countof(headers) / 2, NULL, 0);
	}
	tf_free(data->user_app);
	tf_free(data->data);
	tf_http_request_unref(data->request);

	if (*data->notify_want_blob_id)
	{
		tf_ssb_notify_blob_want_added(ssb, data->notify_want_blob_id);
	}
	tf_free(data);
}

void tf_httpd_endpoint_app(tf_http_request_t* request)
{
	tf_http_request_ref(request);
	tf_task_t* task = request->user_data;
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	app_blob_t* data = tf_malloc(sizeof(app_blob_t));
	*data = (app_blob_t) { .request = request };
	tf_ssb_run_work(ssb, _httpd_endpoint_app_blob_work, _httpd_endpoint_app_blob_after_work, data);
}

typedef struct _app_t
{
	JSContext* context;
	tf_http_request_t* request;
	uv_timer_t timer;
	const char* settings;
	JSValue opaque;
	JSValue credentials;
	JSValue process;
	uint64_t last_ping_ms;
	uint64_t last_active_ms;
	int ref_count;
	bool got_hello;
} app_t;

static void _httpd_auth_query_work(tf_ssb_t* ssb, void* user_data)
{
	app_t* work = user_data;
	work->settings = tf_ssb_db_get_property(ssb, "core", "settings");
}

typedef struct _app_hello_t
{
	app_t* app;
	JSValue message;
	const char* user;
	const char* path;
	char blob_id[k_id_base64_len];
	tf_ssb_identity_info_t* identity_info;
	tf_httpd_user_app_t* user_app;
} app_hello_t;

static void _httpd_app_hello_work(tf_ssb_t* ssb, void* user_data)
{
	app_hello_t* work = user_data;

	work->user_app = tf_httpd_parse_user_app_from_path(work->path, NULL);
	if (work->user_app)
	{
		size_t length = strlen("path:") + strlen(work->user_app->app) + 1;
		char* key = alloca(length);
		snprintf(key, length, "path:%s", work->user_app->app);
		const char* value = tf_ssb_db_get_property(ssb, work->user_app->user, key);
		tf_string_set(work->blob_id, sizeof(work->blob_id), value);
		tf_free((void*)value);
	}
	else if (work->path[0] == '/' && (work->path[1] == '%' || work->path[1] == '&') && strlen(work->path) >= 1 + k_blob_id_len && strstr(work->path, ".sha256"))
	{
		tf_http_decode_uri(work->blob_id, sizeof(work->blob_id), work->path + 1, strstr(work->path, ".sha256") - work->path - 1 + strlen(".sha256"));
	}

	if (*work->blob_id)
	{
		work->identity_info = tf_ssb_db_get_identity_info(ssb, work->user, work->user_app ? work->user_app->user : NULL, work->user_app ? work->user_app->app : NULL);
	}
}

static void _http_json_send(tf_http_request_t* request, JSContext* context, JSValue value)
{
	JSValue json = JS_JSONStringify(context, value, JS_NULL, JS_NULL);
	size_t json_length = 0;
	const char* payload = JS_ToCStringLen(context, &json_length, json);
	tf_http_request_websocket_send(request, 0x1, payload, json_length);
	JS_FreeCString(context, payload);
	JS_FreeValue(context, json);
}

static JSValue _httpd_app_on_tfrpc(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* func_data)
{
	const char* id = tf_util_get_property_as_string(context, argv[0], "id");
	if (id)
	{
		app_t* app = JS_GetOpaque(func_data[0], s_app_class_id);
		JSValue calls = JS_IsObject(app->process) ? JS_GetPropertyStr(context, app->process, "_calls") : JS_UNDEFINED;
		JSValue call = JS_IsObject(calls) ? JS_GetPropertyStr(context, calls, id) : JS_UNDEFINED;
		if (!JS_IsUndefined(call))
		{
			JSValue error = JS_GetPropertyStr(context, argv[0], "error");
			if (!JS_IsUndefined(error))
			{
				JSValue reject = JS_GetPropertyStr(context, call, "reject");
				JSValue result = JS_Call(context, reject, JS_UNDEFINED, 1, &error);
				tf_util_report_error(context, result);
				JS_FreeValue(context, result);
				JS_FreeValue(context, reject);
			}
			else
			{
				JSValue resolve = JS_GetPropertyStr(context, call, "resolve");
				JSValue message_result = JS_GetPropertyStr(context, argv[0], "result");
				JSValue result = JS_Call(context, resolve, JS_UNDEFINED, 1, &message_result);
				JS_FreeValue(context, message_result);
				tf_util_report_error(context, result);
				JS_FreeValue(context, result);
				JS_FreeValue(context, resolve);
			}
			JS_FreeValue(context, error);
		}
		JS_FreeValue(context, call);
		JS_FreeValue(context, calls);
	}
	JS_FreeCString(context, id);
	return JS_UNDEFINED;
}

static JSValue _httpd_app_on_output(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* func_data)
{
	app_t* app = JS_GetOpaque(func_data[0], s_app_class_id);
	if (app && app->request)
	{
		_http_json_send(app->request, context, argv[0]);
	}
	return JS_UNDEFINED;
}

static JSValue _httpd_app_on_process_start(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* func_data)
{
	app_t* app = JS_GetOpaque(func_data[0], s_app_class_id);
	app->process = JS_DupValue(context, argv[0]);

	JSValue client_api = JS_GetPropertyStr(context, app->process, "client_api");
	JSValue tfrpc = JS_NewCFunctionData(context, _httpd_app_on_tfrpc, 1, 0, 1, func_data);
	JS_SetPropertyStr(context, client_api, "tfrpc", tfrpc);
	JS_FreeValue(context, client_api);

	JSValue on_output = JS_NewCFunctionData(context, _httpd_app_on_output, 1, 0, 1, func_data);
	JS_SetPropertyStr(context, app->process, "_on_output", on_output);

	JSValue send = JS_GetPropertyStr(context, app->process, "send");
	JSValue result = JS_Call(context, send, app->process, 0, NULL);
	JS_FreeValue(context, send);
	tf_util_report_error(context, result);
	JS_FreeValue(context, result);

	return JS_UNDEFINED;
}

static void _httpd_app_hello_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	app_hello_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);

	if (!*work->blob_id)
	{
		JSValue object = JS_NewObject(context);
		JS_SetPropertyStr(context, object, "action", JS_NewString(context, "tfrpc"));
		JS_SetPropertyStr(context, object, "method", JS_NewString(context, "error"));
		JSValue params = JS_NewArray(context);
		size_t length = strlen(work->path) + strlen(" not found") + 1;
		char* message = alloca(length);
		snprintf(message, length, "%s not found", work->path);
		JS_SetPropertyUint32(context, params, 0, JS_NewString(context, message));
		JS_SetPropertyStr(context, object, "params", params);
		JS_SetPropertyStr(context, object, "id", JS_NewInt32(context, -1));
		if (work->app->request)
		{
			_http_json_send(work->app->request, context, object);
		}
		JS_FreeValue(context, object);
	}
	else
	{
		JSValue object = JS_NewObject(context);
		JS_SetPropertyStr(context, object, "action", JS_NewString(context, "session"));
		JS_SetPropertyStr(context, object, "credentials", JS_DupValue(context, work->app->credentials));
		JS_SetPropertyStr(context, object, "id", JS_NewString(context, work->blob_id));

		if (work->identity_info)
		{
			JSValue identities = JS_NewArray(context);
			for (int i = 0; i < work->identity_info->count; i++)
			{
				JS_SetPropertyUint32(context, identities, i, JS_NewString(context, work->identity_info->identity[i]));
			}
			JS_SetPropertyStr(context, object, "identities", identities);

			JSValue names = JS_NewObject(context);
			for (int i = 0; i < work->identity_info->count; i++)
			{
				JS_SetPropertyStr(context, names, work->identity_info->identity[i],
					JS_NewString(context, work->identity_info->name[i] ? work->identity_info->name[i] : work->identity_info->identity[i]));
			}
			JS_SetPropertyStr(context, object, "names", names);
			JS_SetPropertyStr(context, object, "identity", JS_NewString(context, work->identity_info->active_identity));
		}
		if (work->app->request)
		{
			_http_json_send(work->app->request, context, object);
		}
		JS_FreeValue(context, object);

		JSValue edit_only = JS_GetPropertyStr(context, work->message, "edit_only");
		bool is_edit_only = JS_ToBool(context, edit_only) > 0;
		JS_FreeValue(context, edit_only);

		if (is_edit_only)
		{
			JSValue global = JS_GetGlobalObject(context);
			JSValue version = JS_GetPropertyStr(context, global, "version");
			JS_FreeValue(context, global);

			JSValue ready = JS_NewObject(context);
			JS_SetPropertyStr(context, ready, "action", JS_NewString(context, "ready"));
			JS_SetPropertyStr(context, ready, "version", JS_Call(context, version, JS_NULL, 0, NULL));
			JS_SetPropertyStr(context, ready, "edit_only", JS_TRUE);
			if (work->app->request)
			{
				_http_json_send(work->app->request, context, ready);
			}
			JS_FreeValue(context, ready);
			JS_FreeValue(context, version);
		}
		else
		{
			JSValue options = JS_NewObject(context);
			JSValue api = JS_GetPropertyStr(context, work->message, "api");
			JS_SetPropertyStr(context, options, "api", JS_IsUndefined(api) ? JS_NewArray(context) : api);
			JS_SetPropertyStr(context, options, "credentials", JS_DupValue(context, work->app->credentials));
			JS_SetPropertyStr(context, options, "packageOwner", work->user_app ? JS_NewString(context, work->user_app->user) : JS_UNDEFINED);
			JS_SetPropertyStr(context, options, "packageName", work->user_app ? JS_NewString(context, work->user_app->app) : JS_UNDEFINED);
			JS_SetPropertyStr(context, options, "url", JS_GetPropertyStr(context, work->message, "url"));

			JSValue global = JS_GetGlobalObject(context);
			JSValue exports = JS_GetPropertyStr(context, global, "exports");
			JSValue get_process_blob = JS_GetPropertyStr(context, exports, "getProcessBlob");

			JSValue args[] = {
				JS_NewString(context, work->blob_id),
				options,
			};
			JSValue result = JS_Call(context, get_process_blob, JS_UNDEFINED, tf_countof(args), args);
			tf_util_report_error(context, result);

			JSValue promise_then = JS_GetPropertyStr(context, result, "then");

			if (!s_app_class_id)
			{
				JS_NewClassID(&s_app_class_id);
				JSClassDef def = {
					.class_name = "AppData",
					.finalizer = _httpd_app_finalizer,
				};
				if (JS_NewClass(JS_GetRuntime(context), s_app_class_id, &def) != 0)
				{
					tf_printf("Failed to register %s.", def.class_name);
				}
			}

			work->app->opaque = JS_NewObjectClass(context, s_app_class_id);
			work->app->ref_count++;
			JS_SetOpaque(work->app->opaque, work->app);
			JSValue then = JS_NewCFunctionData(context, _httpd_app_on_process_start, 1, 0, 1, &work->app->opaque);

			JSValue promise = JS_Call(context, promise_then, result, 1, &then);
			tf_util_report_error(context, promise);
			JS_FreeValue(context, promise);

			/* except? */

			JS_FreeValue(context, then);
			JS_FreeValue(context, promise_then);

			JS_FreeValue(context, result);

			JS_FreeValue(context, get_process_blob);
			JS_FreeValue(context, exports);
			JS_FreeValue(context, global);

			for (int i = 0; i < tf_countof(args); i++)
			{
				JS_FreeValue(context, args[i]);
			}
		}
	}

	if (work->app->request)
	{
		tf_http_request_unref(work->app->request);
	}
	JS_FreeCString(context, work->user);
	JS_FreeCString(context, work->path);
	JS_FreeValue(context, work->message);
	tf_free(work->identity_info);
	tf_free(work->user_app);
	tf_free(work);
}

static void _httpd_app_message_hello(app_t* work, JSValue message)
{
	JSContext* context = work->request->context;
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	tf_http_request_ref(work->request);
	work->got_hello = true;

	JSValue session = JS_IsObject(work->credentials) ? JS_GetPropertyStr(context, work->credentials, "session") : JS_UNDEFINED;
	const char* user = tf_util_get_property_as_string(context, session, "name");
	JS_FreeValue(context, session);

	app_hello_t* hello = tf_malloc(sizeof(app_hello_t));
	*hello = (app_hello_t) {
		.app = work,
		.user = user,
		.message = JS_DupValue(context, message),
		.path = tf_util_get_property_as_string(context, message, "path"),
	};
	tf_ssb_run_work(ssb, _httpd_app_hello_work, _httpd_app_hello_after_work, hello);
}

static bool _httpd_app_message_call_client_api(app_t* work, JSValue message, const char* action_string)
{
	bool called = false;
	JSContext* context = work->request->context;
	JSValue client_api = JS_IsObject(work->process) ? JS_GetPropertyStr(context, work->process, "client_api") : JS_UNDEFINED;
	JSValue callback = JS_IsObject(client_api) ? JS_GetPropertyStr(context, client_api, action_string) : JS_UNDEFINED;
	if (!JS_IsUndefined(callback))
	{
		JSValue args = JS_GetPropertyStr(context, message, "args");
		int argc = tf_util_get_length(context, args);
		JSValue buffer[8];
		JSValue* argv = argc <= tf_countof(buffer) ? buffer : tf_malloc(sizeof(JSValue) * argc);
		for (int i = 0; i < argc; i++)
		{
			argv[i] = JS_GetPropertyUint32(context, args, i);
		}
		JSValue result = JS_Call(context, callback, JS_NULL, argc, argv);
		tf_util_report_error(context, result);
		JS_FreeValue(context, result);
		for (int i = 0; i < argc; i++)
		{
			JS_FreeValue(context, argv[i]);
		}
		JS_FreeValue(context, args);
		if (argv != buffer)
		{
			tf_free(argv);
		}
		called = true;
	}
	JS_FreeValue(context, callback);
	JS_FreeValue(context, client_api);
	return called;
}

static bool _httpd_app_message_call_message_handler(app_t* work, JSValue message)
{
	bool called = false;
	JSContext* context = work->request->context;
	JSValue event_handlers = JS_GetPropertyStr(context, work->process, "eventHandlers");
	if (JS_IsObject(event_handlers))
	{
		JSValue handler_array = JS_GetPropertyStr(context, event_handlers, "message");
		if (!JS_IsUndefined(handler_array))
		{
			for (int i = 0; i < tf_util_get_length(context, handler_array); i++)
			{
				JSValue handler = JS_GetPropertyUint32(context, handler_array, i);
				JSValue result = JS_Call(context, handler, JS_NULL, 1, &message);
				tf_util_report_error(context, result);
				JS_FreeValue(context, result);
				JS_FreeValue(context, handler);
				called = true;
			}
		}
		JS_FreeValue(context, handler_array);
	}
	JS_FreeValue(context, event_handlers);
	return called;
}

static void _httpd_app_on_message(tf_http_request_t* request, int op_code, const void* data, size_t size)
{
	app_t* work = request->user_data;
	JSContext* context = request->context;
	tf_task_t* task = tf_task_get(context);
	work->last_active_ms = uv_now(tf_task_get_loop(task));
	switch (op_code)
	{
	/* TEXT */
	case 0x1:
	/* BINARY */
	case 0x2:
		{
			JSValue message = JS_ParseJSON(context, data, size, NULL);
			if (JS_IsException(message) || !JS_IsObject(message))
			{
				tf_util_report_error(context, message);
				tf_http_request_websocket_close(request);
			}
			else
			{
				JSValue action = JS_GetPropertyStr(context, message, "action");
				const char* action_string = JS_ToCString(context, action);
				if (action_string && !work->got_hello && strcmp(action_string, "hello") == 0)
				{
					_httpd_app_message_hello(work, message);
				}
				else if (!_httpd_app_message_call_client_api(work, message, action_string))
				{
					_httpd_app_message_call_message_handler(work, message);
				}
				JS_FreeCString(context, action_string);
				JS_FreeValue(context, action);
			}
			JS_FreeValue(context, message);
		}
		break;
	/* CLOSE */
	case 0x8:
		_httpd_process_kill_task(work->request->context, work->process);
		tf_http_request_websocket_send(request, 0x8, data, tf_min(size, sizeof(uint16_t)));
		break;
	/* PONG */
	case 0xa:
		break;
	}
}

static void _httpd_app_finish(app_t* app)
{
	JSContext* context = app->context;
	JSValue process = app->process;
	JSValue opaque = app->opaque;

	JS_FreeValue(context, app->credentials);
	app->credentials = JS_UNDEFINED;

	app->process = JS_UNDEFINED;
	app->opaque = JS_UNDEFINED;

	if (!app->ref_count && !app->timer.data && !app->request)
	{
		tf_free(app);
	}

	if (!JS_IsUndefined(process))
	{
		_httpd_process_kill_task(context, process);
		JS_FreeValue(context, process);
	}

	JS_FreeValue(context, opaque);
}

static void _httpd_app_on_timer_close(uv_handle_t* handle)
{
	app_t* work = handle->data;
	handle->data = NULL;
	_httpd_app_finish(work);
}

static void _httpd_app_finalizer(JSRuntime* runtime, JSValue value)
{
	app_t* app = JS_GetOpaque(value, s_app_class_id);
	app->ref_count--;
	_httpd_app_finish(app);
}

static void _httpd_app_on_close(tf_http_request_t* request)
{
	app_t* work = request->user_data;
	work->request = NULL;
	if (work->timer.data)
	{
		uv_close((uv_handle_t*)&work->timer, _httpd_app_on_timer_close);
	}
	tf_http_request_unref(request);
}

static void _httpd_app_on_timer(uv_timer_t* timer)
{
	app_t* app = timer->data;
	if (app->request)
	{
		uint64_t now_ms = uv_now(timer->loop);
		uint64_t repeat_ms = uv_timer_get_repeat(timer);
		if (now_ms - app->last_active_ms < repeat_ms)
		{
			/* Active. */
		}
		else if (app->last_ping_ms > app->last_active_ms)
		{
			/* Timed out. */
			tf_http_request_websocket_close(app->request);
		}
		else
		{
			tf_http_request_websocket_send(app->request, 0x9, NULL, 0);
			app->last_ping_ms = now_ms;
		}
	}
}

static void _httpd_auth_query_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	app_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue session = JS_GetPropertyStr(context, work->credentials, "session");
	JSValue name = JS_GetPropertyStr(context, session, "name");
	JS_FreeValue(context, session);
	const char* name_string = JS_ToCString(context, name);
	JSValue settings_value = work->settings ? JS_ParseJSON(context, work->settings, strlen(work->settings), NULL) : JS_UNDEFINED;
	tf_free((void*)work->settings);
	work->settings = NULL;

	JSValue out_permissions = JS_NewObject(context);
	JS_SetPropertyStr(context, work->credentials, "permissions", out_permissions);
	JSValue permissions = !JS_IsUndefined(settings_value) ? JS_GetPropertyStr(context, settings_value, "permissions") : JS_UNDEFINED;
	JSValue user_permissions = name_string && !JS_IsUndefined(permissions) ? JS_GetPropertyStr(context, permissions, name_string) : JS_UNDEFINED;
	int length = !JS_IsUndefined(user_permissions) ? tf_util_get_length(context, user_permissions) : 0;
	for (int i = 0; i < length; i++)
	{
		JSValue permission = JS_GetPropertyUint32(context, user_permissions, i);
		const char* permission_string = JS_ToCString(context, permission);
		JS_SetPropertyStr(context, out_permissions, permission_string, JS_TRUE);
		JS_FreeCString(context, permission_string);
		JS_FreeValue(context, permission);
	}
	JS_FreeValue(context, user_permissions);
	JS_FreeValue(context, permissions);
	JS_FreeValue(context, settings_value);
	JS_FreeValue(context, name);

	tf_http_request_t* request = work->request;
	const char* header_sec_websocket_key = tf_http_request_get_header(request, "sec-websocket-key");

	static const char* k_magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
	size_t key_length = strlen(header_sec_websocket_key);
	size_t size = key_length + 36;
	uint8_t* key_magic = alloca(size);
	memcpy(key_magic, header_sec_websocket_key, key_length);
	memcpy(key_magic + key_length, k_magic, 36);

	uint8_t digest[20];
	SHA1_CTX sha1 = { 0 };
	SHA1Init(&sha1);
	SHA1Update(&sha1, key_magic, size);
	SHA1Final(digest, &sha1);

	char key[41] = { 0 };
	tf_base64_encode(digest, sizeof(digest), key, sizeof(key));

	const char* headers[64] = { 0 };
	int headers_count = 0;

	headers[headers_count * 2 + 0] = "Upgrade";
	headers[headers_count * 2 + 1] = "websocket";
	headers_count++;

	headers[headers_count * 2 + 0] = "Connection";
	headers[headers_count * 2 + 1] = "Upgrade";
	headers_count++;

	headers[headers_count * 2 + 0] = "Sec-WebSocket-Accept";
	headers[headers_count * 2 + 1] = key;
	headers_count++;

	const char* session_token = tf_httpd_make_session_jwt(tf_ssb_get_context(ssb), ssb, name_string);
	const char* cookie = tf_httpd_make_set_session_cookie_header(request, session_token);
	tf_free((void*)session_token);
	headers[headers_count * 2 + 0] = "Set-Cookie";
	headers[headers_count * 2 + 1] = cookie ? cookie : "";
	headers_count++;

	bool send_version = !tf_http_request_get_header(request, "sec-websocket-version") || strcmp(tf_http_request_get_header(request, "sec-websocket-version"), "13") != 0;
	if (send_version)
	{
		headers[headers_count * 2 + 0] = "Sec-WebSocket-Accept";
		headers[headers_count * 2 + 1] = key;
		headers_count++;
	}

	tf_http_request_websocket_upgrade(request);
	tf_http_respond(request, 101, headers, headers_count, NULL, 0);

	uv_timer_start(&work->timer, _httpd_app_on_timer, 6 * 1000, 6 * 1000);

	tf_free((void*)cookie);
	JS_FreeCString(context, name_string);

	request->on_message = _httpd_app_on_message;
	request->on_close = _httpd_app_on_close;
	request->context = context;
	request->user_data = work;
}

void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
{
	const char* header_connection = tf_http_request_get_header(request, "connection");
	const char* header_upgrade = tf_http_request_get_header(request, "upgrade");
	const char* header_sec_websocket_key = tf_http_request_get_header(request, "sec-websocket-key");
	if (!header_connection || !header_upgrade || !header_sec_websocket_key || !strstr(header_connection, "Upgrade") || strcasecmp(header_upgrade, "websocket"))
	{
		tf_http_respond(request, 500, NULL, 0, NULL, 0);
		return;
	}

	tf_task_t* task = request->user_data;
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	JSContext* context = tf_task_get_context(task);
	const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");

	JSValue jwt = tf_httpd_authenticate_jwt(ssb, context, session);
	tf_free((void*)session);

	JSValue credentials = JS_NewObject(context);
	if (!JS_IsUndefined(jwt))
	{
		JS_SetPropertyStr(context, credentials, "session", jwt);
	}

	tf_http_request_ref(request);
	app_t* work = tf_malloc(sizeof(app_t));
	*work = (app_t) {
		.context = context,
		.request = request,
		.credentials = credentials,
		.timer = { .data = work },
	};
	uv_timer_init(tf_ssb_get_loop(ssb), &work->timer);
	tf_ssb_run_work(ssb, _httpd_auth_query_work, _httpd_auth_query_after_work, work);
}
