#include "../win-update/updater/manifest.hpp"
#include "update-helpers.hpp"
#include "shared-update.hpp"
#include "update-window.hpp"
#include "remote-text.hpp"
#include "qt-wrappers.hpp"
#include "win-update.hpp"
#include "obs-app.hpp"

#include <QMessageBox>

#include <string>
#include <mutex>

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>

#include <util/windows/WinHandle.hpp>
#include <util/util.hpp>

#ifdef BROWSER_AVAILABLE
#include <browser-panel.hpp>
#endif

using namespace std;
using namespace updater;

/* ------------------------------------------------------------------------ */

#ifndef WIN_MANIFEST_URL
#define WIN_MANIFEST_URL "https://obsproject.com/update_studio/manifest.json"
#endif

#ifndef WIN_MANIFEST_BASE_URL
#define WIN_MANIFEST_BASE_URL "https://obsproject.com/update_studio/"
#endif

#ifndef WIN_BRANCHES_URL
#define WIN_BRANCHES_URL "https://obsproject.com/update_studio/branches.json"
#endif

#ifndef WIN_DEFAULT_BRANCH
#define WIN_DEFAULT_BRANCH "stable"
#endif

#ifndef WIN_UPDATER_URL
#define WIN_UPDATER_URL "https://obsproject.com/update_studio/updater.exe"
#endif

/* ------------------------------------------------------------------------ */

static bool ParseUpdateManifest(const char *manifest_data,
				bool *updatesAvailable, string &notes,
				string &updateVer, const string &branch)
try {
	constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL |
					    OBS_RELEASE_CANDIDATE << 8ULL |
					    OBS_BETA;
	constexpr bool isPreRelease =
		currentVersion & 0xffff ||
		std::char_traits<char>::length(OBS_COMMIT);

	json manifestContents = json::parse(manifest_data);
	Manifest manifest = manifestContents.get<Manifest>();

	if (manifest.version_major == 0 && manifest.commit.empty())
		throw strprintf("Invalid version number: %d.%d.%d",
				manifest.version_major, manifest.version_minor,
				manifest.version_patch);

	notes = manifest.notes;

	if (manifest.commit.empty()) {
		uint64_t new_ver =
			MAKE_SEMANTIC_VERSION((uint64_t)manifest.version_major,
					      (uint64_t)manifest.version_minor,
					      (uint64_t)manifest.version_patch);
		new_ver <<= 16;
		/* RC builds are shifted so that rc1 and beta1 versions do not result
		 * in the same new_ver. */
		if (manifest.rc > 0)
			new_ver |= (uint64_t)manifest.rc << 8;
		else if (manifest.beta > 0)
			new_ver |= (uint64_t)manifest.beta;

		updateVer = to_string(new_ver);

		/* When using a pre-release build or non-default branch we only check if
		 * the manifest version is different, so that it can be rolled back. */
		if (branch != WIN_DEFAULT_BRANCH || isPreRelease)
			*updatesAvailable = new_ver != currentVersion;
		else
			*updatesAvailable = new_ver > currentVersion;
	} else {
		/* Test or nightly builds may not have a (valid) version number,
		 * so compare commit hashes instead. */
		updateVer = manifest.commit.substr(0, 8);
		*updatesAvailable = !currentVersion ||
				    !manifest.commit.compare(
					    0, strlen(OBS_COMMIT), OBS_COMMIT);
	}

	return true;

} catch (string &text) {
	blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str());
	return false;
}

/* ------------------------------------------------------------------------ */

bool GetBranchAndUrl(string &selectedBranch, string &manifestUrl)
{
	const char *config_branch =
		config_get_string(GetGlobalConfig(), "General", "UpdateBranch");
	if (!config_branch)
		return true;

	bool found = false;
	for (const UpdateBranch &branch : App()->GetBranches()) {
		if (branch.name != config_branch)
			continue;
		/* A branch that is found but disabled will just silently fall back to
		 * the default. But if the branch was removed entirely, the user should
		 * be warned, so leave this false *only* if the branch was removed. */
		found = true;

		if (branch.is_enabled) {
			selectedBranch = branch.name.toStdString();
			if (branch.name != WIN_DEFAULT_BRANCH) {
				manifestUrl = WIN_MANIFEST_BASE_URL;
				manifestUrl += "manifest_" +
					       branch.name.toStdString() +
					       ".json";
			}
		}
		break;
	}

	return found;
}

