// AUTOGENERATED COPYRIGHT HEADER START
// Copyright (C) 2020-2023 Michael Fabian 'Xaymar' Dirks <info@xaymar.com>
// AUTOGENERATED COPYRIGHT HEADER END

#include "updater.hpp"
#include "version.hpp"
#include "configuration.hpp"
#include "plugin.hpp"
#include "util/util-logging.hpp"

#include "warning-disable.hpp"
#include <fstream>
#include <mutex>
#include <regex>
#include <string_view>
#include "warning-enable.hpp"

#ifdef _DEBUG
#define ST_PREFIX "<%s> "
#define D_LOG_ERROR(x, ...) P_LOG_ERROR(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__)
#define D_LOG_WARNING(x, ...) P_LOG_WARN(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__)
#define D_LOG_INFO(x, ...) P_LOG_INFO(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__)
#define D_LOG_DEBUG(x, ...) P_LOG_DEBUG(ST_PREFIX##x, __FUNCTION_SIG__, __VA_ARGS__)
#else
#define ST_PREFIX "<updater> "
#define D_LOG_ERROR(...) P_LOG_ERROR(ST_PREFIX __VA_ARGS__)
#define D_LOG_WARNING(...) P_LOG_WARN(ST_PREFIX __VA_ARGS__)
#define D_LOG_INFO(...) P_LOG_INFO(ST_PREFIX __VA_ARGS__)
#define D_LOG_DEBUG(...) P_LOG_DEBUG(ST_PREFIX __VA_ARGS__)
#endif

// TODO:
// - Cache result in the configuration directory (not as a configuration value).
// - Move 'autoupdater.last_checked_at' to out of the configuration.
// - Figure out if nightly updates are viable at all.

#define ST_CFG_DATASHARING "updater.datasharing"
#define ST_CFG_AUTOMATION "updater.automation"
#define ST_CFG_CHANNEL "updater.channel"
#define ST_CFG_LASTCHECKEDAT "updater.lastcheckedat"

streamfx::version_stage streamfx::stage_from_string(std::string_view str)
{
	if (str == "a") {
		return version_stage::ALPHA;
	} else if (str == "b") {
		return version_stage::BETA;
	} else if (str == "c") {
		return version_stage::CANDIDATE;
	} else {
		return version_stage::STABLE;
	}
}

std::string_view streamfx::stage_to_string(version_stage t)
{
	switch (t) {
	case version_stage::ALPHA:
		return "a";
	case version_stage::BETA:
		return "b";
	case version_stage::CANDIDATE:
		return "c";
	default:
	case version_stage::STABLE:
		return ".";
	}
}

void streamfx::to_json(nlohmann::json& json, const version_stage& stage)
{
	json = stage_to_string(stage);
}

void streamfx::from_json(const nlohmann::json& json, version_stage& stage)
{
	stage = stage_from_string(json.get<std::string>());
}

streamfx::version_info::version_info() : major(0), minor(0), patch(0), tweak(0), stage(version_stage::STABLE), url(""), name("") {}

streamfx::version_info::version_info(const std::string text) : version_info()
{
	// text can be:
	// 0.0.0 (Stable)
	// 0.0.0a0 (Testing)
	// 0.0.0b0 (Testing)
	// 0.0.0c0 (Testing)
	// 0.0.0_0 (Development)
	static const std::regex re_version("([0-9]+)\\.([0-9]+)\\.([0-9]+)(([\\._abc]{1,1})([0-9]+|)|)(-g([0-9a-fA-F]{8,8})|)");
	std::smatch             matches;
	if (std::regex_match(text, matches, re_version, std::regex_constants::match_any | std::regex_constants::match_continuous)) {
		major = static_cast<uint16_t>(strtoul(matches[1].str().c_str(), nullptr, 10));
		minor = static_cast<uint16_t>(strtoul(matches[2].str().c_str(), nullptr, 10));
		patch = static_cast<uint16_t>(strtoul(matches[3].str().c_str(), nullptr, 10));
		if (matches.size() >= 5) {
			stage = stage_from_string(matches[5].str());
			tweak = static_cast<uint16_t>(strtoul(matches[6].str().c_str(), nullptr, 10));
		}
	} else {
		throw std::invalid_argument("Provided string is not a version.");
	}
}

void streamfx::to_json(nlohmann::json& json, const version_info& info)
{
	auto version     = nlohmann::json::object();
	version["major"] = info.major;
	version["minor"] = info.minor;
	version["patch"] = info.patch;
	version["type"]  = info.stage;
	version["tweak"] = info.tweak;
	json["version"]  = version;
	json["url"]      = info.url;
	json["name"]     = info.name;
}

