#include "iosu_boss.h"

#include <sstream>
#include <iomanip>
#include <thread>
#include <algorithm>
#include <array>
#include <queue>
#include <filesystem>
#include <fstream>

#include "config/ActiveSettings.h"
#include "config/NetworkSettings.h"
#include "curl/curl.h"
#include "openssl/bn.h"
#include "openssl/x509.h"
#include "openssl/ssl.h"

#include "iosu_ioctl.h"
#include "Cafe/OS/libs/nn_common.h"
#include "iosu_act.h"
#include "iosu_crypto.h"
#include "util/crypto/aes128.h"
#include "config/CemuConfig.h"

#include "util/helpers/helpers.h"
#include "Cemu/ncrypto/ncrypto.h"
#include "Cafe/CafeSystem.h"

namespace iosu
{
	enum TurnState
	{
		kUnknown = 0,
		kStopped = 1,
		kStoppedByPolicyList = 2,
		kWaitTime = 3,
		kWaitRun = 4,
		kWaitResume = 5,
		kRunning = 6,
		kFinished = 7,
		kSuccess = 16,
		kError = 17,
	};

	enum class ContentType
	{
		kUnknownContent,
		kXmlContent,
		kBinaryFile,
		kText,
	};

	enum class FileType
	{
		kUnknownFile,
		kAppData,
	};

	enum TaskSettingType : uint32
	{
		kRawDlTaskSetting = 0x10000698,
	};

	struct TaskFile
	{
		TaskFile(std::string file_name, uint32 data_id, FileType type, std::string url, uint32 size)
			: file_name(file_name), data_id(data_id), file_type(type), url(url), size(size) {}

		std::string file_name;
		uint32 data_id;
		FileType file_type;
		std::string url;
		uint32 size;
		uint64 last_modified = 0;
	};

	struct TaskSetting
	{
		static const uint32 kBossCode = 0x7C0;
		static const uint32 kBossCodeLen = 0x20;

		static const uint32 kURL = 0x48;
		static const uint32 kURLLen = 0x100;

		static const uint32 kClientCert = 0x41;
		static const uint32 kCACert = 0x188;

		static const uint32 kServiceToken = 0x590;
		static const uint32 kServiceTokenLen = 0x200;

		static const uint32 kDirectorySizeLimit = 0x7F0;
		static const uint32 kDirectoryName = 0x7C8;
		static const uint32 kDirectoryNameLen = 0x8;

		static const uint32 kFileName = 0x7D0;
		static const uint32 kNbdlFileName = 0x7F8;
		static const uint32 kFileNameLen = 0x20;

		std::array<uint8, 0x1000> settings;
		uint32be taskType; // +0x1000
	};

	static_assert(sizeof(TaskSetting) == 0x1004, "sizeof(TaskSetting_t)");

	struct Task
	{
		char task_id[8]{};
		uint32 account_id;
		uint64 title_id;

		TaskSetting task_settings;

		uint32 exec_count = 0;
		std::shared_ptr<CURL> curl;
		uint64 content_length = 0;
		uint64 processed_length = 0;

		uint32 turn_state = 0;
		uint32 wait_state = 0;

		long http_status_code = 0;
		ContentType content_type = ContentType::kUnknownContent;

		std::vector<uint8> result_buffer;

		std::queue<TaskFile> queued_files;
		std::vector<uint8> file_buffer;
		uint32 processed_file_size = 0;

		Task(const char* id, uint32 account_id, uint64 title_id, TaskSetting* settings)
		{
			strncpy(task_id, id, sizeof(task_id));
			this->account_id = account_id;
			this->title_id = title_id;
			this->task_settings.settings = settings->settings;
			this->task_settings.taskType = settings->taskType;

			curl = std::shared_ptr<CURL>(curl_easy_init(), curl_easy_cleanup);
			if(GetConfig().proxy_server.GetValue() != "")
			{
			  curl_easy_setopt(curl.get(), CURLOPT_PROXY, GetConfig().proxy_server.GetValue().c_str());
			}
		}
	};

#define FAD_ENTRY_MAX_COUNT (512)

	struct BossStorageFadEntry
	{
		char name[0x20];
		uint32be fileNameId;
		uint32 ukn24;
		uint32 ukn28;
		uint32 ukn2C;
		uint32 ukn30;
		uint32be timestampRelated; // guessed
	};

	static_assert(sizeof(BossStorageFadEntry) == 0x38, "sizeof(BossStorageFadEntry)");

	struct BossStorageFadFile
	{
		uint8 _00[0x08];
		BossStorageFadEntry entries[FAD_ENTRY_MAX_COUNT];
	};

	static_assert(sizeof(BossStorageFadFile) == 28680, "sizeof(BossStorageFadFile)");