/* ------------------------------------------------------------------------ */

void AutoUpdateThread::infoMsg(const QString &title, const QString &text)
{
	OBSMessageBox::information(App()->GetMainWindow(), title, text);
}

void AutoUpdateThread::info(const QString &title, const QString &text)
{
	QMetaObject::invokeMethod(this, "infoMsg", Qt::BlockingQueuedConnection,
				  Q_ARG(QString, title), Q_ARG(QString, text));
}

int AutoUpdateThread::queryUpdateSlot(bool localManualUpdate,
				      const QString &text)
{
	OBSUpdate updateDlg(App()->GetMainWindow(), localManualUpdate, text);
	return updateDlg.exec();
}

int AutoUpdateThread::queryUpdate(bool localManualUpdate, const char *text_utf8)
{
	int ret = OBSUpdate::No;
	QString text = text_utf8;
	QMetaObject::invokeMethod(this, "queryUpdateSlot",
				  Qt::BlockingQueuedConnection,
				  Q_RETURN_ARG(int, ret),
				  Q_ARG(bool, localManualUpdate),
				  Q_ARG(QString, text));
	return ret;
}

bool AutoUpdateThread::queryRepairSlot()
{
	QMessageBox::StandardButton res = OBSMessageBox::question(
		App()->GetMainWindow(), QTStr("Updater.RepairConfirm.Title"),
		QTStr("Updater.RepairConfirm.Text"),
		QMessageBox::Yes | QMessageBox::Cancel);

	return res == QMessageBox::Yes;
}

bool AutoUpdateThread::queryRepair()
{
	bool ret = false;
	QMetaObject::invokeMethod(this, "queryRepairSlot",
				  Qt::BlockingQueuedConnection,
				  Q_RETURN_ARG(bool, ret));
	return ret;
}

