#include "httpd.js.h"

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

#include "picohttpparser.h"

#include <stdlib.h>

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

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];
	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;
	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(data->app_blob_id, sizeof(data->app_blob_id), value);
			tf_free(app_path);
			tf_free((void*)value);
			data->file = last_slash + 1;
		}
	}
	else if (request->path[0] == '/' && request->path[1] == '&')
	{
		const char* end = strstr(request->path, ".sha256/");
		if (end)
		{
			snprintf(data->app_blob_id, sizeof(data->app_blob_id), "%.*s", (int)(end + strlen(".sha256") - request->path - 1), request->path + 1);
			data->file = end + strlen(".sha256/");
		}
	}

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

static void _httpd_call_app_handler(tf_ssb_t* ssb, tf_http_request_t* request, const char* app_blob_id, const char* path, const char* package_owner, const char* app)
{
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue global = JS_GetGlobalObject(context);
	JSValue exports = JS_GetPropertyStr(context, global, "exports");
	JSValue call_app_handler = JS_GetPropertyStr(context, exports, "callAppHandler");

	JSValue response = tf_httpd_make_response_object(context, request);
	tf_http_request_ref(request);
	JSValue handler_blob_id = JS_NewString(context, app_blob_id);
	JSValue path_value = JS_NewString(context, path);
	JSValue package_owner_value = JS_NewString(context, package_owner);
	JSValue app_value = JS_NewString(context, app);
	JSValue query_value = request->query ? JS_NewString(context, request->query) : JS_UNDEFINED;

	JSValue headers = JS_NewObject(context);
	for (int i = 0; i < request->headers_count; i++)
	{
		char name[256] = "";
		snprintf(name, sizeof(name), "%.*s", (int)request->headers[i].name_len, request->headers[i].name);
		JS_SetPropertyStr(context, headers, name, JS_NewStringLen(context, request->headers[i].value, request->headers[i].value_len));
	}

	JSValue args[] = {
		response,
		handler_blob_id,
		path_value,
		query_value,
		headers,
		package_owner_value,
		app_value,
	};

	JSValue result = JS_Call(context, call_app_handler, JS_NULL, tf_countof(args), args);
	tf_util_report_error(context, result);
	JS_FreeValue(context, result);

	JS_FreeValue(context, headers);
	JS_FreeValue(context, query_value);
	JS_FreeValue(context, app_value);
	JS_FreeValue(context, package_owner_value);
	JS_FreeValue(context, handler_blob_id);
	JS_FreeValue(context, path_value);
	JS_FreeValue(context, response);
	JS_FreeValue(context, call_app_handler);
	JS_FreeValue(context, exports);
	JS_FreeValue(context, global);
}

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)
	{
		tf_http_respond(data->request, 304, NULL, 0, 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->user, data->user_app->app);
	}
	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);
	}
	tf_free(data->user_app);
	tf_free(data->data);
	tf_http_request_unref(data->request);
	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);
}

void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
{
	tf_task_t* task = request->user_data;
	tf_ssb_t* ssb = tf_task_get_ssb(task);

	JSContext* context = tf_ssb_get_context(ssb);
	JSValue global = JS_GetGlobalObject(context);
	JSValue exports = JS_GetPropertyStr(context, global, "exports");
	JSValue app_socket = JS_GetPropertyStr(context, exports, "app_socket");

	JSValue request_object = JS_NewObject(context);
	JSValue headers = JS_NewObject(context);
	for (int i = 0; i < request->headers_count; i++)
	{
		JS_SetPropertyStr(context, headers, request->headers[i].name, JS_NewString(context, request->headers[i].value));
	}
	JS_SetPropertyStr(context, request_object, "headers", headers);

	JSValue response = tf_httpd_make_response_object(context, request);
	tf_http_request_ref(request);

	JSValue args[] = {
		request_object,
		response,
	};

	JSValue result = JS_Call(context, app_socket, JS_NULL, tf_countof(args), args);
	tf_util_report_error(context, result);
	JS_FreeValue(context, result);

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

	JS_FreeValue(context, app_socket);
	JS_FreeValue(context, exports);
	JS_FreeValue(context, global);
}
