#include "pch.h"
#include "serverauthentication.h"
#include "convar.h"
#include "hookutils.h"
#include "masterserver.h"
#include "httplib.h"
#include "gameutils.h"
#include <fstream>
#include <filesystem>
#include <thread>

const char* AUTHSERVER_VERIFY_STRING = "I am a northstar server!";

// hook types

typedef void*(*CBaseServer__ConnectClientType)(void* server, void* a2, void* a3, uint32_t a4, uint32_t a5, int32_t a6, void* a7, void* a8, char* serverFilter, void* a10, char a11, void* a12, char a13, char a14, int64_t uid, uint32_t a16, uint32_t a17);
CBaseServer__ConnectClientType CBaseServer__ConnectClient;

typedef char(*CBaseClient__ConnectType)(void* self, char* name, __int64 netchan_ptr_arg, char b_fake_player_arg, __int64 a5, char* Buffer, int a7);
CBaseClient__ConnectType CBaseClient__Connect;

typedef void(*CBaseClient__ActivatePlayerType)(void* self);
CBaseClient__ActivatePlayerType CBaseClient__ActivatePlayer;

typedef void(*CBaseClient__DisconnectType)(void* self, uint32_t unknownButAlways1, const char* reason, ...);
CBaseClient__DisconnectType CBaseClient__Disconnect;

typedef char(*CGameClient__ExecuteStringCommandType)(void* self, uint32_t unknown, const char* pCommandString);
CGameClient__ExecuteStringCommandType CGameClient__ExecuteStringCommand;

// global vars
ServerAuthenticationManager* g_ServerAuthenticationManager;

ConVar* Cvar_ns_player_auth_port;
ConVar* Cvar_ns_erase_auth_info;
ConVar* CVar_ns_auth_allow_insecure;
ConVar* CVar_ns_auth_allow_insecure_write;
ConVar* CVar_sv_quota_stringcmdspersecond;

void ServerAuthenticationManager::StartPlayerAuthServer()
{
	if (m_runningPlayerAuthThread)
	{
		spdlog::warn("ServerAuthenticationManager::StartPlayerAuthServer was called while m_runningPlayerAuthThread is true");
		return;
	}

	m_runningPlayerAuthThread = true;

	// listen is a blocking call so thread this
	std::thread serverThread([this] {
			// this is just a super basic way to verify that servers have ports open, masterserver will try to read this before ensuring server is legit
			m_playerAuthServer.Get("/verify", [](const httplib::Request& request, httplib::Response& response) {
					response.set_content(AUTHSERVER_VERIFY_STRING, "text/plain");
				});

			m_playerAuthServer.Post("/authenticate_incoming_player", [this](const httplib::Request& request, httplib::Response& response) {
					if (!request.has_param("id") || !request.has_param("authToken") || request.remote_addr != Cvar_ns_masterserver_hostname->m_pszString)
					{
						response.set_content("{\"success\":false}", "application/json");
						return;
					}

					AuthData newAuthData;
					strncpy(newAuthData.uid, request.get_param_value("id").c_str(), sizeof(newAuthData.uid));
					newAuthData.uid[sizeof(newAuthData.uid) - 1] = 0;

					newAuthData.pdataSize = request.body.size();
					newAuthData.pdata = new char[newAuthData.pdataSize];
					memcpy(newAuthData.pdata, request.body.c_str(), newAuthData.pdataSize);

					std::lock_guard<std::mutex> guard(m_authDataMutex);
					m_authData.insert(std::make_pair(request.get_param_value("authToken"), newAuthData));

					response.set_content("{\"success\":true}", "application/json");
				});

			m_playerAuthServer.listen("0.0.0.0", Cvar_ns_player_auth_port->m_nValue);
		});
	
	serverThread.detach();
}

void ServerAuthenticationManager::StopPlayerAuthServer()
{
	if (!m_runningPlayerAuthThread)
	{
		spdlog::warn("ServerAuthenticationManager::StopPlayerAuthServer was called while m_runningPlayerAuthThread is false");
		return;
	}

	m_runningPlayerAuthThread = false;
	m_playerAuthServer.stop();
}

