/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */

#include <string>
#include <vector>
#include <fstream>
#include <direct.h>
#include <windows.h>
#include <shlwapi.h>
#include <shellapi.h>
#include <shlobj.h>
#include <sys/types.h>
#include <sys/stat.h>

static std::string GetIcingaInstallPath(void)
{
	char szFileName[MAX_PATH];
	if (!GetModuleFileName(nullptr, szFileName, sizeof(szFileName)))
		return "";

	if (!PathRemoveFileSpec(szFileName))
		return "";

	if (!PathRemoveFileSpec(szFileName))
		return "";

	return szFileName;
}


static bool ExecuteCommand(const std::string& app, const std::string& arguments)
{
	SHELLEXECUTEINFO sei = {};
	sei.cbSize = sizeof(sei);
	sei.fMask = SEE_MASK_NOCLOSEPROCESS;
	sei.lpFile = app.c_str();
	sei.lpParameters = arguments.c_str();
	sei.nShow = SW_HIDE;
	if (!ShellExecuteEx(&sei))
		return false;

	if (!sei.hProcess)
		return false;

	WaitForSingleObject(sei.hProcess, INFINITE);

	DWORD exitCode;
	BOOL res = GetExitCodeProcess(sei.hProcess, &exitCode);
	CloseHandle(sei.hProcess);

	if (!res)
		return false;

	return exitCode == 0;
}

static bool ExecuteIcingaCommand(const std::string& arguments)
{
	return ExecuteCommand(GetIcingaInstallPath() + "\\sbin\\icinga2.exe", arguments);
}

static std::string DirName(const std::string& path)
{
	char *spath = strdup(path.c_str());

	if (!PathRemoveFileSpec(spath)) {
		free(spath);
		throw std::runtime_error("PathRemoveFileSpec failed");
	}

	std::string result = spath;

	free(spath);

	return result;
}

static bool PathExists(const std::string& path)
{
	struct _stat statbuf;
	return (_stat(path.c_str(), &statbuf) >= 0);
}

static std::string GetIcingaDataPath(void)
{
	char path[MAX_PATH];
	if (!SUCCEEDED(SHGetFolderPath(nullptr, CSIDL_COMMON_APPDATA, nullptr, 0, path)))
		throw std::runtime_error("SHGetFolderPath failed");
	return std::string(path) + "\\icinga2";
}

static void MkDir(const std::string& path)
{
	if (mkdir(path.c_str()) < 0 && errno != EEXIST)
		throw std::runtime_error("mkdir failed");
}

static void MkDirP(const std::string& path)
{
	size_t pos = 0;

	while (pos != std::string::npos) {
		pos = path.find_first_of("/\\", pos + 1);

		std::string spath = path.substr(0, pos + 1);
		struct _stat statbuf;
		if (_stat(spath.c_str(), &statbuf) < 0 && errno == ENOENT)
			MkDir(path.substr(0, pos));
	}
}

static std::string GetNSISInstallPath(void)
{
	HKEY hKey;
	//TODO: Change hardcoded key
	if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE\\Icinga Development Team\\ICINGA2", 0,
		KEY_QUERY_VALUE | KEY_WOW64_32KEY, &hKey) == ERROR_SUCCESS) {
		BYTE pvData[MAX_PATH];
		DWORD cbData = sizeof(pvData) - 1;
		DWORD lType;
		if (RegQueryValueEx(hKey, nullptr, nullptr, &lType, pvData, &cbData) == ERROR_SUCCESS && lType == REG_SZ) {
			pvData[cbData] = '\0';

			return (char *)pvData;
		}

		RegCloseKey(hKey);
	}

	return "";
}

static bool CopyDirectory(const std::string& source, const std::string& destination)
{
	// SHFileOperation requires file names to be terminated with two \0s
	std::string tmpSource = source + std::string(1, '\0');
	std::string tmpDestination = destination + std::string(1, '\0');

	SHFILEOPSTRUCT fop;
	fop.wFunc = FO_COPY;
	fop.pFrom = tmpSource.c_str();
	fop.pTo = tmpDestination.c_str();
	fop.fFlags = FOF_NO_UI;

	return (SHFileOperation(&fop) == 0);
}

static bool DeleteDirectory(const std::string& dir)
{
	// SHFileOperation requires file names to be terminated with two \0s
	std::string tmpDir = dir + std::string(1, '\0');

	SHFILEOPSTRUCT fop;
	fop.wFunc = FO_DELETE;
	fop.pFrom = tmpDir.c_str();
	fop.fFlags = FOF_NO_UI;

	return (SHFileOperation(&fop) == 0);
}

static int UpgradeNSIS(void)
{
	std::string installPath = GetNSISInstallPath();

	if (installPath.empty())
		return 0;

	std::string uninstallerPath = installPath + "\\uninstall.exe";

	if (!PathExists(uninstallerPath))
		return 0;

	std::string dataPath = GetIcingaDataPath();

	if (dataPath.empty())
		return 1;

	bool moveUserData = !PathExists(dataPath);

	/* perform open heart surgery on the user's data dirs - yay */
	if (moveUserData) {
		MkDir(dataPath.c_str());

		std::string oldNameEtc = installPath + "\\etc";
		std::string newNameEtc = dataPath + "\\etc";
		if (!CopyDirectory(oldNameEtc, newNameEtc))
			return 1;

		std::string oldNameVar = installPath + "\\var";
		std::string newNameVar = dataPath + "\\var";
		if (!CopyDirectory(oldNameVar, newNameVar))
			return 1;
	}

	ExecuteCommand(uninstallerPath, "/S _?=" + installPath);

	_unlink(uninstallerPath.c_str());

	if (moveUserData) {
		std::string oldNameEtc = installPath + "\\etc";
		if (!DeleteDirectory(oldNameEtc))
			return 1;

		std::string oldNameVar = installPath + "\\var";
		if (!DeleteDirectory(oldNameVar))
			return 1;

		_rmdir(installPath.c_str());
	}

	return 0;
}