	struct BossNbdlHeader
	{
		/* +0x00 */
		uint32be magic;
		/* +0x04 */
		uint32be version; // guessed
		/* +0x08 */
		uint16be ukn08; // must always be 1
		/* +0x0A */
		uint16be ukn0A; // must always be 2
		/* +0x0C */
		uint8 nonce[0xC];
		/* +0x18 */
		uint32 padding18; // unused
		/* +0x1C */
		uint32 padding1C; // unused
		/* +0x20 */
		struct
		{
			uint8 uknHashData[0x20];
		} encryptedHeader;
	} ;

	static_assert(sizeof(BossNbdlHeader) == 0x40, "BossNbdlHeader has invalid size");
	static_assert(offsetof(BossNbdlHeader, encryptedHeader) == 0x20, "offsetof(BossNbdlHeader, encryptedHeader)");


	struct
	{
		bool is_initialized;

		std::vector<Task> tasks;
	} g_boss = {};

	/*
		X-BOSS-Closed
		X-BOSS-TitleId
		X-Boss-UniqueId
		�\r���\r�(X-BOSS-Digest
	
		LOAD:E01038B4 0000000C C %susr/boss/
		LOAD:E0103A6C 00000011 C %susr/boss/%08x/
		LOAD:E0103B04 00000016 C %susr/boss/%08x/%08x/
		LOAD:E0103B1C 0000001B C %susr/boss/%08x/%08x/user/
		LOAD:E0103B5C 00000020 C %susr/boss/%08x/%08x/user/%08x/
		LOAD:E0103B38 00000022 C %susr/boss/%08x/%08x/user/common/
		LOAD:E0103AC4 00000020 C %susr/boss/%08x/%08x/user/temp/
	
		LOAD:E01106DC 0000000A C /dev/boss
	
		LOAD:05063698 0000001C C /vol/storage_mlc01/usr/boss
		LOAD:E2299CA8 00000028 C /vol/storage_mlc01/usr/save/system/boss
	 */

	template <typename ... TArgs>
	curl_slist* append_header_param(struct curl_slist* list, const char* format, TArgs&& ... args)
	{
		return curl_slist_append(list, fmt::format(fmt::runtime(format), std::forward<TArgs>(args)...).c_str());
	}

	bool starts_with(const char* str, const char* pre)
	{
		const size_t len_string = strlen(str);
		const size_t len_prefix = strlen(pre);
		return len_string < len_prefix ? false : strncmp(pre, str, len_prefix) == 0;
	}

	size_t task_write_callback(char* ptr, size_t size, size_t nmemb, void* userdata)
	{
		Task* task = (Task*)userdata;
		const size_t writeByteSize = size * nmemb;

		//if (task->result_buffer.size() - task->processed_length < writeByteSize)
		//	task->result_buffer.resize(task->result_buffer.size() + writeByteSize);
		//writeByteSize = min(writeByteSize, task->result_buffer.capacity() - task->processed_length);

		//cemuLog_logDebug(LogType::Force, "task_write_callback: {} (processed: {})", writeByteSize, task->processed_length);
		if (writeByteSize > 0)
		{
			//memcpy(task->result_buffer.data() + task->processed_length, ptr, writeByteSize);
			task->result_buffer.insert(task->result_buffer.end(), ptr, ptr + writeByteSize);
			task->processed_length += writeByteSize;
		}

		return writeByteSize;
	}

	size_t task_download_header_callback(char* ptr, size_t size, size_t nitems, void* userdata)
	{
		//cemuLog_logDebug(LogType::Force, "\tHeader: {}", ptr);

		return size * nitems;
	}

	size_t task_download_filecallback(char* ptr, size_t size, size_t nmemb, void* userdata)
	{
		Task* task = (Task*)userdata;
		const size_t writeByteSize = size * nmemb;
		//writeByteSize = min(writeByteSize, task->file_buffer.capacity() - task->processed_file_size);
		if (writeByteSize > 0)
		{
			//memcpy(task->file_buffer.data() + task->processed_file_size, ptr, writeByteSize);
			task->file_buffer.insert(task->file_buffer.end(), ptr, ptr + writeByteSize);
			task->processed_file_size += writeByteSize;
		}
		return writeByteSize;
	}

	size_t task_header_callback(char* ptr, size_t size, size_t nitems, void* userdata)
	{
		Task* task = (Task*)userdata;
		if (starts_with(ptr, "Content-Length: "))
		{
			task->content_length = strtol(&ptr[16], nullptr, 0);
			task->result_buffer.clear();
			task->result_buffer.reserve(task->content_length);
			task->processed_length = 0;
		}
		else if (starts_with(ptr, "Content-Type: "))
		{
			const char* type = &ptr[14];
			if (starts_with(type, "application/xml") || starts_with(type, "text/xml"))
				task->content_type = ContentType::kXmlContent;
			else if (starts_with(type, "x-application/octet-stream"))
				task->content_type = ContentType::kBinaryFile;
			else if (starts_with(type, "text/html"))
				task->content_type = ContentType::kText;
			else
			{
				cemuLog_logDebug(LogType::Force, "task_header_callback: unknown content type > {}", type);
			}
		}
		else if (starts_with(ptr, "Last-Modified: "))
		{
			// TODO timestamp (?)
		}

		//cemuLog_logDebug(LogType::Force, "task_header_callback: len {} ({}) and type {}", task->content_length, task->result_buffer.capacity(), task->content_type);
		//cemuLog_logDebug(LogType::Force, "\t{}", ptr);
		return size * nitems;
	}