bool ServerAuthenticationManager::AuthenticatePlayer(void* player, int64_t uid, char* authToken)
{
	std::string strUid = std::to_string(uid);

	std::lock_guard<std::mutex> guard(m_authDataMutex);

	bool authFail = true;
	if (!m_authData.empty() && m_authData.count(std::string(authToken)))
	{
		// use stored auth data
		AuthData authData = m_authData[authToken];
		if (!strcmp(strUid.c_str(), authData.uid)) // connecting client's uid is different from auth's uid
		{
			authFail = false;
			// uuid
			strcpy((char*)player + 0xF500, strUid.c_str());

			// copy pdata into buffer
			memcpy((char*)player + 0x4FA, authData.pdata, authData.pdataSize);

			// set persistent data as ready, we use 0x4 internally to mark the client as using remote persistence
			*((char*)player + 0x4a0) = (char)0x4;
		}
	}


	if (authFail)
	{
		if (!CVar_ns_auth_allow_insecure->m_nValue) // no auth data and insecure connections aren't allowed, so dc the client
			return false;

		// insecure connections are allowed, try reading from disk
		// uuid
		strcpy((char*)player + 0xF500, strUid.c_str());

		// try reading pdata file for player
		std::string pdataPath = "playerdata/playerdata_";
		pdataPath += strUid;
		pdataPath += ".pdata";

		std::fstream pdataStream(pdataPath, std::ios_base::in);
		if (pdataStream.fail()) // file doesn't exist, use placeholder
			pdataStream = std::fstream("playerdata/placeholder_playerdata.pdata");
		
		// get file length
		pdataStream.seekg(0, pdataStream.end);
		int length = pdataStream.tellg();
		pdataStream.seekg(0, pdataStream.beg);

		// copy pdata into buffer
		pdataStream.read((char*)player + 0x4FA, length);

		pdataStream.close();

		// set persistent data as ready, we use 0x3 internally to mark the client as using local persistence
		*((char*)player + 0x4a0) = (char)0x3;
	}

	return true; // auth successful, client stays on
}

bool ServerAuthenticationManager::RemovePlayerAuthData(void* player)
{
	if (!Cvar_ns_erase_auth_info->m_nValue)
		return false;

	// hack for special case where we're on a local server, so we erase our own newly created auth data on disconnect
	if (m_bNeedLocalAuthForNewgame && !strcmp((char*)player + 0xF500, g_LocalPlayerUserID))
		return false;

	// we don't have our auth token at this point, so lookup authdata by uid
	for (auto& auth : m_authData)
	{
		if (!strcmp((char*)player + 0xF500, auth.second.uid))
		{
			// pretty sure this is fine, since we don't iterate after the erase
			// i think if we iterated after it'd be undefined behaviour tho
			std::lock_guard<std::mutex> guard(m_authDataMutex);

			delete[] auth.second.pdata;
			m_authData.erase(auth.first);
			return true;
		}
	}

	return false;
}

void ServerAuthenticationManager::WritePersistentData(void* player)
{
	// we use 0x4 internally to mark clients as using remote persistence
	if (*((char*)player + 0x4A0) == (char)0x4)
	{
		g_MasterServerManager->WritePlayerPersistentData((char*)player + 0xF500, (char*)player + 0x4FA, m_additionalPlayerData[player].pdataSize);
	}
	else if (CVar_ns_auth_allow_insecure_write->m_nValue)
	{
		// todo: write pdata to disk here
	}
}


// auth hooks

// store these in vars so we can use them in CBaseClient::Connect
// this is fine because ptrs won't decay by the time we use this, just don't use it outside of cbaseclient::connect
char* nextPlayerToken;
int64_t nextPlayerUid;