static int InstallIcinga(void)
{
	std::string installDir = GetIcingaInstallPath();
	std::string skelDir = installDir + "\\share\\skel";
	std::string dataDir = GetIcingaDataPath();

	if (!PathExists(dataDir)) {
		std::string sourceDir = skelDir + std::string(1, '\0');
		std::string destinationDir = dataDir + std::string(1, '\0');

		SHFILEOPSTRUCT fop;
		fop.wFunc = FO_COPY;
		fop.pFrom = sourceDir.c_str();
		fop.pTo = destinationDir.c_str();
		fop.fFlags = FOF_NO_UI | FOF_NOCOPYSECURITYATTRIBS;

		if (SHFileOperation(&fop) != 0)
			return 1;

		MkDirP(dataDir + "/etc/icinga2/pki");
		MkDirP(dataDir + "/var/cache/icinga2");
		MkDirP(dataDir + "/var/lib/icinga2/certs");
		MkDirP(dataDir + "/var/lib/icinga2/certificate-requests");
		MkDirP(dataDir + "/var/lib/icinga2/agent/inventory");
		MkDirP(dataDir + "/var/lib/icinga2/api/config");
		MkDirP(dataDir + "/var/lib/icinga2/api/log");
		MkDirP(dataDir + "/var/lib/icinga2/api/zones");
		MkDirP(dataDir + "/var/log/icinga2/compat/archive");
		MkDirP(dataDir + "/var/log/icinga2/crash");
		MkDirP(dataDir + "/var/run/icinga2/cmd");
		MkDirP(dataDir + "/var/spool/icinga2/perfdata");
		MkDirP(dataDir + "/var/spool/icinga2/tmp");
	}

	// Upgrade from versions older than 2.13 by making the windowseventlog feature available,
	// enable it by default and disable the old mainlog feature.
	if (!PathExists(dataDir + "/etc/icinga2/features-available/windowseventlog.conf")) {
		// Disable the old mainlog feature as it is replaced by windowseventlog by default.
		std::string mainlogEnabledFile = dataDir + "/etc/icinga2/features-enabled/mainlog.conf";
		if (PathExists(mainlogEnabledFile)) {
			if (DeleteFileA(mainlogEnabledFile.c_str()) == 0) {
				throw std::runtime_error("deleting '" + mainlogEnabledFile + "' failed");
			}
		}

		// Install the new windowseventlog feature. As features-available/windowseventlog.conf is used as a marker file,
		// copy it as the last step, so that this is run again should the upgrade be interrupted.
		for (const std::string& d : {"features-enabled", "features-available"}) {
			std::string sourceFile = skelDir + "/etc/icinga2/" + d + "/windowseventlog.conf";
			std::string destinationFile = dataDir + "/etc/icinga2/" + d + "/windowseventlog.conf";

			if (CopyFileA(sourceFile.c_str(), destinationFile.c_str(), false) == 0) {
				throw std::runtime_error("copying '" + sourceFile + "' to '" + destinationFile + "' failed");
			}
		}
	}

	// TODO: In Icinga 2.14, rename features-available/mainlog.conf to mainlog.conf.deprecated
	//       so that it's no longer listed as an available feature.

	if (!ExecuteCommand("icacls", "\"" + dataDir + "\" /grant *S-1-5-20:(oi)(ci)m")){
		throw std::runtime_error("failed to set ACLs for " + dataDir);
	}
	if (!ExecuteCommand("icacls", "\"" + dataDir + "\\etc\" /inheritance:r /grant:r *S-1-5-20:(oi)(ci)m *S-1-5-32-544:(oi)(ci)f")) {
		throw std::runtime_error("failed to set ACLs for " + dataDir + "\\etc");
	}
	if (!ExecuteCommand("icacls", "\"" + dataDir + "\\var\" /inheritance:r /grant:r *S-1-5-20:(oi)(ci)m *S-1-5-32-544:(oi)(ci)f")) {
		throw std::runtime_error("failed to set ACLs for " + dataDir + "\\var");
	}

	ExecuteIcingaCommand("--scm-install daemon");

	return 0;
}

static int UninstallIcinga(void)
{
	ExecuteIcingaCommand("--scm-uninstall");

	return 0;
}

/**
* Entry point for the installer application.
*/
int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
	CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);

	//AllocConsole();
	int rc;

	if (strcmp(lpCmdLine, "install") == 0) {
		rc = InstallIcinga();
	} else if (strcmp(lpCmdLine, "uninstall") == 0) {
		rc = UninstallIcinga();
	} else if (strcmp(lpCmdLine, "upgrade-nsis") == 0) {
		rc = UpgradeNSIS();
	} else {
		MessageBox(nullptr, "This application should only be run by the MSI installer package.", "Icinga 2 Installer", MB_ICONWARNING);
		rc = 1;
	}

	//::Sleep(3000s);

	return rc;
}