	static CURLcode task_sslctx_function(CURL* curl, void* sslctx, void* param)
	{
		auto task_settings = (TaskSetting*)param;
		if (task_settings->taskType == kRawDlTaskSetting)
		{
			cemuLog_logDebug(LogType::Force, "sslctx_function: adding client cert: {}", (int)task_settings->settings[TaskSetting::kClientCert]);
			if (!iosuCrypto_addClientCertificate(sslctx, task_settings->settings[TaskSetting::kClientCert]))
				assert_dbg();

			uint32 location = TaskSetting::kCACert;
			for (int i = 0; i < 3; ++i)
			{
				if (task_settings->settings[location] != 0)
				{
					cemuLog_logDebug(LogType::Force, "sslctx_function: adding ca cert: {}", (int)task_settings->settings[location]);
					if (!iosuCrypto_addCACertificate(sslctx, task_settings->settings[location]))
					{
						cemuLog_log(LogType::Force, "Failed to load CA certificate file");
						assert_dbg();
					}
				}

				location += TaskSetting::kCACert;
			}
		}
		else
		{
			if (!iosuCrypto_addCACertificate(sslctx, 105))
			{
				cemuLog_log(LogType::Force, "Failed to load certificate file");
				assert_dbg();
			}

			if (!iosuCrypto_addClientCertificate(sslctx, 3))
			{
				cemuLog_log(LogType::Force, "Failed to load client certificate file");
				assert_dbg();
			}
		}

		SSL_CTX_set_cipher_list((SSL_CTX*)sslctx, "AES256-SHA");
		// TLS_RSA_WITH_AES_256_CBC_SHA (in CURL it's called rsa_aes_256_sha)
		SSL_CTX_set_mode((SSL_CTX*)sslctx, SSL_MODE_AUTO_RETRY);
		SSL_CTX_set_verify_depth((SSL_CTX*)sslctx, 2);
		SSL_CTX_set_verify((SSL_CTX*)sslctx, SSL_VERIFY_PEER, nullptr);
		return CURLE_OK;
	}

	auto get_task(const char* taskId, uint32 accountId, uint64 titleId)
	{
		const auto it = std::find_if(g_boss.tasks.begin(), g_boss.tasks.end(), [taskId, accountId, titleId](const Task& task)
		{
			return 0 == strncmp(taskId, task.task_id, sizeof(Task::task_id)) && accountId == task.account_id && titleId == task.
				title_id;
		});

		return it;
	}

	bool parse_xml_content(Task& task)
	{
		tinyxml2::XMLDocument doc;
		//cemuLog_log(LogType::Force, (char*)task.result_buffer.data());
		if (doc.Parse((const char*)task.result_buffer.data(), task.processed_length) != tinyxml2::XML_SUCCESS)
			return false;

		for (tinyxml2::XMLElement* sheet = doc.FirstChildElement("TaskSheet"); sheet; sheet = sheet->
		     NextSiblingElement("TaskSheet"))
		{
			const auto files = sheet->FirstChildElement("Files");
			if (!files)
				continue;

			for (tinyxml2::XMLElement* file = files->FirstChildElement("File"); file; file = file->NextSiblingElement("File"))
			{
				auto file_name = file->FirstChildElement("Filename");
				if (!file_name)
					continue;

				auto data_id = file->FirstChildElement("DataId");
				if (!data_id)
					continue;

				auto type = file->FirstChildElement("Type");
				if (!type)
					continue;

				auto url = file->FirstChildElement("Url");
				if (!url)
					continue;

				auto size = file->FirstChildElement("Size");
				if (!size)
					continue;

				FileType file_type;
				if (0 == strcmp(type->GetText(), "AppData"))
					file_type = FileType::kAppData;
				else
				{
					file_type = FileType::kUnknownFile;
				}

				task.queued_files.emplace(file_name->GetText(), data_id->IntText(), file_type, url->GetText(), size->IntText());
			}
		}

		return true;
	}

	const uint64 kTimeStampConvertSeconds = 946684800ULL;
	BossStorageFadEntry* boss_storage_fad_find_entry(BossStorageFadFile& fad_file, uint32 data_id)
	{
		for (auto& entry : fad_file.entries)
		{
			if (entry.fileNameId == data_id)
				return &entry;
		}

		return nullptr;
	}

	void boss_storage_fad_append_or_update(BossStorageFadFile& fad_file, const char* name, uint32 data_id, uint64 timestamp)
	{
		for (auto& entry : fad_file.entries)
		{
			if (entry.fileNameId == 0 || strcmp(entry.name, name) == 0)
			{
				entry.fileNameId = data_id;
				strcpy(entry.name, name);
				entry.timestampRelated = (uint32)(timestamp - kTimeStampConvertSeconds); // time since 2000
				return;
			}
		}
	}