void streamfx::from_json(const nlohmann::json& json, version_info& info)
{
	if (json.find("html_url") == json.end()) {
		auto version = json.at("version");
		info.major   = version.at("major").get<uint16_t>();
		info.minor   = version.at("minor").get<uint16_t>();
		info.patch   = version.at("patch").get<uint16_t>();
		info.stage   = version.at("type");
		info.tweak   = version.at("tweak").get<uint16_t>();
		info.url     = json.at("url").get<std::string>();
		info.name    = json.at("name").get<std::string>();
	} else {
		// This is a response from GitHub.

		// Retrieve entries from the release object.
		auto entry_tag_name = json.find("tag_name");
		auto entry_name     = json.find("name");
		auto entry_url      = json.find("html_url");
		if ((entry_tag_name == json.end()) || (entry_name == json.end()) || (entry_url == json.end())) {
			throw std::runtime_error("JSON is missing one or more required keys.");
		}

		// Parse the information.
		std::string tag_name = entry_tag_name->get<std::string>();
		info                 = {tag_name};
		info.url             = entry_url->get<std::string>();
		info.name            = entry_name->get<std::string>();
	}
}

bool streamfx::version_info::is_older_than(const version_info other)
{
	// 'true' if other is newer, otherwise false.
	// Except for stable releases, this simply compares the numbers of the version.

	// Compare Major version:
	// - Theirs is greater: Remote is newer.
	if (major < other.major)
		return true;
	// - Ours is greater: Remote is older.
	if (major > other.major)
		return false;
	// - Continue the check.

	// Compare Minor version:
	// - Theirs is greater: Remote is newer.
	if (minor < other.minor)
		return true;
	// - Ours is greater: Remote is older.
	if (minor > other.minor)
		return false;
	// - Continue the check.

	// Compare Patch version:
	// - Theirs is greater: Remote is newer.
	if (patch < other.patch)
		return true;
	// - Ours is greater: Remote is older.
	if (patch > other.patch)
		return false;
	// - Continue the check.

	// Compare Stage
	// - Theirs is Stable, we are not: Remote is newer.
	if ((stage != version_stage::STABLE) && (other.stage == version_stage::STABLE))
		return true;
	// - Continue the check.

	// Compare Tweak
	// - Theirs is greater: Remote is newer.
	if (tweak < other.tweak)
		return true;
	// - Ours is greater: Remote is older.
	if (tweak > other.tweak)
		return false;
	// - Continue the check.

	// Compare Stage (again)
	// - Ours is greater: Remote is newer.
	if (stage > other.stage)
		return true;
	// - Theirs is greater: Remote is older.
	if (stage < other.stage)
		return false;
	
	// If all tests failed so far, assume the compared version is identical or newer.
	return false;
}

streamfx::version_info::operator std::string()
{
	std::vector<char> buffer(25, 0);
	if (stage != version_stage::STABLE) {
		auto types = stage_to_string(stage);
		int  len   = snprintf(buffer.data(), buffer.size(), "%" PRIu16 ".%" PRIu16 ".%" PRIu16 "%.1s%" PRIu16, major, minor, patch, types.data(), tweak);
		return std::string(buffer.data(), buffer.data() + len);
	} else {
		int len = snprintf(buffer.data(), buffer.size(), "%" PRIu16 ".%" PRIu16 ".%" PRIu16, major, minor, patch);
		return std::string(buffer.data(), buffer.data() + len);
	}
}

