#include "capesdk.h"
#include "twitchsdk/core/httprequest.h"
#include "twitchsdk/core/json/writer.h"
#include "version.inl"

namespace json = ttv::json;
using namespace TwitchInGames;

namespace {
	struct ScienceEvent {
		using vector= std::vector<ScienceEvent>;

		tstring clientId, token, userId, method;
		std::map<tstring, tstring> properties;
	};

	std::atomic<ScienceEvent::vector*> events;
	std::promise<void> stopRequest, stopResponse;
	tstring const spadeUrl= _T("https://spade.twitch.tv/");
	tstring currentToken;
	std::string applicationName, deviceId, currentUserId;
	std::future<void> event_task;
	tstring const empty;

	std::string EncodeProperties(std::map<tstring, tstring> const& properties) {
		auto jsonSdkPayload= json::Value(json::objectValue);
		for(auto const& p : properties) {
			jsonSdkPayload[FromTstring(p.first)]= FromTstring(p.second);
		}
		json::FastWriter writer;
		std::string json= writer.write(jsonSdkPayload);
		while(isspace(json.back())) {
			json.pop_back();
		}
		return json;
	}

	void SendScienceEvent(ScienceEvent const& scienceEvent) {
		// Hold onto the HttpRequest pointer of the CAPE SDK for the duration
		// of this invocation.
		auto p = ttv::GetHttpRequest();

		// Get the user identifier, if available or necessary.
		std::string userId;
		if(!scienceEvent.userId.empty()) {
			userId= FromTstring(scienceEvent.userId);
		} else if(!scienceEvent.token.empty()) {
			if(scienceEvent.token != currentToken) {
				// Fetch the user for its identifier.
				currentUserId= FromTstring(User::FetchCurrent(scienceEvent.clientId, scienceEvent.token).get().Id);
			}
			userId= currentUserId;
		}

		// Create a JSON payload for science.
		json::Value value;
		value["event"]= "sdk_request";
		auto& jsonProperties= value["properties"]= json::Value(json::objectValue);
		jsonProperties["client_id"]= FromTstring(scienceEvent.clientId);
		jsonProperties["sdk_method"]= FromTstring(scienceEvent.method);
		jsonProperties["user_id"]= userId;
		jsonProperties["sdk_version"]= sdkVersion;
		jsonProperties["platform"]= sdkPlatform;
		jsonProperties["runtime"]= sdkRuntime;
		jsonProperties["application_name"]= applicationName;
		jsonProperties["device_id"]= deviceId;
		jsonProperties["sdk_json_payload"]= EncodeProperties(scienceEvent.properties);

		// Format the payload as a base64-encoded POST request and send it to Spade.
		json::FastWriter writer;
		std::string json= writer.write(value);
		while(isspace(json.back())) {
			json.pop_back();
		}
		std::string data= Base64Encode(std::vector<char>(json.cbegin(), json.cend()));
		auto const httpResponse= TwitchInGames::HttpRequest().Post(spadeUrl, std::vector<char>(data.cbegin(), data.cend()));
#ifdef _DEBUG
		int const result= httpResponse.Result;
		if(result == 0) {
			auto resultCode= httpResponse.ResultCode;
			auto responseBody= httpResponse.Response;
			UNREFERENCED_PARAMETER(resultCode);
			UNREFERENCED_PARAMETER(responseBody);
		}
#else
		UNREFERENCED_PARAMETER(httpResponse);
#endif
	}

	void SendScienceEvents() {
		auto checkStop= stopRequest.get_future();
		do {
			std::unique_ptr<ScienceEvent::vector> p(events.exchange(nullptr));
			if(p) {
				for(auto const& e : *p) {
					try {
						SendScienceEvent(e);
					} catch(std::exception const& ex) {
						UNREFERENCED_PARAMETER(ex);
						DebugWriteLine(_T("SendScienceEvents failed: %s"), ex.what());
					}
				}
			}
		} while(checkStop.wait_for(std::chrono::seconds(5)) != std::future_status::ready);
		stopResponse.set_value();
	}

	void StopScienceEvents() {
		stopRequest.set_value();
		stopResponse.get_future().get();
		TTV_ShutdownLibrary();
	}

	void SendScienceEvent(tstring const& clientId, tstring const& token, tstring const& userId, tstring const& method, std::map<tstring, tstring> const& properties) noexcept {
		try {
			std::unique_ptr<ScienceEvent::vector> p(events.exchange(nullptr));
			if(p == nullptr) {
				p.reset(new ScienceEvent::vector);
			}
			p->push_back({ clientId, token, userId, method, properties });
			events.exchange(p.release());
			if(!event_task.valid()) {
				applicationName = FromTstring(GetApplicationName());
				deviceId = FromTstring(GetDeviceId());
				event_task = std::async(std::launch::async, SendScienceEvents);
				atexit(StopScienceEvents);
			}
		} catch(std::exception const& ex) {
			UNREFERENCED_PARAMETER(ex);
			DebugWriteLine(_T("warning: SendScienceEvent failed: %s"), ToTstring(ex.what()).c_str());
			// Ignore exceptions.
		} catch(...) {
			DebugWriteLine(_T("warning: SendScienceEvent failed"));
			// Ignore exceptions.
		}
	}
}

void TwitchInGames::SendTokenScienceEvent(tstring const& clientId, tstring const& token, tstring const& method, std::map<tstring, tstring> const& properties) noexcept {
	SendScienceEvent(clientId, token, empty, method, properties);
}

void TwitchInGames::SendUserScienceEvent(tstring const& clientId, tstring const& userId, tstring const& method, std::map<tstring, tstring> const& properties) noexcept {
	SendScienceEvent(clientId, empty, userId, method, properties);
}