void AutoUpdateThread::run()
try {
	string text;
	string branch = WIN_DEFAULT_BRANCH;
	string manifestUrl = WIN_MANIFEST_URL;
	vector<string> extraHeaders;
	bool updatesAvailable = false;

	struct FinishedTrigger {
		inline ~FinishedTrigger()
		{
			QMetaObject::invokeMethod(App()->GetMainWindow(),
						  "updateCheckFinished");
		}
	} finishedTrigger;

	/* ----------------------------------- *
	 * get branches from server            */

	if (FetchAndVerifyFile("branches", "obs-studio\\updates\\branches.json",
			       WIN_BRANCHES_URL, &text))
		App()->SetBranchData(text);

	/* ----------------------------------- *
	 * check branch and get manifest url   */

	if (!GetBranchAndUrl(branch, manifestUrl)) {
		config_set_string(GetGlobalConfig(), "General", "UpdateBranch",
				  WIN_DEFAULT_BRANCH);
		info(QTStr("Updater.BranchNotFound.Title"),
		     QTStr("Updater.BranchNotFound.Text"));
	}

	/* allow server to know if this was a manual update check in case
	 * we want to allow people to bypass a configured rollout rate */
	if (manualUpdate)
		extraHeaders.emplace_back("X-OBS2-ManualUpdate: 1");

	/* ----------------------------------- *
	 * get manifest from server            */

	text.clear();
	if (!FetchAndVerifyFile("manifest",
				"obs-studio\\updates\\manifest.json",
				manifestUrl.c_str(), &text, extraHeaders))
		return;

	/* ----------------------------------- *
	 * check manifest for update           */

	string notes;
	string updateVer;

	if (!ParseUpdateManifest(text.c_str(), &updatesAvailable, notes,
				 updateVer, branch))
		throw string("Failed to parse manifest");

	if (!updatesAvailable && !repairMode) {
		if (manualUpdate)
			info(QTStr("Updater.NoUpdatesAvailable.Title"),
			     QTStr("Updater.NoUpdatesAvailable.Text"));
		return;
	} else if (updatesAvailable && repairMode) {
		info(QTStr("Updater.RepairButUpdatesAvailable.Title"),
		     QTStr("Updater.RepairButUpdatesAvailable.Text"));
		return;
	}

	/* ----------------------------------- *
	 * skip this version if set to skip    */

	const char *skipUpdateVer = config_get_string(
		GetGlobalConfig(), "General", "SkipUpdateVersion");
	if (!manualUpdate && !repairMode && skipUpdateVer &&
	    updateVer == skipUpdateVer)
		return;

	/* ----------------------------------- *
	 * fetch updater module                */

	if (!FetchAndVerifyFile("updater", "obs-studio\\updates\\updater.exe",
				WIN_UPDATER_URL, nullptr))
		return;

	/* ----------------------------------- *
	 * query user for update               */

	if (repairMode) {
		if (!queryRepair())
			return;
	} else {
		int queryResult = queryUpdate(manualUpdate, notes.c_str());

		if (queryResult == OBSUpdate::No) {
			if (!manualUpdate) {
				long long t = (long long)time(nullptr);
				config_set_int(GetGlobalConfig(), "General",
					       "LastUpdateCheck", t);
			}
			return;

		} else if (queryResult == OBSUpdate::Skip) {
			config_set_string(GetGlobalConfig(), "General",
					  "SkipUpdateVersion",
					  updateVer.c_str());
			return;
		}
	}

	/* ----------------------------------- *
	 * get working dir                     */

	wchar_t cwd[MAX_PATH];
	GetModuleFileNameW(nullptr, cwd, _countof(cwd) - 1);
	wchar_t *p = wcsrchr(cwd, '\\');
	if (p)
		*p = 0;

	/* ----------------------------------- *
	 * execute updater                     */

	BPtr<char> updateFilePath =
		GetConfigPathPtr("obs-studio\\updates\\updater.exe");
	BPtr<wchar_t> wUpdateFilePath;

	size_t size = os_utf8_to_wcs_ptr(updateFilePath, 0, &wUpdateFilePath);
	if (!size)
		throw string("Could not convert updateFilePath to wide");

	/* note, can't use CreateProcess to launch as admin. */
	SHELLEXECUTEINFO execInfo = {};

	execInfo.cbSize = sizeof(execInfo);
	execInfo.lpFile = wUpdateFilePath;

	string parameters;
	if (branch != WIN_DEFAULT_BRANCH)
		parameters += "--branch=" + branch;

	obs_cmdline_args obs_args = obs_get_cmdline_args();
	for (int idx = 1; idx < obs_args.argc; idx++) {
		if (!parameters.empty())
			parameters += " ";

		parameters += obs_args.argv[idx];
	}

	/* Portable mode can be enabled via sentinel files, so copying the
	 * command line doesn't guarantee the flag to be there. */
	if (App()->IsPortableMode() &&
	    parameters.find("--portable") == string::npos) {
		if (!parameters.empty())
			parameters += " ";
		parameters += "--portable";
	}

	BPtr<wchar_t> lpParameters;
	size = os_utf8_to_wcs_ptr(parameters.c_str(), 0, &lpParameters);
	if (!size && !parameters.empty())
		throw string("Could not convert parameters to wide");

	execInfo.lpParameters = lpParameters;
	execInfo.lpDirectory = cwd;
	execInfo.nShow = SW_SHOWNORMAL;

	if (!ShellExecuteEx(&execInfo)) {
		QString msg = QTStr("Updater.FailedToLaunch");
		info(msg, msg);
		throw strprintf("Can't launch updater '%s': %d",
				updateFilePath.Get(), GetLastError());
	}

	/* force OBS to perform another update check immediately after updating
	 * in case of issues with the new version */
	config_set_int(GetGlobalConfig(), "General", "LastUpdateCheck", 0);
	config_set_string(GetGlobalConfig(), "General", "SkipUpdateVersion",
			  "0");

	QMetaObject::invokeMethod(App()->GetMainWindow(), "close");

} catch (string &text) {
	blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str());
}