void streamfx::updater::task(streamfx::util::threadpool::task_data_t)
{
	try {
		auto query_fn = [](std::vector<char>& buffer) {
			static constexpr std::string_view ST_API_URL = "https://api.github.com/repos/Xaymar/obs-StreamFX/releases?per_page=25&page=1";

			streamfx::util::curl curl;
			size_t               buffer_offset = 0;

			// Set headers (User-Agent is needed so Github can contact us!).
			curl.set_header("User-Agent", "StreamFX Updater v" STREAMFX_VERSION_STRING);
			curl.set_header("Accept", "application/vnd.github.v3+json");

			// Set up request.
			curl.set_option(CURLOPT_HTTPGET, true); // GET
			curl.set_option(CURLOPT_POST, false); // Not POST
			curl.set_option(CURLOPT_URL, ST_API_URL);
			curl.set_option(CURLOPT_TIMEOUT, 30); // 10s until we fail.

			// Callbacks
			curl.set_write_callback([&buffer, &buffer_offset](void* data, size_t s1, size_t s2) {
				size_t size = s1 * s2;
				if (buffer.size() < (size + buffer_offset))
					buffer.resize(buffer_offset + size);

				memcpy(buffer.data() + buffer_offset, data, size);
				buffer_offset += size;

				return s1 * s2;
			});
			//std::bind(&streamfx::updater::task_write_cb, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)

			// Clear any unknown data and reserve 64KiB of memory.
			buffer.clear();
			buffer.reserve(0xFFFF);

			// Finally, execute the request.
			D_LOG_DEBUG("Querying for latest releases...", "");
			if (CURLcode res = curl.perform(); res != CURLE_OK) {
				D_LOG_ERROR("Performing query failed with error: %s", curl_easy_strerror(res));
				throw std::runtime_error(curl_easy_strerror(res));
			}

			int32_t status_code = 0;
			if (CURLcode res = curl.get_info(CURLINFO_HTTP_CODE, status_code); res != CURLE_OK) {
				D_LOG_ERROR("Retrieving status code failed with error: %s", curl_easy_strerror(res));
				throw std::runtime_error(curl_easy_strerror(res));
			}
			D_LOG_DEBUG("API returned status code %d.", status_code);

			if (status_code != 200) {
				D_LOG_ERROR("API returned unexpected status code %d.", status_code);
				throw std::runtime_error("Request failed due to one or more reasons.");
			}
		};
		auto parse_fn = [this](nlohmann::json json) {
			// Check if it was parsed as an object.
			if (json.type() != nlohmann::json::value_t::array) {
				throw std::runtime_error("Invalid response from API.");
			}

			// Decide on the latest version for all update channels.
			std::lock_guard<decltype(_lock)> lock(_lock);
			_updates.clear();
			for (auto obj : json) {
				try {
					auto info = obj.get<streamfx::version_info>();

					switch (info.stage) {
					case version_stage::STABLE:
						if (get_update_info(version_stage::STABLE).is_older_than(info)) {
							_updates.emplace(version_stage::STABLE, info);
						}
						[[fallthrough]];
					case version_stage::CANDIDATE:
						if (get_update_info(version_stage::CANDIDATE).is_older_than(info)) {
							_updates.emplace(version_stage::CANDIDATE, info);
						}
						[[fallthrough]];
					case version_stage::BETA:
						if (get_update_info(version_stage::BETA).is_older_than(info)) {
							_updates.emplace(version_stage::BETA, info);
						}
						[[fallthrough]];
					case version_stage::ALPHA:
						if (get_update_info(version_stage::ALPHA).is_older_than(info)) {
							_updates.emplace(version_stage::ALPHA, info);
						}
					}

				} catch (const std::exception& ex) {
					D_LOG_DEBUG("Failed to parse entry, error: %s", ex.what());
				}
			}
		};

		{ // Query and parse the response.
			nlohmann::json json;

			// Query the API or parse a crafted response.
			auto debug_path = streamfx::config_file_path("github_release_query_response.json");
			if (std::filesystem::exists(debug_path)) {
				std::ifstream fs{debug_path};
				json = nlohmann::json::parse(fs);
				fs.close();
			} else {
				std::vector<char> buffer;
				query_fn(buffer);
				json = nlohmann::json::parse(buffer.begin(), buffer.end());
			}

			// Parse the JSON response from the API.
			parse_fn(json);
		}

		// Print all update information to the log file.
		D_LOG_INFO("Current Version: %s", static_cast<std::string>(_current_info).c_str());
		D_LOG_INFO("Latest Stable Version: %s", static_cast<std::string>(get_update_info(version_stage::STABLE)).c_str());
		D_LOG_INFO("Latest Candidate Version: %s", static_cast<std::string>(get_update_info(version_stage::CANDIDATE)).c_str());
		D_LOG_INFO("Latest Beta Version: %s", static_cast<std::string>(get_update_info(version_stage::BETA)).c_str());
		D_LOG_INFO("Latest Alpha Version: %s", static_cast<std::string>(get_update_info(version_stage::ALPHA)).c_str());
		if (is_update_available()) {
			D_LOG_INFO("Update is available.", "");
		}

		// Notify listeners of the update.
		events.refreshed.call(*this);
	} catch (const std::exception& ex) {
		// Notify about the error.
		std::string message = ex.what();
		events.error.call(*this, message);
	}
}

bool streamfx::updater::can_check()
{
#ifdef _DEBUG
	return true;
#else
	auto now       = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch());
	auto threshold = (_lastcheckedat + std::chrono::minutes(10));
	return (now > threshold);
#endif
}