	uint32 task_run(const char* taskId, uint32 accountId, uint64 titleId)
	{
		const auto it = get_task(taskId, accountId, titleId);
		if (it == g_boss.tasks.cend())
		{
			//it->turn_state = kError;
			//it->wait_state = TRUE;
			return BUILD_NN_RESULT(NN_RESULT_LEVEL_FATAL, NN_RESULT_MODULE_NN_BOSS, 0);
		}

		if (!ActiveSettings::IsOnlineEnabled())
		{
			it->turn_state = kError;
			it->wait_state = TRUE;
			return BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_BOSS, 0);
		}
		cemuLog_logDebug(LogType::Force, "task run state: {} | exec: {} (tasks: {})", it->turn_state, it->exec_count, g_boss.tasks.size());
		it->turn_state = kRunning;
		it->exec_count++;

		/*
	https://nppl.app.nintendo.net/p01/policylist/1/1/AT
	https://npts.app.nintendo.net/p01/tasksheet/1/zvGSM4kO***kKnpT/schdat2?c=XX&l=en
	https://npts.app.nintendo.net/p01/tasksheet/1/zvGSM4kO***kKnpT/optdat2?c=XX&l=en
	https://npts.app.nintendo.net/p01/tasksheet/1/8UsM86l***jFk8z/wood1?c=XX&l=en
	https://npts.app.nintendo.net/p01/tasksheet/1/8UsM86l***kjFk8z/woodBGM?c=XX&l=en
	
	
	https://npts.app.nintendo.net/p01/tasksheet/%s/%s/%s/%s?c=%s&l=%s
	https://npts.app.nintendo.net/p01/tasksheet/%s/%s/%s?c=%s&l=%s
		1 == version
		bossCode
		initFile
	
		 */

		uint32 turnstate = kSuccess;
		struct curl_slist* list_headerParam = nullptr;
		list_headerParam = append_header_param(list_headerParam, "X-BOSS-Digest"); // ??? 
		list_headerParam = append_header_param(list_headerParam, "X-Boss-UniqueId: {:05x}", ((titleId >> 8) & 0xFFFFF)); // %05x
		list_headerParam = append_header_param(list_headerParam, "X-BOSS-TitleId: /usr/packages/title/{:016x}", titleId); // "/usr/packages/title/%016llx"
		
		CURL* curl = it->curl.get();
		curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
		curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 2);
#ifdef CEMU_DEBUG_ASSERT
		curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
		char errbuf[CURL_ERROR_SIZE]{};
		curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
#else
		curl_easy_setopt(curl, CURLOPT_VERBOSE, 0);
