#include "ssb.export.h"

#include "log.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"

#include "sqlite3.h"
#include "uv.h"

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

static void _write_file(const char* path, const void* blob, size_t size, bool force_add_trailing_newline)
{
	FILE* file = fopen(path, "wb");
	if (file)
	{
		fwrite(blob, 1, size, file);
		if (force_add_trailing_newline)
		{
			fputc('\n', file);
		}
		fclose(file);
	}
	else
	{
		tf_printf("Failed to open %s for write: %s.\n", path, strerror(errno));
	}
}

static void _make_dir(const char* path)
{
#if defined(_WIN32)
	if (mkdir(path) && errno != EEXIST)
#else
	if (mkdir(path, 0755) && errno != EEXIST)
#endif
	{
		tf_printf("Failed to create directory %s: %s.\n", path, strerror(errno));
	}
}

typedef struct _tf_export_t
{
	tf_ssb_t* ssb;
	const char* parent;
	JSValue files;
	uv_fs_t req;
	bool done;
} tf_export_t;

static void _tf_ssb_export_scandir(uv_fs_t* req)
{
	tf_export_t* export = req->data;
	JSContext* context = tf_ssb_get_context(export->ssb);
	uv_dirent_t ent;
	while (uv_fs_scandir_next(req, &ent) == 0)
	{
		if (ent.type == UV_DIRENT_FILE)
		{
			JSValue found = JS_GetPropertyStr(context, export->files, ent.name);
			if (JS_IsUndefined(found))
			{
				size_t len = strlen(export->parent) + strlen(ent.name) + 2;
				char* path = tf_malloc(len);
				snprintf(path, len, "%s/%s", export->parent, ent.name);
				uv_fs_t req = { 0 };
				int r = uv_fs_unlink(tf_ssb_get_loop(export->ssb), &req, path, NULL);
				if (r)
				{
					tf_printf("Failed to unlink %s: %s.", path, uv_strerror(r));
				}
				uv_fs_req_cleanup(&req);
				tf_free(path);
			}
			JS_FreeValue(context, found);
		}
	}
	export->done = true;
}

void tf_ssb_export(tf_ssb_t* ssb, const char* key)
{
	char user[256] = { 0 };
	char path[256] = { 0 };
	if (sscanf(key, "/~%255[^/]/%255s", user, path) != 2)
	{
		tf_printf("Unable to export %s.\n", key);
		return;
	}

	char app_blob_id[64] = { 0 };
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_busy_timeout(db, 10000);
	sqlite3_stmt* statement;
	if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ?1 AND key = 'path:' || ?2", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, path, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
		{
			int len = sqlite3_column_bytes(statement, 0);
			if (len >= (int)sizeof(app_blob_id))
			{
				len = sizeof(app_blob_id) - 1;
			}
			memcpy(app_blob_id, sqlite3_column_text(statement, 0), len);
			app_blob_id[len] = '\0';
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);

	if (!*app_blob_id)
	{
		tf_printf("Did not find app blob ID for %s.\n", key);
		return;
	}

	uint8_t* blob = NULL;
	size_t size = 0;
	if (!tf_ssb_db_blob_get(ssb, app_blob_id, &blob, &size))
	{
		tf_printf("Did not find blob for %s: %s.\n", key, app_blob_id);
		return;
	}
	char file_path[1024];
	_make_dir("apps/");
	snprintf(file_path, sizeof(file_path), "apps/%s", path);
	_make_dir(file_path);
	snprintf(file_path, sizeof(file_path), "apps/%s.json", path);
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue app = JS_ParseJSON(context, (const char*)blob, size, NULL);
	tf_free(blob);

	JSValue files = JS_GetPropertyStr(context, app, "files");
	JSPropertyEnum* ptab = NULL;
	uint32_t plen = 0;
	if (JS_GetOwnPropertyNames(context, &ptab, &plen, files, JS_GPN_STRING_MASK) == 0)
	{
		for (uint32_t i = 0; i < plen; ++i)
		{
			JSPropertyDescriptor desc;
			if (JS_GetOwnProperty(context, &desc, files, ptab[i].atom) == 1)
			{
				JSValue key = JS_AtomToString(context, ptab[i].atom);
				const char* file_name = JS_ToCString(context, key);
				const char* blob_id = JS_ToCString(context, desc.value);

				uint8_t* file_blob = NULL;
				size_t file_size = 0;
				if (tf_ssb_db_blob_get(ssb, blob_id, &file_blob, &file_size))
				{
					snprintf(file_path, sizeof(file_path), "apps/%s/%s", path, file_name);
					_write_file(file_path, file_blob, file_size, false);
					tf_free(file_blob);
				}

				JS_FreeCString(context, file_name);
				JS_FreeValue(context, key);
				JS_FreeCString(context, blob_id);
				JS_FreeValue(context, desc.value);
				JS_FreeValue(context, desc.setter);
				JS_FreeValue(context, desc.getter);
			}
		}
	}

	for (uint32_t i = 0; i < plen; ++i)
	{
		JS_FreeAtom(context, ptab[i].atom);
	}
	js_free(context, ptab);

	JSAtom files_atom = JS_NewAtom(context, "files");
	JS_DeleteProperty(context, app, files_atom, 0);
	JS_FreeAtom(context, files_atom);

	JSValue tab = JS_NewString(context, "\t");
	JSValue json = JS_JSONStringify(context, app, JS_NULL, tab);
	JS_FreeValue(context, tab);
	size_t length = 0;
	const char* string = JS_ToCStringLen(context, &length, json);
	snprintf(file_path, sizeof(file_path), "apps/%s.json", path);
	_write_file(file_path, string, length, true);
	JS_FreeCString(context, string);
	JS_FreeValue(context, json);

	snprintf(file_path, sizeof(file_path), "apps/%s", path);
	tf_export_t export =
	{
		.parent = file_path,
		.ssb = ssb,
		.files = files,
		.req =
		{
			.data = &export,
		},
	};
	int r = uv_fs_scandir(tf_ssb_get_loop(ssb), &export.req, file_path, 0, _tf_ssb_export_scandir);
	if (r)
	{
		tf_printf("Failed to scan directory %s: %s.", file_path, uv_strerror(r));
	}
	while (!export.done)
	{
		uv_run(tf_ssb_get_loop(ssb), UV_RUN_ONCE);
	}
	uv_fs_req_cleanup(&export.req);

	JS_FreeValue(context, files);
	JS_FreeValue(context, app);
}