void streamfx::updater::load()
{
	std::lock_guard<decltype(_lock)> lock(_lock);
	if (auto config = streamfx::configuration::instance(); config) {
		auto dataptr = config->get();

		if (obs_data_has_user_value(dataptr.get(), ST_CFG_DATASHARING))
			_data_sharing_allowed = obs_data_get_bool(dataptr.get(), ST_CFG_DATASHARING);
		if (obs_data_has_user_value(dataptr.get(), ST_CFG_AUTOMATION))
			_automation = obs_data_get_bool(dataptr.get(), ST_CFG_AUTOMATION);
		if (obs_data_has_user_value(dataptr.get(), ST_CFG_CHANNEL))
			_channel = static_cast<version_stage>(obs_data_get_int(dataptr.get(), ST_CFG_CHANNEL));
		if (obs_data_has_user_value(dataptr.get(), ST_CFG_LASTCHECKEDAT))
			_lastcheckedat = std::chrono::seconds(obs_data_get_int(dataptr.get(), ST_CFG_LASTCHECKEDAT));
	}
}

void streamfx::updater::save()
{
	if (auto config = streamfx::configuration::instance(); config) {
		auto dataptr = config->get();

		obs_data_set_bool(dataptr.get(), ST_CFG_DATASHARING, _data_sharing_allowed);
		obs_data_set_bool(dataptr.get(), ST_CFG_AUTOMATION, _automation);
		obs_data_set_int(dataptr.get(), ST_CFG_CHANNEL, static_cast<long long>(_channel));
		obs_data_set_int(dataptr.get(), ST_CFG_LASTCHECKEDAT, static_cast<long long>(_lastcheckedat.count()));

		config->save();
	}
}

streamfx::updater::updater()
	: _lock(), _task(),

	  _data_sharing_allowed(false), _automation(true), _channel(version_stage::STABLE), _lastcheckedat(),

	  _current_info(), _updates(), _dirty(false)
{
	// Load information from configuration.
	load();

	// Build current version information.
	try {
		_current_info = {STREAMFX_VERSION_STRING};
	} catch (...) {
		D_LOG_ERROR("Failed to parse current version information, results may be inaccurate.", "");
	}
}

streamfx::updater::~updater()
{
	save();
}

bool streamfx::updater::is_data_sharing_allowed()
{
	return _data_sharing_allowed;
}

void streamfx::updater::set_data_sharing_allowed(bool value)
{
	_dirty                = true;
	_data_sharing_allowed = value;
	events.gdpr_changed(*this, _data_sharing_allowed);

	{
		std::lock_guard<decltype(_lock)> lock(_lock);
		save();
	}

	D_LOG_INFO("User %s the processing of data.", _data_sharing_allowed ? "allowed" : "disallowed");
}

bool streamfx::updater::is_automated()
{
	return _automation;
}

void streamfx::updater::set_automation(bool value)
{
	_automation = value;
	events.automation_changed(*this, _automation);

	{
		std::lock_guard<decltype(_lock)> lock(_lock);
		save();
	}

	D_LOG_INFO("Automatic checks at launch are now %s.", value ? "enabled" : "disabled");
}

streamfx::version_stage streamfx::updater::get_channel()
{
	return _channel;
}

void streamfx::updater::set_channel(version_stage value)
{
	std::lock_guard<decltype(_lock)> lock(_lock);

	_dirty   = true;
	_channel = value;
	events.channel_changed(*this, _channel);

	save();

	D_LOG_INFO("Update channel changed to '%s'.", stage_to_string(value).data());
}

void streamfx::updater::refresh()
{
	if (!_task.expired() || !is_data_sharing_allowed()) {
		return;
	}

	if (can_check()) {
		std::lock_guard<decltype(_lock)> lock(_lock);

		// Update last checked time.
		_lastcheckedat = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch());
		save();

		// Spawn a new task.
		_task = streamfx::threadpool()->push(std::bind(&streamfx::updater::task, this, std::placeholders::_1), nullptr);
	} else {
		events.refreshed(*this);
	}
}

bool streamfx::updater::is_update_available()
{
	return _current_info.is_older_than(get_update_info());
}

bool streamfx::updater::is_update_available(version_stage channel)
{
	return _current_info.is_older_than(get_update_info(channel));
}

streamfx::version_info streamfx::updater::get_current_info()
{
	return _current_info;
}

streamfx::version_info streamfx::updater::get_update_info()
{
	return get_update_info(_channel);
}

streamfx::version_info streamfx::updater::get_update_info(version_stage channel)
{
	std::lock_guard<decltype(_lock)> lock(_lock);
	if (auto iter = _updates.find(channel); iter != _updates.end()) {
		return iter->second;
	} else {
		return {};
	}
}

std::shared_ptr<streamfx::updater> streamfx::updater::instance()
{
	static std::weak_ptr<streamfx::updater> _instance;
	static std::mutex                       _lock;

	std::lock_guard<std::mutex> lock(_lock);
	if (_instance.expired()) {
		auto ptr  = std::make_shared<streamfx::updater>();
		_instance = ptr;
		return ptr;
	} else {
		return _instance.lock();
	}
}