#endif
		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, task_write_callback);
		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &(*it));
		curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, task_header_callback);
		curl_easy_setopt(curl, CURLOPT_HEADERDATA, &(*it));
		curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0x3C);
		if (IsNetworkServiceSSLDisabled(ActiveSettings::GetNetworkService()))
		{ 
			curl_easy_setopt(curl,CURLOPT_SSL_VERIFYPEER,0L);
		}
		else 
		{
			curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, task_sslctx_function);
			curl_easy_setopt(curl, CURLOPT_SSL_CTX_DATA, &it->task_settings);
			curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_0);
		}

		std::string requestUrl;
		if(it->task_settings.taskType == kRawDlTaskSetting)
		{
			char serviceToken[TaskSetting::kServiceTokenLen];
			strncpy(serviceToken, (char*)&it->task_settings.settings[TaskSetting::kServiceToken], TaskSetting::kServiceTokenLen);
			list_headerParam = append_header_param(list_headerParam, "X-Nintendo-ServiceToken: {}", serviceToken);

			char url[TaskSetting::kURLLen + 1]{};
			strncpy(url, (char*)&it->task_settings.settings[TaskSetting::kURL], TaskSetting::kURLLen);
			requestUrl.assign(url);
		}
		else
		{
			char languageCode[8];
			switch (GetConfig().console_language)
			{
			case CafeConsoleLanguage::JA:
				strcpy(languageCode, "ja");
				break;
			case CafeConsoleLanguage::EN:
				strcpy(languageCode, "en");
				break;
			case CafeConsoleLanguage::FR:
				strcpy(languageCode, "fr");
				break;
			case CafeConsoleLanguage::DE:
				strcpy(languageCode, "de");
				break;
			case CafeConsoleLanguage::IT:
				strcpy(languageCode, "it");
				break;
			case CafeConsoleLanguage::ES:
				strcpy(languageCode, "es");
				break;
			case CafeConsoleLanguage::ZH:
				strcpy(languageCode, "zh");
				break;
			case CafeConsoleLanguage::KO:
				strcpy(languageCode, "ko");
				break;
			case CafeConsoleLanguage::NL:
				strcpy(languageCode, "nl");
				break;
			case CafeConsoleLanguage::PT:
				strcpy(languageCode, "pt");
				break;
			case CafeConsoleLanguage::RU:
				strcpy(languageCode, "ru");
				break;
			case CafeConsoleLanguage::TW:
				strcpy(languageCode, "tw"); // usually zh-tw?
				break;
			default:
				strcpy(languageCode, "en");
				break;
			}

			const char* countryCode = NCrypto::GetCountryAsString(Account::GetCurrentAccount().GetCountry());

			char boss_code[0x20];
			strncpy(boss_code, (char*)&it->task_settings.settings[TaskSetting::kBossCode], TaskSetting::kBossCodeLen);

			switch (ActiveSettings::GetNetworkService())
			{
			case NetworkService::Pretendo:
				requestUrl = PretendoURLs::BOSSURL;
				break;
			case NetworkService::Custom:
				requestUrl = GetNetworkConfig().urls.BOSS.GetValue();
				break;
			case NetworkService::Nintendo:
			default:
				requestUrl = NintendoURLs::BOSSURL;
				break;
			}
			requestUrl.append(fmt::format(fmt::runtime("/{}/{}/{}?c={}&l={}"), "1", boss_code, it->task_id, countryCode, languageCode));
		}

		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list_headerParam);
		curl_easy_setopt(curl, CURLOPT_URL, requestUrl.c_str());

		int curl_result = curl_easy_perform(curl);
		curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &it->http_status_code);
		static_assert(sizeof(it->http_status_code) == sizeof(long));

		//it->turn_state = kFinished;

		curl_slist_free_all(list_headerParam);
		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, nullptr);

		if (curl_result != CURLE_OK)
		{
#ifdef CEMU_DEBUG_ASSERT
			cemuLog_logDebug(LogType::Force, "curl error buff: {}", errbuf);
#endif
			it->turn_state = kError;
			it->wait_state = TRUE;
			cemuLog_logDebug(LogType::Force, "task_run curl fail: {}", curl_result);
			return BUILD_NN_RESULT(NN_RESULT_LEVEL_FATAL, NN_RESULT_MODULE_NN_BOSS, 0);
		}
		else
		{
			if (it->http_status_code != 200)
			{
				cemuLog_logDebug(LogType::Force, "BOSS task_run: Received unexpected HTTP response code");
			}
			if (it->http_status_code == 404)
			{
				// todo - is this correct behavior?
				it->turn_state = kError;
				it->wait_state = TRUE;
				cemuLog_logDebug(LogType::Force, "task_run failed due to 404 error");
				return BUILD_NN_RESULT(NN_RESULT_LEVEL_FATAL, NN_RESULT_MODULE_NN_BOSS, 0);
			}
		}

		switch (it->content_type)
		{
		case ContentType::kXmlContent:
			parse_xml_content(*it);
			break;
		case ContentType::kBinaryFile:

			break;
		case ContentType::kText:
			cemuLog_logDebug(LogType::Force, "task_run returns text: {}", fmt::ptr(it->result_buffer.data()));
			break;
		}


		if (!it->queued_files.empty())
		{
			curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, task_download_header_callback);
			curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, task_download_filecallback);

			std::string taskIdStr = it->task_id;

			try
			{
				fs::path path = ActiveSettings::GetMlcPath("usr/boss/{:08x}/{:08x}/user/common/data/{}", (uint32)(it->title_id >> 32), (uint32)(it->title_id & 0xFFFFFFFF), taskIdStr);
				if (!fs::exists(path))
					fs::create_directories(path);

				char targetFileName[TaskSetting::kFileNameLen + 1]{};
				strncpy(targetFileName, (char*)&it->task_settings.settings[TaskSetting::kNbdlFileName], TaskSetting::kFileNameLen);
				cemuLog_logDebug(LogType::Force, "\tnbdl task target filename: \"{}\"", targetFileName);
				const bool hasFileName = targetFileName[0] != '\0';

				while (!it->queued_files.empty())
				{
					auto file = it->queued_files.front();
					it->queued_files.pop();
					// download only specific file
					if (hasFileName && file.file_name != targetFileName)
						continue;

					it->processed_file_size = 0;
					it->file_buffer.clear();
					it->file_buffer.reserve(file.size);

					// create/open fad db content
					BossStorageFadFile fad_content{};
					fs::path db_file = ActiveSettings::GetMlcPath("usr/boss/{:08x}/{:08x}/user/common/{:08x}/{}", (uint32)(it->title_id >> 32), (uint32)(it->title_id & 0xFFFFFFFF), it->account_id, taskIdStr);
					if (!fs::exists(db_file))
						fs::create_directories(db_file);

					db_file /= "fad.db";
					std::ifstream fad_file(db_file, std::ios::in | std::ios::binary);
					if (fad_file.is_open())
					{
						if (!fad_file.read((char*)&fad_content, sizeof(BossStorageFadFile)))
							fad_content = {};

						fad_file.close();
					}

					auto currentEntry = boss_storage_fad_find_entry(fad_content, file.data_id);
					//TODO deep dive into IOSU to figure out how caching actually works on th Wii U
					if(currentEntry && fs::exists(path / fmt::format(L"{:08x}", file.data_id)))
					{
						uint64 timestamp = (uint64)currentEntry->timestampRelated + kTimeStampConvertSeconds;
						curl_easy_setopt(curl, CURLOPT_TIMEVALUE, timestamp);
						curl_easy_setopt(curl, CURLOPT_TIMECONDITION, CURL_TIMECOND_IFMODSINCE);
					}
					else
					{
						curl_easy_setopt(curl, CURLOPT_TIMEVALUE, 0);
						curl_easy_setopt(curl, CURLOPT_TIMECONDITION, CURL_TIMECOND_NONE);
					}

					curl_easy_setopt(curl, CURLOPT_FILETIME, 1L);
					curl_easy_setopt(curl, CURLOPT_HEADERDATA, task_download_header_callback);
					curl_easy_setopt(curl, CURLOPT_URL, file.url.c_str());
					curl_result = curl_easy_perform(curl);
					if (curl_result != CURLE_OK)
					{
						cemuLog_logDebug(LogType::Force, "task_run curl failed on file download ({}): {} > {}", curl_result, file.file_name, file.url);
						if (hasFileName)
						{
							turnstate = kError;
							break;
						}
						else
							continue;
					}

					long unmet = 1;
					const CURLcode result = curl_easy_getinfo(curl, CURLINFO_CONDITION_UNMET, &unmet);
					if(result == CURLE_OK && unmet == 1)
					{
						// file is already up2date
						if (hasFileName)
							break;
						else
							continue;
					}

					if(it->processed_file_size != file.size)
					{
						cemuLog_logDebug(LogType::Force, "task_run file download size mismatch: {} > {} > {} from {} bytes", file.file_name, file.url, it->processed_file_size, file.size);
						if (hasFileName)
						{
							turnstate = kError;
							break;
						}
						else
							continue;
					}

					uint64 filetime;
					curl_easy_getinfo(curl, CURLINFO_FILETIME, &filetime);

					// dunno about that one
					it->processed_length += it->processed_file_size;
					it->content_length += file.size;

					// bossAesKey = OTP.XorKey ^ bossXor
					// bossXor: 33 AC 6D 15 C2 26 0A 91 3B BF 73 C3 55 D8 66 04
					uint8 bossAesKey[16] = { 0x39,0x70,0x57,0x35,0x58,0x70,0x34,0x58,0x37,0x41,0x7a,0x30,0x71,0x5a,0x70,0x74 }; // "9pW5Xp4X7Az0qZpt"
					
					BossNbdlHeader* nbdlHeader = (BossNbdlHeader*)it->file_buffer.data();

					if (nbdlHeader->magic != 'boss')
						break;
					if (nbdlHeader->version != 0x20001)
						break;
					if (nbdlHeader->ukn08 != 1)
						break;
					if (nbdlHeader->ukn0A != 2)
						break;

					// file must be padded to 16 byte alignment for AES decryption (padding is cut off after decryption)
					const uint32 file_size = (it->processed_file_size + 0xF) & (~0xF);
					if (file_size != it->processed_file_size)
					{
						it->file_buffer.resize(file_size);
						nbdlHeader = (BossNbdlHeader*)it->file_buffer.data();
					}
					
					// prepare nonce for AES128-CTR
					uint8 aesNonce[0x10];
					memset(aesNonce, 0, sizeof(aesNonce));
					memcpy(aesNonce, nbdlHeader->nonce, 0xC);
					aesNonce[0xF] = 1;

					// decrypt header
					AES128CTR_transform(it->file_buffer.data() + offsetof(BossNbdlHeader, encryptedHeader), sizeof(BossNbdlHeader::encryptedHeader), bossAesKey, aesNonce);
					// decrypt everything else
					AES128CTR_transform(it->file_buffer.data() + sizeof(BossNbdlHeader), file_size - sizeof(BossNbdlHeader), bossAesKey, aesNonce);

					try
					{
						// create file with content
						fs::path file_path = path / fmt::format(L"{:08x}", file.data_id);
						std::ofstream new_file(file_path, std::ios::out | std::ios::binary | std::ios::trunc);
						new_file.write((char*)it->file_buffer.data() + sizeof(BossNbdlHeader), it->processed_file_size - sizeof(BossNbdlHeader));
						new_file.flush();
						new_file.close();


						boss_storage_fad_append_or_update(fad_content, file.file_name.c_str(), file.data_id, filetime);
						std::ofstream fad_file_updated(db_file, std::ios::out | std::ios::binary | std::ios::trunc);
						fad_file_updated.write((char*)&fad_content, sizeof(BossStorageFadFile));
						fad_file_updated.flush();
						fad_file_updated.close();
					}
					catch (const std::exception& ex)
					{
						cemuLog_logDebug(LogType::Force, "file error: {}", ex.what());
					}

					if (hasFileName)
						break;
				}
			}
			catch (const std::exception& ex)
			{
				cemuLog_logDebug(LogType::Force, "dir error: {}", ex.what());
			}
		}


		if (it->task_settings.taskType == kRawDlTaskSetting)
		{
			char directoryName[TaskSetting::kDirectoryNameLen + 1]{};
			if (it->task_settings.settings[TaskSetting::kDirectoryName] != '\0')
				strncpy(directoryName, (char*)&it->task_settings.settings[TaskSetting::kDirectoryName], TaskSetting::kDirectoryNameLen);
			else
				strncpy(directoryName, it->task_id, TaskSetting::kDirectoryNameLen);

			char fileName[TaskSetting::kFileNameLen + 1]{};
			strncpy(fileName, (char*)&it->task_settings.settings[TaskSetting::kFileName], TaskSetting::kFileNameLen);

			//  mcl01\usr\boss\00050000\1018dd00\user\<persistentId>\<storageName>\<filename>
			fs::path path = ActiveSettings::GetMlcPath("usr/boss/{:08x}/{:08x}/user/{:08x}", (uint32)(it->title_id >> 32),
				(uint32)(it->title_id & 0xFFFFFFFF), iosuAct_getAccountIdOfCurrentAccount());
			path /= directoryName;

			if (!fs::exists(path))
				fs::create_directories(path);

			path /= fileName;

			std::ofstream file(path);
			if (file.is_open())
			{
				file.write((char*)it->result_buffer.data(), it->result_buffer.size());
				file.flush();
				file.close();
			}
		}

		it->turn_state = turnstate;
		it->wait_state = TRUE;
		return BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_BOSS, 0);
	}


	bool task_is_registered(const char* taskId, uint32 accountId, uint64 titleId)
	{
		const auto it = get_task(taskId, accountId, titleId);
		return it != g_boss.tasks.cend();
	}

	bool task_wait(const char* taskId, uint32 accountId, uint64 titleId, uint32 wait_state, uint32 timeout = 0)
	{
		const auto it = get_task(taskId, accountId, titleId);
		if (it == g_boss.tasks.cend())
		{
			return false;
		}

		const auto start = tick_cached();
		while (it->wait_state != wait_state)
		{
			if (timeout != 0 && (uint32)std::chrono::duration_cast<std::chrono::seconds>(tick_cached() - start).count() >= timeout)
			{
				cemuLog_logDebug(LogType::Force, "task_wait: timeout reached -> {} seconds passed", timeout);
				return false;
			}

			std::this_thread::yield();
			std::this_thread::sleep_for(std::chrono::milliseconds(1));
		}

		return true;
	}

	uint32 task_register(const char* taskId, uint32 accountId, uint64 titleId, void* settings)
	{
		g_boss.tasks.emplace_back(taskId, accountId, titleId, (TaskSetting*)settings);
		g_boss.tasks[g_boss.tasks.size() - 1].turn_state = kWaitTime;
		return BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_BOSS, 0);
	}

	uint32 task_register_immediate_run(const char* taskId, uint32 accountId, uint64 titleId, void* settings)
	{
		g_boss.tasks.emplace_back(taskId, accountId, titleId, (TaskSetting*)settings);
		g_boss.tasks[g_boss.tasks.size() - 1].turn_state = kWaitRun;
		return BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_BOSS, 0);
	}

	void task_unregister(const char* taskId, uint32 accountId, uint64 titleId)
	{
		const auto it = get_task(taskId, accountId, titleId);
		if (it != g_boss.tasks.cend())
			g_boss.tasks.erase(it);
	}

	std::pair<uint32, uint64> task_get_content_length(const char* taskId, uint32 accountId, uint64 titleId)
	{
		const auto it = get_task(taskId, accountId, titleId);
		return it != g_boss.tasks.cend() ? std::make_pair(it->exec_count, it->content_length) : std::make_pair(0u, (uint64)0);
	}

	std::pair<uint32, uint64> task_get_processed_length(const char* taskId, uint32 accountId, uint64 titleId)
	{
		const auto it = get_task(taskId, accountId, titleId);
		return it != g_boss.tasks.cend() ? std::make_pair(it->exec_count, it->processed_length) : std::make_pair(0u, (uint64)0);
	}

	std::pair<uint32, long> task_get_http_status_code(const char* taskId, uint32 accountId, uint64 titleId)
	{
		const auto it = get_task(taskId, accountId, titleId);
		return it != g_boss.tasks.cend() ? std::make_pair(it->exec_count, it->http_status_code) : std::make_pair(0u, (long)0);
	}

	std::pair<uint32, uint32> task_get_turn_state(const char* taskId, uint32 accountId, uint64 titleId)
	{
		const auto it = get_task(taskId, accountId, titleId);
		return it != g_boss.tasks.cend() ? std::make_pair(it->exec_count, it->turn_state) : std::make_pair(0u, (uint32)0);
	}

	uint32 task_stop_scheduling(const char* task_id, uint32 account_id, uint64 title_id)
	{
		const auto it = get_task(task_id, account_id, title_id);
		if (it != g_boss.tasks.cend())
		{
			it->turn_state = kStopped;
			// todo actually cancel the task if currently running (curl call)
			// curl_easy_pause() -> resume on start scheduling if paused
		}

		return BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_BOSS, 0);
	}

	void boss_thread()
	{
		SetThreadName("boss_thread");
		while (true)
		{
			const uint32 return_value = 0; // Ioctl return value

			ioQueueEntry_t* ioQueueEntry = iosuIoctl_getNextWithWait(IOS_DEVICE_BOSS);
			if (ioQueueEntry->request == IOSU_BOSS_REQUEST_CEMU)
			{
				iosuBossCemuRequest_t* cemu_request = (iosuBossCemuRequest_t*)ioQueueEntry->bufferVectors[0].buffer.GetPtr();
				cemu_request->returnCode = 0;

				uint64 title_id;
				if (cemu_request->titleId == 0)
					title_id = CafeSystem::GetForegroundTitleId();
				else
					title_id = cemu_request->titleId;

				uint32 account_id;
				if (cemu_request->accountId == 0)
					account_id = iosuAct_getAccountIdOfCurrentAccount();
				else
					account_id = cemu_request->accountId;

				if (cemu_request->requestCode == IOSU_NN_BOSS_TASK_RUN)
				{
					cemu_request->returnCode = task_run(cemu_request->taskId, account_id, title_id);
				}
				else if (cemu_request->requestCode == IOSU_NN_BOSS_TASK_GET_CONTENT_LENGTH)
				{
					auto result = task_get_content_length(cemu_request->taskId, account_id, title_id);
					cemu_request->u64.exec_count = std::get<0>(result);
					cemu_request->u64.result = std::get<1>(result);
				}
				else if (cemu_request->requestCode == IOSU_NN_BOSS_TASK_GET_PROCESSED_LENGTH)
				{
					auto result = task_get_processed_length(cemu_request->taskId, account_id, title_id);
					cemu_request->u64.exec_count = std::get<0>(result);
					cemu_request->u64.result = std::get<1>(result);
				}
				else if (cemu_request->requestCode == IOSU_NN_BOSS_TASK_GET_HTTP_STATUS_CODE)
				{
					auto result = task_get_http_status_code(cemu_request->taskId, account_id, title_id);
					cemu_request->u32.exec_count = std::get<0>(result);
					cemu_request->u32.result = std::get<1>(result);
				}
				else if (cemu_request->requestCode == IOSU_NN_BOSS_TASK_GET_TURN_STATE)
				{
					auto result = task_get_turn_state(cemu_request->taskId, account_id, title_id);
					cemu_request->u32.exec_count = std::get<0>(result);
					cemu_request->u32.result = std::get<1>(result);
				}
				else if (cemu_request->requestCode == IOSU_NN_BOSS_TASK_WAIT)
				{
					cemu_request->returnCode = task_wait(cemu_request->taskId, account_id, title_id, cemu_request->waitState,
					                                     cemu_request->timeout);
				}
				else if (cemu_request->requestCode == IOSU_NN_BOSS_TASK_REGISTER)
				{
					cemu_request->returnCode = task_register(cemu_request->taskId, account_id, title_id, cemu_request->settings);
				}
				else if (cemu_request->requestCode == IOSU_NN_BOSS_TASK_IS_REGISTERED)
				{
					cemu_request->returnCode = task_is_registered(cemu_request->taskId, account_id, title_id) ? TRUE : FALSE;
				}
				else if (cemu_request->requestCode == IOSU_NN_BOSS_TASK_REGISTER_FOR_IMMEDIATE_RUN)
				{
					cemu_request->returnCode = task_register_immediate_run(cemu_request->taskId, account_id, title_id,
					                                                       cemu_request->settings);
				}
				else if (cemu_request->requestCode == IOSU_NN_BOSS_TASK_UNREGISTER)
				{
					task_unregister(cemu_request->taskId, account_id, title_id);
				}
				else if (cemu_request->requestCode == IOSU_NN_BOSS_TASK_START_SCHEDULING)
				{
					// we just run it no matter what
					//if(cemu_request->bool_parameter)
						cemu_request->returnCode = task_run(cemu_request->taskId, account_id, title_id);
					/*else
					{
						const auto it = get_task(cemu_request->taskId, account_id, title_id);
						if (it != g_boss.tasks.cend())
						{
							it->turn_state = kWaitRun;
						}
					}*/
				}
				else if (cemu_request->requestCode == IOSU_NN_BOSS_TASK_STOP_SCHEDULING)
				{
					cemu_request->returnCode = task_stop_scheduling(cemu_request->taskId, account_id, title_id);
				}
				else
					assert_dbg();
			}
			else
				assert_dbg();

			iosuIoctl_completeRequest(ioQueueEntry, return_value);
		}
	}

	void boss_init()
	{
		if (g_boss.is_initialized)
			return;

		// start the boss thread
		std::thread t(boss_thread);
		t.detach();

		g_boss.is_initialized = true;
	}
}