void* CBaseServer__ConnectClientHook(void* server, void* a2, void* a3, uint32_t a4, uint32_t a5, int32_t a6, void* a7, void* a8, char* serverFilter, void* a10, char a11, void* a12, char a13, char a14, int64_t uid, uint32_t a16, uint32_t a17)
{
	// auth tokens are sent with serverfilter, can't be accessed from player struct to my knowledge, so have to do this here
	nextPlayerToken = serverFilter;
	nextPlayerUid = uid;

	return CBaseServer__ConnectClient(server, a2, a3, a4, a5, a6, a7, a8, serverFilter, a10, a11, a12, a13, a14, uid, a16, a17);
}

char CBaseClient__ConnectHook(void* self, char* name, __int64 netchan_ptr_arg, char b_fake_player_arg, __int64 a5, char* Buffer, int a7)
{
	// try to auth player, dc if it fails
	// we connect irregardless of auth, because returning bad from this function can fuck client state p bad
	char ret = CBaseClient__Connect(self, name, netchan_ptr_arg, b_fake_player_arg, a5, Buffer, a7);
	if (strlen(name) >= 64) // fix for name overflow bug
		CBaseClient__Disconnect(self, 1, "Invalid name");
	else if (!g_ServerAuthenticationManager->AuthenticatePlayer(self, nextPlayerUid, nextPlayerToken) && g_MasterServerManager->m_bRequireClientAuth)
		CBaseClient__Disconnect(self, 1, "Authentication Failed");

	if (!g_ServerAuthenticationManager->m_additionalPlayerData.count(self))
	{
		AdditionalPlayerData additionalData;
		additionalData.pdataSize = g_ServerAuthenticationManager->m_authData[nextPlayerToken].pdataSize;
		additionalData.usingLocalPdata = *((char*)self + 0x4a0) == (char)0x3;

		g_ServerAuthenticationManager->m_additionalPlayerData.insert(std::make_pair(self, additionalData));
	}

	return ret;
}

void CBaseClient__ActivatePlayerHook(void* self)
{
	// if we're authed, write our persistent data
	// RemovePlayerAuthData returns true if it removed successfully, i.e. on first call only, and we only want to write on >= second call (since this func is called on map loads)
	if (*((char*)self + 0x4A0) >= (char)0x3 && !g_ServerAuthenticationManager->RemovePlayerAuthData(self))
	{
		g_ServerAuthenticationManager->WritePersistentData(self);
		g_MasterServerManager->UpdateServerPlayerCount(g_ServerAuthenticationManager->m_additionalPlayerData.size());
	}

	CBaseClient__ActivatePlayer(self);
}

void CBaseClient__DisconnectHook(void* self, uint32_t unknownButAlways1, const char* reason, ...)
{
	// have to manually format message because can't pass varargs to original func
	char buf[1024];

	va_list va;
	va_start(va, reason);
	vsprintf(buf, reason, va);
	va_end(va);


	// this reason is used while connecting to a local server, hacky, but just ignore it
	if (strcmp(reason, "Connection closing"))
	{
		// dcing, write persistent data
		if (g_ServerAuthenticationManager->m_additionalPlayerData[self].needPersistenceWriteOnLeave)
			g_ServerAuthenticationManager->WritePersistentData(self);
		g_ServerAuthenticationManager->RemovePlayerAuthData(self); // won't do anything 99% of the time, but just in case
	}

	if (g_ServerAuthenticationManager->m_additionalPlayerData.count(self))
	{
		g_ServerAuthenticationManager->m_additionalPlayerData.erase(self);
		g_MasterServerManager->UpdateServerPlayerCount(g_ServerAuthenticationManager->m_additionalPlayerData.size());
	}

	CBaseClient__Disconnect(self, unknownButAlways1, buf);
}

// maybe this should be done outside of auth code, but effort to refactor rn and it sorta fits
char CGameClient__ExecuteStringCommandHook(void* self, uint32_t unknown, const char* pCommandString)
{
	if (CVar_sv_quota_stringcmdspersecond->m_nValue != -1)
	{
		// note: this isn't super perfect, legit clients can trigger it in lobby, mostly good enough tho imo
		// https://github.com/perilouswithadollarsign/cstrike15_src/blob/f82112a2388b841d72cb62ca48ab1846dfcc11c8/engine/sv_client.cpp#L1513
		if (Plat_FloatTime() - g_ServerAuthenticationManager->m_additionalPlayerData[self].lastClientCommandQuotaStart >= 1.0f)
		{
			// reset quota
			g_ServerAuthenticationManager->m_additionalPlayerData[self].lastClientCommandQuotaStart = Plat_FloatTime();
			g_ServerAuthenticationManager->m_additionalPlayerData[self].numClientCommandsInQuota = 0;
		}

		g_ServerAuthenticationManager->m_additionalPlayerData[self].numClientCommandsInQuota++;
		if (g_ServerAuthenticationManager->m_additionalPlayerData[self].numClientCommandsInQuota > CVar_sv_quota_stringcmdspersecond->m_nValue)
		{
			// too many stringcmds, dc player
			CBaseClient__Disconnect(self, 1, "Sent too many stringcmd commands");
			return false;
		}
	}

	// todo later, basically just limit to CVar_sv_quota_stringcmdspersecond->m_nValue stringcmds per client per second
	return CGameClient__ExecuteStringCommand(self, unknown, pCommandString);
}

void InitialiseServerAuthentication(HMODULE baseAddress)
{
	g_ServerAuthenticationManager = new ServerAuthenticationManager;

	Cvar_ns_erase_auth_info = RegisterConVar("ns_erase_auth_info", "1", FCVAR_GAMEDLL, "Whether auth info should be erased from this server on disconnect or crash");
	CVar_ns_auth_allow_insecure = RegisterConVar("ns_auth_allow_insecure", "0", FCVAR_GAMEDLL, "Whether this server will allow unauthenicated players to connect");
	CVar_ns_auth_allow_insecure_write = RegisterConVar("ns_auth_allow_insecure_write", "0", FCVAR_GAMEDLL, "Whether the pdata of unauthenticated clients will be written to disk when changed");
	// literally just stolen from a fix valve used in csgo
	CVar_sv_quota_stringcmdspersecond = RegisterConVar("sv_quota_stringcmdspersecond", "60", FCVAR_GAMEDLL, "How many string commands per second clients are allowed to submit, 0 to disallow all string commands");
	Cvar_ns_player_auth_port = RegisterConVar("ns_player_auth_port", "8081", FCVAR_GAMEDLL, "");

	HookEnabler hook;
	ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x114430, &CBaseServer__ConnectClientHook, reinterpret_cast<LPVOID*>(&CBaseServer__ConnectClient));
	ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x101740, &CBaseClient__ConnectHook, reinterpret_cast<LPVOID*>(&CBaseClient__Connect));
	ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x100F80, &CBaseClient__ActivatePlayerHook, reinterpret_cast<LPVOID*>(&CBaseClient__ActivatePlayer));
	ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x1012C0, &CBaseClient__DisconnectHook, reinterpret_cast<LPVOID*>(&CBaseClient__Disconnect));
	ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x1022E0, &CGameClient__ExecuteStringCommandHook, reinterpret_cast<LPVOID*>(&CGameClient__ExecuteStringCommand));

	// patch to disable kicking based on incorrect serverfilter in connectclient, since we repurpose it for use as an auth token
	{
		void* ptr = (char*)baseAddress + 0x114655;
		TempReadWrite rw(ptr);
		*((char*)ptr) = (char)0xEB; // jz => jmp
	}

	// patch to disable fairfight marking players as cheaters and kicking them
	{
		void* ptr = (char*)baseAddress + 0x101012;
		TempReadWrite rw(ptr);
		*((char*)ptr) = (char)0xE9; // jz => jmp
		*((char*)ptr + 1) = (char)0x90;
		*((char*)ptr + 2) = (char)0x0;
	}

	// patch to allow same of multiple account
	{
		void* ptr = (char*)baseAddress + 0x114510;
		TempReadWrite rw(ptr);
		*((char*)ptr) = (char)0xEB; // jz => jmp
	}

}