// What is this module called?
module main;

// What does this module require to function?
import core.memory;
import core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit;
import core.sys.posix.signal;
import core.thread;
import core.time;
import std.algorithm;
import std.concurrency;
import std.conv;
import std.datetime;
import std.file;
import std.getopt;
import std.net.curl: CurlException;
import std.parallelism;
import std.path;
import std.process;
import std.socket: SocketException;
import std.stdio;
import std.string;
import std.traits;

// What other modules that we have created do we need to import?
import config;
import log;
import curlEngine;
import util;
import onedrive;
import syncEngine;
import itemdb;
import clientSideFiltering;
import monitor;
import webhook;
import intune;
import socketio;

// What other constant variables do we require?
const int EXIT_RESYNC_REQUIRED = 126;

// Class objects
ApplicationConfig appConfig;
OneDriveWebhook oneDriveWebhook;
SyncEngine syncEngineInstance;
ItemDatabase itemDB;
ClientSideFiltering selectiveSync;
Monitor filesystemMonitor;
OneDriveSocketIo oneDriveSocketIo;

// Class variables
// Flag for performing a synchronised shutdown
bool shutdownInProgress = false;
// Flag if a --dry-run is being performed, as, on shutdown, once config is destroyed, we have no reference here
bool dryRun = false;
// Configure the runtime database file path so that it is available to us on shutdown so objects can be destroyed and removed if required
// - Typically this will be the default, but in a --dry-run scenario, we use a separate database file
string runtimeDatabaseFile = "";
// Flag for if we are performing filesystem monitoring 
bool performFileSystemMonitoring = false;
// Flag for if we perform a database vacuum. This gets set to false if we have not performed a 'no-sync' task
bool performDatabaseVacuum = true;
// Flag if SIGTERM is used
bool sigtermHandlerTriggered = false;

int main(string[] cliArgs) {
	// Application Start Time - used during monitor loop to detail how long it has been running for
	auto applicationStartTime = Clock.currTime();
	// Disable buffering on stdout - this is needed so that when we are using plain write() it will go to the terminal without flushing
	stdout.setvbuf(0, _IONBF);
	
	// Required main function variables
	string genericHelpMessage = "Please use 'onedrive --help' for further assistance in regards to running this application.";
	// If the user passes in --confdir we need to store this as a variable
	string confdirOption = "";
	// running as what user?
	string runtimeUserName = "";
	// Are we online?
	bool online = false;
	// Does the operating environment have shell environment variables set
	bool shellEnvSet = false;
	// What is the runtime synchronisation directory that will be used
	// Typically this will be '~/OneDrive' .. however tilde expansion is unreliable
	string runtimeSyncDirectory = "";
	// Verbosity Logging Count - this defines if verbose or debug logging is being used
	long verbosityCount = 0;
	// Monitor loop failures
	bool monitorFailures = false;
	// Help requested
	bool helpRequested = false;
	// Did the user specify --sync or --monitor
	bool syncOrMonitorMissing = false;
	// Was a no-sync type operation requested
	bool noSyncTaskOperationRequested = false;
	
	// DEVELOPER OPTIONS OUTPUT VARIABLES
	bool displayMemoryUsage = false;
	bool displaySyncOptions = false;
	
	// Application Version
	immutable string applicationVersion = "onedrive " ~ strip(import("version"));
	
	// Define 'exit' and 'failure' scopes
	scope(exit) {
		// Detail what scope was called
		if (debugLogging) {addLogEntry("Exit scope was called", ["debug"]);}
		// Perform synchronised exit
		performSynchronisedExitProcess("exitScope");
		// Setup signal handling for the exit scope
		setupExitScopeSignalHandler();
	}
	
	scope(failure) {
		// Detail what scope was called
		if (debugLogging) {addLogEntry("Failure scope was called", ["debug"]);}
		// Perform synchronised exit
		performSynchronisedExitProcess("failureScope");
		// Setup signal handling for the exit scope
		setupExitScopeSignalHandler();
	}
	
	// Read in application options as passed in
	try {
		bool printVersion = false;
		auto cliOptions = getopt(
			cliArgs,
			std.getopt.config.passThrough,
			std.getopt.config.bundling,
			std.getopt.config.caseSensitive,
			"confdir", "Set the directory used to store the configuration files", &confdirOption,
			"verbose|v+", "Print more details, useful for debugging (repeat for extra debugging)", &verbosityCount,
			"version", "Print the version and exit", &printVersion
		);
		
		// Print help and exit
		if (cliOptions.helpWanted) {
			cliArgs ~= "--help";
			helpRequested = true;
		}
		// Print the version and exit
		if (printVersion) {
			writeln(applicationVersion);
			exit(EXIT_SUCCESS);
		}
	} catch (GetOptException e) {
		// Option errors
		writeln(e.msg);
		writeln(genericHelpMessage);
		return EXIT_FAILURE;
	} catch (Exception e) {
		// Generic error
		writeln(e.msg);
		writeln(genericHelpMessage);
		return EXIT_FAILURE;
	}
	
	// Determine the application logging verbosity
	// - As these flags are used to reduce application processing when not required, specifically in a 'debug' scenario, both verboseLogging and debugLogging need to be enabled
	if (verbosityCount == 1) { verboseLogging = true;} // set __gshared bool verboseLogging in log.d
	if (verbosityCount >= 2) { verboseLogging = true; debugLogging = true;}   // set __gshared bool verboseLogging & debugLogging in log.d
	
	// Initialize the application logging class, as we know the application verbosity level
	// If we need to enable logging to a file, we can only do this once we know the application configuration which is done slightly later on
    initialiseLogging(verboseLogging, debugLogging);
	
	// Log application start time, log line has start time
	if (debugLogging) {addLogEntry("Application started", ["debug"]);}
	
	// Who are we running as? This will print the ProcessID, UID, GID and username the application is running as
	runtimeUserName = getUserName();
	
	// Print the application version and how this was compiled as soon as possible
	if (debugLogging) {
		addLogEntry("Application Version: " ~ applicationVersion, ["debug"]);
		addLogEntry("Application Compiled With: " ~ compilerDetails(), ["debug"]);
	
		// How was this application started - what options were passed in
		addLogEntry("Passed in 'cliArgs': " ~ to!string(cliArgs), ["debug"]);
		addLogEntry("Note: --confdir and --verbose are not listed in 'cliArgs' array", ["debug"]);
		addLogEntry("Passed in --confdir if present: " ~ confdirOption, ["debug"]);
		addLogEntry("Passed in --verbose count if present: " ~ to!string(verbosityCount), ["debug"]);
	}
	
	// Create a new AppConfig object with default values, 
	appConfig = new ApplicationConfig();
	// Update the default application configuration with the verbosity count so this can be used throughout the application as needed
	appConfig.verbosityCount = verbosityCount;
	
	// Initialise the application configuration, utilising --confdir if it was passed in
	// Otherwise application defaults will be used to configure the application
	if (!appConfig.initialise(confdirOption, helpRequested)) {
		// There was an error loading the user specified application configuration
		// Error message already printed
		return EXIT_FAILURE;
	}
	
	// Update the current runtime application configuration (default or 'config' file read in options) from any passed in command line arguments
	appConfig.updateFromArgs(cliArgs);
	
	// Set the default thread pool value based on configuration or maximum logical CPUs
	setDefaultApplicationThreads();
	
	// If --debug-https has been used, set the applicable flag
	debugHTTPSResponse = appConfig.getValueBool("debug_https"); // set __gshared bool debugHTTPSResponse in log.d now that we have read-in any CLI arguments
	
	// Read in the configured 'sync_dir' from appConfig with '~' if present correctly expanded based on the user environment
	runtimeSyncDirectory = appConfig.initialiseRuntimeSyncDirectory();
	
	// Are we doing a --sync or a --monitor operation? Both of these will be false if they are not set
	if ((!appConfig.getValueBool("synchronize")) && (!appConfig.getValueBool("monitor"))) {
		syncOrMonitorMissing = true; // --sync or --monitor is missing 
	}
	
	// Has the client been configured to use Intune SSO via Microsoft Identity Broker (microsoft-identity-broker) dbus session
	// This is ONLY possible on Linux, not FreeBSD or other platforms
	version (linux) {
		if (appConfig.getValueBool("use_intune_sso")) {
			// The client is configured to use Intune SSO via Microsoft Identity Broker dbus session
			addLogEntry("Client has been configured to use Intune SSO via Microsoft Identity Broker dbus session - checking usage criteria");
			// We need to check that the available dbus is actually available
			if(wait_for_broker()) {
				// Usage criteria met, will attempt to use Intune SSO via dbus
				addLogEntry("Intune SSO via Microsoft Identity Broker dbus session usage criteria met - will attempt to authenticate via Intune");
			} else {
				// Microsoft Identity Broker dbus is not available
				addLogEntry();
				addLogEntry("Required Microsoft Identity Broker dbus capability not found - disabling authentication via Intune SSO");
				addLogEntry();
				appConfig.setValueBool("use_intune_sso" , false);
			}
		}
	} else {
		// Ensure 'use_intune_sso' is disabled
		appConfig.setValueBool("use_intune_sso" , false);
	}
	
	// Has the user configured to use the 'Recycle Bin' locally, for any files that are deleted online?
	if (appConfig.getValueBool("use_recycle_bin")) {
		// Configure the internal application paths which will be used to move rather than delete any online deletes to
		appConfig.setRecycleBinPaths();
		
		// If we are not using --display-config, test if the Recycle Bin Paths exist on the file system
		if (!appConfig.getValueBool("display_config")) {
			
			// We need to test that the configured 'Recycle Bin' path is not within the configured 'sync_dir'
			if (appConfig.checkRecycleBinPathAsChildOfSyncDir) {
				// ERROR: 'Recycle Bin' path is a child of the configured 'sync_dir'
				addLogEntry();
				addLogEntry("ERROR: The configured 'recycle_bin_path' (" ~ appConfig.recycleBinParentPath ~ ") is located within the configured 'sync_dir' (" ~ appConfig.runtimeSyncDirectory ~ ").", ["info", "notify"]);
				addLogEntry("       This would cause locally recycled items to be re-uploaded to Microsoft OneDrive.");
				addLogEntry("       Please set 'recycle_bin_path' to a location outside of 'sync_dir' and restart the client.");
				addLogEntry();
				return EXIT_FAILURE;
			} else {
				// 'Recycle Bin' path is not within the configured 'sync_dir'
				// We need to ensure that the Recycle Bin Paths exist on the file system, and if they do not exist, create them
				// Test for appConfig.recycleBinFilePath
				if (!exists(appConfig.recycleBinFilePath)) {
					try {
						// Attempt to create the 'Recycle Bin' file path we have been configured with
						mkdirRecurse(appConfig.recycleBinFilePath);
						
						// Configure the applicable permissions for the folder
						if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ appConfig.recycleBinFilePath, ["debug"]);}
						appConfig.recycleBinFilePath.setAttributes(octal!700); // Set to 0700 as Trash may contain sensitive and is the expected default permissions by GIO or KIO
						
					} catch (std.file.FileException e) {
						// Creating the 'Recycle Bin' file path failed
						addLogEntry("ERROR: Unable to create the configured local 'Recycle Bin' file directory: " ~ e.msg, ["info", "notify"]);
						// Use exit scopes to shutdown API
						return EXIT_FAILURE;
					}
				}
				
				// Test for appConfig.recycleBinInfoPath
				if (!exists(appConfig.recycleBinInfoPath)) {
					try {
						// Attempt to create the 'Recycle Bin' info path we have been configured with
						mkdirRecurse(appConfig.recycleBinInfoPath);
						
						// Configure the applicable permissions for the folder
						if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ appConfig.recycleBinInfoPath, ["debug"]);}
						appConfig.recycleBinInfoPath.setAttributes(octal!700); // Set to 0700 as Trash may contain sensitive and is the expected default permissions by GIO or KIO
						
						
					} catch (std.file.FileException e) {
						// Creating the 'Recycle Bin' info path failed
						addLogEntry("ERROR: Unable to create the configured local 'Recycle Bin' info directory: " ~ e.msg, ["info", "notify"]);
						// Use exit scopes to shutdown API
						return EXIT_FAILURE;
					}
				}
			}
		}
	}
	
	// Are we performing some sort of 'no-sync' operation task?
	noSyncTaskOperationRequested = appConfig.hasNoSyncOperationBeenRequested(); // returns true if we are
	
	// If 'syncOrMonitorMissing' is true and 'noSyncTaskOperationRequested' is false (meaning we are not doing some 'no-sync' operation like '--display-sync-status', '--get-sharepoint-drive-id' or '--display-config'
	// - fail fast here to avoid setting up all the other components, database, initialising the API as this is all pointless if we just fail out later
	
	// If we are not using --display-config, perform this check
	if (!appConfig.getValueBool("display_config")) {
		if (syncOrMonitorMissing && !noSyncTaskOperationRequested) {
			// Before failing fast, has the client been authenticated and does the 'refresh_token' contain data
			if (exists(appConfig.refreshTokenFilePath) && getSize(appConfig.refreshTokenFilePath) > 0) {
				// fail fast - print error message that --sync or --monitor are missing
				printMissingOperationalSwitchesError();
				// Use exit scopes to shutdown API
				return EXIT_FAILURE;
			}
		}
	}
	
	// If --disable-notifications has not been used, check if everything exists to enable notifications
	if (!appConfig.getValueBool("disable_notifications")) {
		// If notifications was compiled in, we need to ensure that these variables are actually available before we enable GUI Notifications
		flagEnvironmentVariablesAvailable(appConfig.validateGUINotificationEnvironmentVariables());
		// If we are not using --display-config attempt to enable GUI notifications
		if (!appConfig.getValueBool("display_config")) {
			// Attempt to enable GUI Notifications
			validateDBUSServerAvailability();
		}
	}
	
	// cURL Version Compatibility Test
	// - Common warning for cURL version issue
	string distributionWarning = "         Please report this to your distribution, requesting an update to a newer cURL version, or consider upgrading it yourself for optimal stability.";
	// If 'force_http_11' = false, we need to check the curl version being used
	if (!appConfig.getValueBool("force_http_11")) {
		// get the curl version
		string curlVersion = getCurlVersionNumeric();
		
		// Is the version of curl or libcurl being used by the platform a known bad curl version for HTTP/2 support
		if (isBadCurlVersion(curlVersion)) {
			// add warning message
			string curlWarningMessage = format("WARNING: Your cURL/libcurl version (%s) has known HTTP/2 bugs that impact the use of this client.", curlVersion);
			addLogEntry();
			addLogEntry(curlWarningMessage, ["info", "notify"]);
			addLogEntry(distributionWarning);
			addLogEntry("         Downgrading all client operations to use HTTP/1.1 to ensure maximum operational stability.");
			addLogEntry("         Please read https://github.com/abraunegg/onedrive/blob/master/docs/usage.md#compatibility-with-curl for more information.");
			addLogEntry();
			appConfig.setValueBool("force_http_11" , true);
		}
	} else {
		// get the curl version - a bad curl version may still be in use
		string curlVersion = getCurlVersionNumeric();
		
		// Is the version of curl or libcurl being used by the platform a known bad curl version
		if (isBadCurlVersion(curlVersion)) {
			// add warning message
			string curlWarningMessage = format("WARNING: Your cURL/libcurl version (%s) has known operational bugs that impact the use of this client.", curlVersion);
			addLogEntry();
			addLogEntry(curlWarningMessage); // curl HTTP/1.1 downgrade in place meaning user took steps to remediate, perform standard logging with no GUI notification
			addLogEntry(distributionWarning);
			addLogEntry();
		}
	}
	
	// In a debug scenario, to assist with understanding the run-time configuration, ensure this flag is set
	if (debugLogging) {
		appConfig.setValueBool("display_running_config", true);
	}
	
	// Configure dryRun so that this can be used here & during shutdown
	dryRun = appConfig.getValueBool("dry_run");
	
	// As early as possible, now re-configure the logging class, given that we have read in any applicable 'config' file and updated the application running config from CLI input:
	// - Enable logging to a file if this is required
	// - Disable GUI notifications if this has been configured
	
	// Configure application logging to a log file only if this has been enabled
	// This is the earliest point that this can be done, as the client configuration has been read in, and any CLI arguments have been processed.
	// Either of those ('config' file, CLI arguments) could be enabling logging, thus this is the earliest point at which this can be validated and enabled.
	// The buffered logging also ensures that all 'output' to this point is also captured and written out to the log file
	if (appConfig.getValueBool("enable_logging")) {
		// Calculate the application logging directory
		string calculatedLogDirPath = appConfig.calculateLogDirectory();
		string calculatedLogFilePath;
		// Initialise using the configured logging directory
		if (verboseLogging) {addLogEntry("Using the following path to store the runtime application log: " ~ calculatedLogDirPath, ["verbose"]);}
		// Calculate the logfile name
		if (calculatedLogDirPath != appConfig.defaultHomePath) {
			// Log file is not going to the home directory
			string logfileName = runtimeUserName ~ ".onedrive.log";
			calculatedLogFilePath = buildNormalizedPath(buildPath(calculatedLogDirPath, logfileName));
		} else {
			// Log file is going to the users home directory
			calculatedLogFilePath = buildNormalizedPath(buildPath(calculatedLogDirPath, "onedrive.log"));
		}
		// Update the logging class to use 'calculatedLogFilePath' for the application log file now that this has been determined
		enableLogFileOutput(calculatedLogFilePath);
	}
	
	// Disable GUI Notifications if configured to do so
	// - This option is reverse action. If 'disable_notifications' is 'true', we need to send 'false'
	if (appConfig.getValueBool("disable_notifications")) {
		// disable_notifications is true, ensure GUI notifications is initialised with false so that NO GUI notification is sent
		disableGUINotifications(false);
		addLogEntry("Disabling GUI notifications as per user configuration");
	}
	
	// Perform a deprecated options check now that the config file (if present) and CLI options have all been parsed to advise the user that their option usage might change
	appConfig.checkDeprecatedOptions(cliArgs);
	
	// Configure Client Side Filtering (selective sync) by parsing and getting a usable regex for skip_file, skip_dir and sync_list config components
	selectiveSync = new ClientSideFiltering(appConfig);
	if (!selectiveSync.initialise()) {
		// exit here as something triggered a selective sync configuration failure
		return EXIT_FAILURE;
	}
	
	// Set runtimeDatabaseFile, this will get updated if we are using --dry-run
	runtimeDatabaseFile = appConfig.databaseFilePath;
	
	// DEVELOPER OPTIONS OUTPUT
	// Set to display memory details as early as possible
	displayMemoryUsage = appConfig.getValueBool("display_memory");
	// set to display sync options
	displaySyncOptions = appConfig.getValueBool("display_sync_options");
	
	// Display the current application configuration (based on all defaults, 'config' file parsing and/or options passed in via the CLI) and exit if --display-config has been used
	if ((appConfig.getValueBool("display_config")) || (appConfig.getValueBool("display_running_config"))) {
		// Display the application configuration
		appConfig.displayApplicationConfiguration();
		// Do we exit? We exit only if '--display-config' has been used
		if (appConfig.getValueBool("display_config")) {
			return EXIT_SUCCESS;
		}
	}
	
	// Check for basic application option conflicts - flags that should not be used together and/or flag combinations that conflict with each other, values that should be present and are not
	if (appConfig.checkForBasicOptionConflicts) {
		// Any error will have been printed by the function itself, but we need a small delay here to allow the buffered logging to output any error
		return EXIT_FAILURE;
	}
	
	// Check for --dry-run operation or a 'no-sync' operation where the 'dry-run' DB copy should be used
	// If this has been requested, we need to ensure that all actions are performed against the dry-run database copy, and, 
	// no actual action takes place - such as deleting files if deleted online, moving files if moved online or local, downloading new & changed files, uploading new & changed files
	if (dryRun || (noSyncTaskOperationRequested)) {
		// Cleanup any existing dry-run elements ... these should never be left hanging around and should be cleaned up first
		cleanupDatabaseFiles(appConfig.databaseFilePathDryRun);
		
		// If --dry-run
		if (dryRun) {
			// This is a --dry-run operation
			addLogEntry("DRY-RUN Configured. Output below shows what 'would' have occurred.");
		 
			// Make a copy of the original items.sqlite3 for use as the dry run copy if it exists
			if (exists(appConfig.databaseFilePath)) {
				// In a --dry-run --resync scenario, we should not copy the existing database file
				if (!appConfig.getValueBool("resync")) {
					// Copy the existing DB file to the dry-run copy
					addLogEntry("DRY-RUN: Copying items.sqlite3 to items-dryrun.sqlite3 to use for dry run operations");
					copy(appConfig.databaseFilePath,appConfig.databaseFilePathDryRun);
				} else {
					// No database copy due to --resync - an empty DB file will be used for the resync operation
					addLogEntry("DRY-RUN: No database copy created for --dry-run due to --resync also being used");
				}
			}
			
			// update runtimeDatabaseFile now that we are using the dry run path
			runtimeDatabaseFile = appConfig.databaseFilePathDryRun;
		}
	} else {
		// Cleanup any existing dry-run elements ... these should never be left hanging around
		cleanupDatabaseFiles(appConfig.databaseFilePathDryRun);
	}
	
	// Handle --logout as separate item, do not 'resync' on a --logout
	if (appConfig.getValueBool("logout")) {
		if (debugLogging) {addLogEntry("--logout requested", ["debug"]);}
		addLogEntry("Deleting the saved authentication status ...");
		if (!dryRun) {
			// Remove the 'refresh_token' file if present
			safeRemove(appConfig.refreshTokenFilePath);
			// Remove the 'intune_account' file if present
			safeRemove(appConfig.intuneAccountDetailsFilePath);
		} else {
			// --dry-run scenario ... technically we should not be making any local file changes .......
			addLogEntry("DRY-RUN: Not removing the saved authentication status");
		}
		// Exit
		return EXIT_SUCCESS;
	}
	
	// Handle --reauth to re-authenticate the client
	if (appConfig.getValueBool("reauth")) {
		if (debugLogging) {addLogEntry("--reauth requested", ["debug"]);}
		addLogEntry("Deleting the saved authentication status ... re-authentication requested");
		if (!dryRun) {
			// Remove the 'refresh_token' file if present
			safeRemove(appConfig.refreshTokenFilePath);
			// Remove the 'intune_account' file if present
			safeRemove(appConfig.intuneAccountDetailsFilePath);
		} else {
			// --dry-run scenario ... technically we should not be making any local file changes .......
			addLogEntry("DRY-RUN: Not removing the saved authentication status");
		}
	}
	
	// --resync should be considered a 'last resort item' or if the application configuration has changed, where a resync is needed .. the user needs to 'accept' this warning to proceed
	// If --resync has not been used (bool value is false), check the application configuration for 'changes' that require a --resync to ensure that the data locally reflects the users requested configuration
	if (appConfig.getValueBool("resync")) {
		// what is the risk acceptance for --resync?
		bool resyncRiskAcceptance = appConfig.displayResyncRiskForAcceptance();
		if (debugLogging) {addLogEntry("Returned --resync risk acceptance: " ~ to!string(resyncRiskAcceptance), ["debug"]);}
		
		// Action based on user response
		if (!resyncRiskAcceptance){
			// --resync risk not accepted
			return EXIT_FAILURE;
		} else {
			if (debugLogging) {addLogEntry("--resync issued and risk accepted", ["debug"]);}
			// --resync risk accepted, perform a cleanup of items that require a cleanup
			appConfig.cleanupHashFilesDueToResync();
			// Make a backup of the applicable configuration file
			appConfig.createBackupConfigFile();
			// Update hash files and generate a new config backup
			appConfig.updateHashContentsForConfigFiles();
			// Remove the items database
			processResyncDatabaseRemoval(runtimeDatabaseFile);
		}
	} else {
		// Is the application currently authenticated? If not, it is pointless checking if a --resync is required until the application is authenticated
		if (exists(appConfig.refreshTokenFilePath)) {
			// Has any of our application configuration that would require a --resync been changed?
			if (appConfig.applicationChangeWhereResyncRequired()) {
				// Application configuration has changed however --resync not issued, fail fast
				addLogEntry();
				addLogEntry("An application configuration change has been detected where a --resync is required", ["info", "notify"]);
				addLogEntry();
				return EXIT_RESYNC_REQUIRED;
			} else {
				// No configuration change that requires a --resync to be issued
				// Special cases need to be checked - if these options were enabled, it creates a false 'Resync Required' flag, so do not create a backup
				if ((!appConfig.getValueBool("list_business_shared_items"))) {
					// Make a backup of the applicable configuration file
					appConfig.createBackupConfigFile();
					// Update hash files and generate a new config backup
					appConfig.updateHashContentsForConfigFiles();
				}
			}
		}
	}
	
	// Implement https://github.com/abraunegg/onedrive/issues/1129
	// Force a synchronisation of a specific folder, only when using --synchronize --single-directory and ignoring all non-default skip_dir and skip_file rules
	if (appConfig.getValueBool("force_sync")) {
		// appConfig.checkForBasicOptionConflicts() has already checked for the basic requirements for --force-sync
		addLogEntry();
		addLogEntry("WARNING: Overriding application configuration to use application defaults for skip_dir and skip_file due to --sync --single-directory --force-sync being used");
		addLogEntry();
		bool forceSyncRiskAcceptance = appConfig.displayForceSyncRiskForAcceptance();
		if (debugLogging) {addLogEntry("Returned --force-sync risk acceptance: " ~ forceSyncRiskAcceptance, ["debug"]);}
		
		// Action based on user response
		if (!forceSyncRiskAcceptance){
			// --force-sync risk not accepted
			return EXIT_FAILURE;
		} else {
			// --force-sync risk accepted
			// reset set config using function to use application defaults
			appConfig.resetSkipToDefaults();
			// update sync engine regex with reset defaults
			selectiveSync.setDirMask(appConfig.getValueString("skip_dir"));
			selectiveSync.setFileMask(appConfig.getValueString("skip_file"));	
		}
	}
	
	// What IP Protocol are we going to use to access the network with
	appConfig.displayIPProtocol();
	
	// Test if OneDrive service can be reached, exit if it cant be reached
	if (debugLogging) {addLogEntry("Testing network to ensure network connectivity to Microsoft OneDrive Service", ["debug"]);}
	online = testInternetReachability(appConfig);
	
	// If we are not 'online' - how do we handle this situation?
	if (!online) {
		// We are unable to initialise the OneDrive API as we are not online
		if (!appConfig.getValueBool("monitor")) {
			// Running as --synchronize
			addLogEntry();
			addLogEntry("ERROR: Unable to reach the Microsoft OneDrive API service, unable to initialise application");
			addLogEntry();
			return EXIT_FAILURE;
		} else {
			// Running as --monitor
			addLogEntry();
			addLogEntry("Unable to reach the Microsoft OneDrive API service at this point in time, re-trying network tests based on applicable intervals");
			addLogEntry();
			// Run the re-try of Internet connectivity test
			online = retryInternetConnectivityTest(appConfig);
		}
	}
	
	// This needs to be a separate 'if' statement, as, if this was an 'if-else' from above, if we were originally offline and using --monitor, we would never get to this point
	if (online) {
		// Check Application Version
		if (!appConfig.getValueBool("disable_version_check")) {
			if (verboseLogging) {addLogEntry("Checking Application Version ...", ["verbose"]);}
			checkApplicationVersion();
		}
		
		// Initialise the OneDrive API
		if (verboseLogging) {addLogEntry("Attempting to initialise the OneDrive API ...", ["verbose"]);}
		OneDriveApi oneDriveApiInstance = new OneDriveApi(appConfig);
		appConfig.apiWasInitialised = oneDriveApiInstance.initialise();
		
		// Did the API initialise successfully?
		if (appConfig.apiWasInitialised) {
			if (verboseLogging) {addLogEntry("The OneDrive API was initialised successfully", ["verbose"]);}
			
			// Flag that we were able to initialise the API in the application config
			oneDriveApiInstance.debugOutputConfiguredAPIItems();
			oneDriveApiInstance.releaseCurlEngine();
			object.destroy(oneDriveApiInstance);
			oneDriveApiInstance = null;
			
			// Need to configure the itemDB and syncEngineInstance for 'sync' and 'non-sync' operations
			if (verboseLogging) {addLogEntry("Opening the item database ...", ["verbose"]);}
			
			// Configure the Item Database
			itemDB = new ItemDatabase(runtimeDatabaseFile);
			// Was the database successfully initialised?
			if (!itemDB.isDatabaseInitialised()) {
				// no .. destroy class
				itemDB = null;
				// exit application
				return EXIT_FAILURE;
			}
			
			// Initialise the syncEngine
			syncEngineInstance = new SyncEngine(appConfig, itemDB, selectiveSync);
			appConfig.syncEngineWasInitialised = syncEngineInstance.initialise();
			
			// Are we not doing a --sync or a --monitor operation?
			if (syncOrMonitorMissing) { // this is 'true' if --sync or a --monitor were not used

				// Do not perform a vacuum on exit, pointless
				performDatabaseVacuum = false;
				
				// Are we performing some sort of 'no-sync' task?
				// - Are we obtaining the Office 365 Drive ID for a given Office 365 SharePoint Shared Library?
				// - Are we displaying the sync status?
				// - Are we getting the URL for a file online?
				// - Are we listing who modified a file last online?
				// - Are we listing OneDrive Business Shared Items?
				// - Are we creating a shareable link for an existing file on OneDrive?
				// - Are we just creating a directory online, without any sync being performed?
				// - Are we just deleting a directory online, without any sync being performed?
				// - Are we renaming or moving a directory?
				// - Are we displaying the quota information?
				// - Did we just authorise the client?

				// --get-sharepoint-drive-id - Get the SharePoint Library drive_id
				if (appConfig.getValueString("sharepoint_library_name") != "") {
					// Get the SharePoint Library drive_id
					syncEngineInstance.querySiteCollectionForDriveID(appConfig.getValueString("sharepoint_library_name"));
					// Exit application
					// Use exit scopes to shutdown API and cleanup data
					return EXIT_SUCCESS;
				}

				// --display-sync-status - Query the sync status
				if (appConfig.getValueBool("display_sync_status")) {
					// path to query variable
					string pathToQueryStatusOn;
					// What path do we query?
					if (!appConfig.getValueString("single_directory").empty) {
						pathToQueryStatusOn = "/" ~ appConfig.getValueString("single_directory");
					} else {
						pathToQueryStatusOn = "/";
					}
					// Query the sync status
					syncEngineInstance.queryOneDriveForSyncStatus(pathToQueryStatusOn);
					// Exit application
					// Use exit scopes to shutdown API and cleanup data
					return EXIT_SUCCESS;
				}

				// --get-file-link - Get the URL path for a synced file
				if (appConfig.getValueString("get_file_link") != "") {
					// Query the OneDrive API for the file link
					syncEngineInstance.queryOneDriveForFileDetails(appConfig.getValueString("get_file_link"), runtimeSyncDirectory, "URL");
					// Exit application
					// Use exit scopes to shutdown API and cleanup data
					return EXIT_SUCCESS;
				}
				
				// --modified-by - Get listing the modified-by details of a provided path
				if (appConfig.getValueString("modified_by") != "") {
					// Query the OneDrive API for the last modified by details
					syncEngineInstance.queryOneDriveForFileDetails(appConfig.getValueString("modified_by"), runtimeSyncDirectory, "ModifiedBy");
					// Exit application
					// Use exit scopes to shutdown API and cleanup data
					return EXIT_SUCCESS;
				}
				
				// --list-shared-items - Get listing OneDrive Business Shared Items
				if (appConfig.getValueBool("list_business_shared_items")) {
					// Is this a business account type?
					if (appConfig.accountType == "business") {
						// List OneDrive Business Shared Items
						syncEngineInstance.listBusinessSharedObjects();
					} else {
						addLogEntry("ERROR: Unsupported account type for listing OneDrive Business Shared Items");
					}
					// Exit application
					// Use exit scopes to shutdown API
					return EXIT_SUCCESS;
				}
				
				// --create-share-link - Create a shareable link for an existing file, based on the local path
				if (appConfig.getValueString("create_share_link") != "") {
					// Query OneDrive for the file, and if valid, create a shareable link for the file
					
					// By default, the shareable link will be read-only. 
					// If the user adds: 
					//		--with-editing-perms 
					// this will create a writeable link
					syncEngineInstance.queryOneDriveForFileDetails(appConfig.getValueString("create_share_link"), runtimeSyncDirectory, "ShareableLink");
					// Exit application
					// Use exit scopes to shutdown API
					return EXIT_SUCCESS;
				}
				
				// --create-directory - Are we just creating a directory online, without any sync being performed?
				if ((appConfig.getValueString("create_directory") != "")) {
					// Handle the remote path creation and updating of the local database without performing a sync
					syncEngineInstance.createDirectoryOnline(appConfig.getValueString("create_directory"));
					// Exit application
					// Use exit scopes to shutdown API
					return EXIT_SUCCESS;
				}
				
				// --remove-directory - Are we just deleting a directory online, without any sync being performed?
				if ((appConfig.getValueString("remove_directory") != "")) {
					// Handle the remote path deletion without performing a sync
					syncEngineInstance.deleteByPathNoSync(appConfig.getValueString("remove_directory"));
					// Exit application
					// Use exit scopes to shutdown API
					return EXIT_SUCCESS;
				}
				
				// Are we renaming or moving a directory online?
				// 	onedrive --source-directory 'path/as/source/' --destination-directory 'path/as/destination'
				if ((appConfig.getValueString("source_directory") != "") && (appConfig.getValueString("destination_directory") != "")) {
					// We are renaming or moving a directory
					syncEngineInstance.moveOrRenameDirectoryOnline(appConfig.getValueString("source_directory"), appConfig.getValueString("destination_directory"));
					// Exit application
					// Use exit scopes to shutdown API
					return EXIT_SUCCESS;
				}
				
				// --display-quota - Are we displaying the quota information?
				if (appConfig.getValueBool("display_quota")) {
					// Query and respond with the quota details
					syncEngineInstance.queryOneDriveForQuotaDetails();
					// Exit application
					// Use exit scopes to shutdown API
					return EXIT_SUCCESS;
				}
				
				// --download-file - Are we downloading a single file from Microsoft OneDrive
				if ((appConfig.getValueString("download_single_file") != "")) {
					// Handle downloading the single file
					syncEngineInstance.downloadSingleFile(appConfig.getValueString("download_single_file"));
					// Exit application
					// Use exit scopes to shutdown API
					return EXIT_SUCCESS;
				}
				
				// If we get to this point, we have not performed a 'no-sync' task ..
				
				// Did we just authorise the client?
				if (appConfig.applicationAuthoriseResponseURIReceived) {
					// Authorisation activity
					if (exists(appConfig.refreshTokenFilePath)) {
						// OneDrive refresh token exists
						addLogEntry();
						addLogEntry("The application has been successfully authorised, but no extra command options have been specified.");
						addLogEntry();
						addLogEntry(genericHelpMessage);
						addLogEntry();
						// Use exit scopes to shutdown API
						return EXIT_SUCCESS;
					} else {
						// We just authorised, but refresh_token does not exist .. probably an auth error?
						addLogEntry();
						addLogEntry("Your application's authorisation was unsuccessful. Please review your URI response entry, then attempt authorisation again with a new URI response.");
						addLogEntry();
						// Use exit scopes to shutdown API
						return EXIT_FAILURE;
					}
				} else {
					// No authorisation activity - print error message
					printMissingOperationalSwitchesError();
					// Use exit scopes to shutdown API
					return EXIT_FAILURE;
				}
			}
		} else {
			// API could not be initialised
			addLogEntry("The OneDrive API could not be initialised");
			return EXIT_FAILURE;
		}
	}
	
	// Configure the sync directory based on the runtimeSyncDirectory configured directory
	if (verboseLogging) {addLogEntry("All application operations will be performed in the configured local 'sync_dir' directory: " ~ runtimeSyncDirectory, ["verbose"]);}
	// Try and set the 'sync_dir', attempt to create if it does not exist
	try {
		if (!exists(runtimeSyncDirectory)) {
			if (debugLogging) {addLogEntry("runtimeSyncDirectory: Configured 'sync_dir' is missing locally. Creating: " ~ runtimeSyncDirectory, ["debug"]);}
			
			// At this point 'sync_dir' is missing and we have requested to create it
			// However ... 'itemDB' is pointing to a valid database file
			// If this database has any entries, an empty 'sync_dir' will cause the application to think that all content in 'sync_dir' has been deleted
			// In this scenario, the application, depending on the options being used, may attempt to delete all files online - which is not desirable
			// Do a sanity check here to ensure that there are no database entries
		
			if (itemDB.getTotalRowCount() == 1) {
				// Technically an 'empty database'
				// An empty database will just have 1 row in it, that row being the account 'root' data added when the API is initially initialised above
				try {
					// Attempt to create the sync dir we have been configured with
					mkdirRecurse(runtimeSyncDirectory);
					// Configure the applicable permissions for the folder
					if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ runtimeSyncDirectory, ["debug"]);}
					runtimeSyncDirectory.setAttributes(appConfig.returnRequiredDirectoryPermissions());
				} catch (std.file.FileException e) {
					// Creating the sync directory failed
					addLogEntry("ERROR: Unable to create the configured local 'sync_dir' directory: " ~ e.msg, ["info", "notify"]);
					// Use exit scopes to shutdown API
					return EXIT_FAILURE;
				}
			} else {
				// Not an empty database
				addLogEntry();
				addLogEntry("An application cache state issue has been detected where a --resync is required", ["info", "notify"]);
				addLogEntry();
				return EXIT_RESYNC_REQUIRED;
			}
		}
	} catch (std.file.FileException e) {
		// Creating the sync directory failed
		addLogEntry("ERROR: Unable to test for the existence of the configured local 'sync_dir' directory: " ~ e.msg);
		// Use exit scopes to shutdown API
		return EXIT_FAILURE;
	}
	
	// Try and change to the working directory to the 'sync_dir' as configured
	try {
		chdir(runtimeSyncDirectory);
	// A FileSystem exception was thrown when attempting to change to the configured 'sync_dir'
	} catch (FileException e) {
		// Log error message
		addLogEntry("FATAL: Unable to change to the configured local 'sync_dir' directory: " ~ runtimeSyncDirectory);
		// A file system exception was generated
		displayFileSystemErrorMessage(e.msg, strip(getFunctionName!({})), runtimeSyncDirectory, FsErrorSeverity.fatal);
		// Use exit scopes to shutdown API as if we are unable to change to the 'sync_dir' we need to exit
		return EXIT_FAILURE;
	}
	
	// Do we need to validate the runtimeSyncDirectory to check for the presence of a '.nosync' file
	checkForNoMountScenario();
	
	// Is the sync engine initialised correctly?
	if (appConfig.syncEngineWasInitialised) {
		// Configure some initial variables
		string singleDirectoryPath;
		string localPath = ".";
		string remotePath = "/";
		
		// If not performing a --resync, check if there are interrupted downloads and/or uploads that need to be completed
		if (!appConfig.getValueBool("resync")) {
			// Check if there are any downloads that need to be resumed
			if (syncEngineInstance.checkForResumableDownloads) {
				// Need to re-process the the 'resumable data' to resume the download
				addLogEntry("There are interrupted downloads that need to be resumed ...");
				// Process the resumable download files
				syncEngineInstance.processResumableDownloadFiles();
			}
			
			// Check if there are interrupted upload session(s)
			if (syncEngineInstance.checkForInterruptedSessionUploads) {
				// Need to re-process the session upload files to resume the failed session uploads
				addLogEntry("There are interrupted session uploads that need to be resumed ...");
				// Process the session upload files
				syncEngineInstance.processInterruptedSessionUploads();
			}
		} else {
			// Clean up any downloads that were due to be resumed, but will not be resumed due to --resync being used
			syncEngineInstance.clearInterruptedDownloads();
			
			// Clean up any uploads that were due to be resumed, but will not be resumed due to --resync being used
			syncEngineInstance.clearInterruptedSessionUploads();
		}
		
		// Are we doing a single directory operation (--single-directory) ?
		if (!appConfig.getValueString("single_directory").empty) {
			// Ensure that the value stored for appConfig.getValueString("single_directory") does not contain any extra quotation marks
			string originalSingleDirectoryValue = appConfig.getValueString("single_directory");
			// Strip quotation marks from provided path to ensure no issues within a Docker environment when using passed in values
			string updatedSingleDirectoryValue = strip(originalSingleDirectoryValue, "\"");
			// Set singleDirectoryPath
			singleDirectoryPath = updatedSingleDirectoryValue;
			
			// Ensure that this is a normalised relative path to runtimeSyncDirectory
			string normalisedRelativePath = replace(buildNormalizedPath(absolutePath(singleDirectoryPath)), buildNormalizedPath(absolutePath(runtimeSyncDirectory)), "." );
			
			// The user provided a directory to sync within the configured 'sync_dir' path
			// This also validates if the path being used exists online and/or does not have a 'case-insensitive match'
			syncEngineInstance.setSingleDirectoryScope(normalisedRelativePath);
			
			// Does the directory we want to sync actually exist locally?
			if (!exists(singleDirectoryPath)) {
				// The requested path to use with --single-directory does not exist locally within the configured 'sync_dir'
				addLogEntry("WARNING: The requested path for --single-directory does not exist locally. Creating requested path within " ~ runtimeSyncDirectory, ["info", "notify"]);
				// Attempt path creation
				try {
					// Attempt to create the required --single-directory path locally
					mkdirRecurse(singleDirectoryPath);
					// Configure the applicable permissions for the folder
					if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ singleDirectoryPath, ["debug"]);}
					singleDirectoryPath.setAttributes(appConfig.returnRequiredDirectoryPermissions());
				} catch (std.file.FileException e) {
					// Creating the sync directory failed
					addLogEntry("ERROR: Unable to create the required --single-directory path: " ~ e.msg, ["info", "notify"]);
					// Use exit scopes to shutdown API
					return EXIT_FAILURE;
				}
			}
			
			// Update the paths that we use to perform the sync actions
			localPath = singleDirectoryPath;
			remotePath = singleDirectoryPath;
			
			// Display that we are syncing from a specific path due to --single-directory
			if (verboseLogging) {addLogEntry("Syncing changes from this selected path: " ~ singleDirectoryPath, ["verbose"]);}
		}
		
		// Handle SIGINT, SIGTERM and SIGSEGV signals
		setupSignalHandler();
		
		// Are we doing a --sync operation? This includes doing any --single-directory operations
		if (appConfig.getValueBool("synchronize")) {
			// We are not using this, so destroy it early
			object.destroy(filesystemMonitor);
			filesystemMonitor = null;
		
			// Did the user specify --upload-only?
			if (appConfig.getValueBool("upload_only")) {
				// Perform the --upload-only sync process
				performUploadOnlySyncProcess(localPath);
			}
			
			// Did the user specify --download-only?
			if (appConfig.getValueBool("download_only")) {
				// Only download data from OneDrive
				syncEngineInstance.syncOneDriveAccountToLocalDisk();
				// Perform the DB consistency check 
				// This will also delete any out-of-sync flagged items if configured to do so
				syncEngineInstance.performDatabaseConsistencyAndIntegrityCheck();
				// Do we cleanup local files?
				// - Deletes of data from online will already have been performed, but what we are now doing is searching the local filesystem
				//   for any new data locally, that usually would be uploaded to OneDrive, but instead, because of the options being
				//   used, will need to be deleted from the local filesystem
				if (appConfig.getValueBool("cleanup_local_files")) {
					// Perform the filesystem walk
					syncEngineInstance.scanLocalFilesystemPathForNewData(localPath);
				}
			}
			
			// If no use of --upload-only or --download-only
			if ((!appConfig.getValueBool("upload_only")) && (!appConfig.getValueBool("download_only"))) {
				// Perform the standard sync process
				performStandardSyncProcess(localPath);
			}
		
			// Detail the outcome of the sync process
			displaySyncOutcome();
		}
		
		// Are we doing a --monitor operation?
		if (appConfig.getValueBool("monitor")) {
			// Update the flag given we are running with --monitor
			performFileSystemMonitoring = true;
			
			// Set initial variable for when we last uploaded something or made an online change from a local inotify event
			lastLocalWrite = MonoTime.currTime() - dur!"hours"(24);
			
			// Is Display Manager Integration enabled?
			if (appConfig.getValueBool("display_manager_integration")) {
				// Attempt to configure the desktop integration whilst the client is running in --monitor mode
				attemptFileManagerIntegration();
			}
		
			// If 'webhooks' are enabled, this is going to conflict with 'websockets' if the OS cURL library supports websockets
			if (appConfig.getValueBool("webhook_enabled") && appConfig.curlSupportsWebSockets) {
				// We have to disable 'websocket' support
				addLogEntry();
				addLogEntry("WARNING: WebSocket support has been disabled because Webhooks are already configured to monitor Microsoft Graph API changes.");
				addLogEntry("         Only one API notification method can be active at a time.");
				addLogEntry();
				// Set the flag that this will not be used
				appConfig.curlSupportsWebSockets = false;
			} else {
				// Double check scenario, this time 'false' checking 'webhook_enabled'
				if ((!appConfig.getValueBool("webhook_enabled")) && (appConfig.curlSupportsWebSockets)) {
				
					// If we are doing --upload-only however .. we need to 'ignore' online change
					if (!appConfig.getValueBool("upload_only")) {
						// Did the user configure to disable 'websocket' support?
						if (!appConfig.getValueBool("disable_websocket_support")) {
							// Log that we are attempting to enable WebSocket Support
							addLogEntry("Attempting to enable WebSocket support to monitor Microsoft Graph API changes in near real-time.");
							
							// Obtain the WebSocket Notification URL from the API endpoint
							syncEngineInstance.obtainWebSocketNotificationURL();
							
							// Were we able to correctly obtain the endpoint response and build the socket.io WS endpoint
							if (appConfig.websocketNotificationUrlAvailable) {
								// Notification URL is available
								if (oneDriveSocketIo is null) {
									oneDriveSocketIo = new OneDriveSocketIo(thisTid, appConfig);
									oneDriveSocketIo.start();
								}
								addLogEntry("Enabled WebSocket support to monitor Microsoft Graph API changes in near real-time.");
							} else {
								addLogEntry("ERROR: Unable to configure WebSocket support to monitor Microsoft Graph API changes in near real-time.");
							}
						} else {
							// WebSocket Support has been disabled
							addLogEntry("WebSocket support has been disabled by user configuration.");
						}
					} else {
						// --upload only being used
						addLogEntry("Online changes will not be monitored by WebSocket support due to --upload-only");
						// Set the flag that this will not be used
						appConfig.curlSupportsWebSockets = false;
					}
				}
			}
			
			// What are the current values for the platform we are running on
			string maxOpenFilesSoft = strip(to!string(getSoftOpenFilesLimit()));
			string maxOpenFilesHard = strip(to!string(getHardOpenFilesLimit()));
			// What is the currently configured maximum inotify watches that can be used
			string maxInotifyWatches = strip(getMaxInotifyWatches());
			
			// Start the monitor process
			addLogEntry("OneDrive synchronisation interval (seconds): " ~ to!string(appConfig.getValueLong("monitor_interval")));
			
			// If we are in a --download-only method of operation, the output of these is not required
			if (!appConfig.getValueBool("download_only")) {
				if (verboseLogging) {
					addLogEntry("Maximum allowed open files (soft):           " ~ maxOpenFilesSoft, ["verbose"]);
					addLogEntry("Maximum allowed open files (hard):           " ~ maxOpenFilesHard, ["verbose"]);
					addLogEntry("Maximum allowed inotify user watches:        " ~ maxInotifyWatches, ["verbose"]);
				}
			}
			
			// Configure the monitor class
			filesystemMonitor = new Monitor(appConfig, selectiveSync);
			
			// Delegated function for when inotify detects a new local directory has been created
			filesystemMonitor.onDirCreated = delegate(string path) {
				// Handle .folder creation if skip_dotfiles is enabled
				if ((appConfig.getValueBool("skip_dotfiles")) && (isDotFile(path))) {
					if (verboseLogging) {addLogEntry("[M] Skipping watching local path - .folder found & --skip-dot-files enabled: " ~ path, ["verbose"]);}
				} else {
					if (verboseLogging) {addLogEntry("[M] Local directory created: " ~ path, ["verbose"]);}
					try {
						syncEngineInstance.scanLocalFilesystemPathForNewData(path);
						markLocalWrite();
					} catch (CurlException e) {
						if (verboseLogging) {addLogEntry("Offline, cannot create remote dir: " ~ path, ["verbose"]);}
					} catch (Exception e) {
						addLogEntry("Cannot create remote directory: " ~ e.msg, ["info", "notify"]);
					}
				}
			};
			
			// Delegated function for when inotify detects a local file has been changed
			filesystemMonitor.onFileChanged = delegate(string[] changedLocalFilesToUploadToOneDrive) {
				// Handle a potentially locally changed file
				// Logging for this event moved to handleLocalFileTrigger() due to threading and false triggers from scanLocalFilesystemPathForNewData() above
				syncEngineInstance.handleLocalFileTrigger(changedLocalFilesToUploadToOneDrive);
				markLocalWrite();
				if (verboseLogging) {addLogEntry("[M] Total number of local file(s) added or changed: " ~ to!string(changedLocalFilesToUploadToOneDrive.length), ["verbose"]);}
			};

			// Delegated function for when inotify detects a delete event
			filesystemMonitor.onDelete = delegate(string path) {
				if (verboseLogging) {addLogEntry("[M] Local item deleted: " ~ path, ["verbose"]);}
				try {
					// The path has been deleted .. we cannot use isDir or isFile to advise what was deleted. This is the best we can Do
					addLogEntry("The operating system sent a deletion notification. Trying to delete this item as requested: " ~ path);
					// perform the delete action
					syncEngineInstance.deleteByPath(path);
					markLocalWrite();
				} catch (CurlException e) {
					if (verboseLogging) {addLogEntry("Offline, cannot delete item: " ~ path, ["verbose"]);}
				} catch (SyncException e) {
					if (e.msg == "The item to delete is not in the local database") {
						if (verboseLogging) {addLogEntry("Item cannot be deleted from Microsoft OneDrive because it was not found in the local database", ["verbose"]);}
					} else {
						addLogEntry("Cannot delete remote item: " ~ e.msg, ["info", "notify"]);
					}
				} catch (FileException e) {
					// Path is gone locally, log and continue.
					addLogEntry("ERROR: The local file system returned an error with the following message: " ~ e.msg, ["verbose"]);
				} catch (Exception e) {
					addLogEntry("Cannot delete remote item: " ~ e.msg, ["info", "notify"]);
				}
			};
			
			// Delegated function for when inotify detects a move event
			filesystemMonitor.onMove = delegate(string from, string to) {
				if (verboseLogging) {addLogEntry("[M] Local item moved: " ~ from ~ " -> " ~ to, ["verbose"]);}
				try {
					// Handle .folder -> folder if skip_dotfiles is enabled
					if ((appConfig.getValueBool("skip_dotfiles")) && (isDotFile(from))) {
						// .folder -> folder handling - has to be handled as a new folder
						syncEngineInstance.scanLocalFilesystemPathForNewData(to);
					} else {
						syncEngineInstance.uploadMoveItem(from, to);
					}
					markLocalWrite();
				} catch (CurlException e) {
					if (verboseLogging) {addLogEntry("Offline, cannot move item !", ["verbose"]);}
				} catch (Exception e) {
					addLogEntry("Cannot move item: " ~ e.msg, ["info", "notify"]);
				}
			};
			
			// Initialise the local filesystem monitor class using inotify to monitor for local filesystem changes
			// If we are in a --download-only method of operation, we do not enable local filesystem monitoring
			if (!appConfig.getValueBool("download_only")) {
				// Not using --download-only
				try {
					addLogEntry("Initialising filesystem inotify monitoring ...", ["info", "notify"]);
					filesystemMonitor.initialise();
					addLogEntry("Performing initial synchronisation to ensure consistent local state ...");
				} catch (MonitorException e) {	
					// monitor class initialisation failed
					addLogEntry("ERROR: " ~ e.msg);
					return EXIT_FAILURE;
				}
			}
		
			// Filesystem monitor loop variables
			// Immutables
			immutable auto checkOnlineInterval = dur!"seconds"(appConfig.getValueLong("monitor_interval"));
			immutable auto githubCheckInterval = dur!"seconds"(86400);
			immutable auto localEchoDebounce = dur!"seconds"(10);
			immutable ulong fullScanFrequency = appConfig.getValueLong("monitor_fullscan_frequency");
			immutable ulong logOutputSuppressionInterval = appConfig.getValueLong("monitor_log_frequency");
			immutable bool webhookEnabled = appConfig.getValueBool("webhook_enabled");
			immutable string loopStartOutputMessage = "################################################## NEW LOOP ##################################################";
			immutable string loopStopOutputMessage = "################################################ LOOP COMPLETE ###############################################";
			
			// Changeable variables
			ulong monitorLoopFullCount = 0;
			ulong fullScanFrequencyLoopCount = 0;
			ulong monitorLogOutputLoopCount = 0;
			MonoTime lastCheckTime = MonoTime.currTime();
			MonoTime lastGitHubCheckTime = MonoTime.currTime();
			
			while (performFileSystemMonitoring) {
				// Do we need to validate the runtimeSyncDirectory to check for the presence of a '.nosync' file - the disk may have been ejected ..
				checkForNoMountScenario();
			
				// If we are in a --download-only method of operation, there is no filesystem monitoring, so no inotify events to check
				if (!appConfig.getValueBool("download_only")) {
					// Process any inotify events
					processInotifyEvents(true);
				}
				
				// WebSocket and Webhook Notification Handling
				bool notificationReceived = false;
				
				// If we are doing --upload-only however .. we need to 'ignore' online change
				if (!appConfig.getValueBool("upload_only")) {
					// Check for notifications pushed from Microsoft to the webhook
					if (webhookEnabled) {
						// Create a subscription on the first run, or renew the subscription
						// on subsequent runs when it is about to expire.
						if (oneDriveWebhook is null) {
							oneDriveWebhook = new OneDriveWebhook(thisTid, appConfig);
							oneDriveWebhook.serve();
						} else {
							oneDriveWebhook.createOrRenewSubscription();
						}
					} else {
						// WebSocket support is enabled by default, but only if the version of libcurl supports it
						if (appConfig.curlSupportsWebSockets) {
							// Did the user configure to disable 'websocket' support?
							if (!appConfig.getValueBool("disable_websocket_support")) {
								// Do we need to renew the notification URL?
								auto renewEarly = dur!"seconds"(120);
								if (appConfig.websocketNotificationUrlAvailable && appConfig.websocketUrlExpiry.length) {
									auto expiry = SysTime.fromISOExtString(appConfig.websocketUrlExpiry);
									auto now    = Clock.currTime(UTC());
									if (expiry - now <= renewEarly) {
										try {
											// Obtain the WebSocket Notification URL from the API endpoint
											syncEngineInstance.obtainWebSocketNotificationURL();
											if (debugLogging) addLogEntry("Refreshed WebSocket notification URL prior to expiry", ["debug"]);
										} catch (Exception e) {
											if (debugLogging) addLogEntry("Failed to refresh WebSocket notification URL: " ~ e.msg, ["debug"]);
										}
									}
								}
							}
						}
					}
				}
				
				// Get the current time this loop is starting
				auto currentTime = MonoTime.currTime();
				
				// Do we perform a sync with OneDrive?
				if ((currentTime - lastCheckTime >= checkOnlineInterval) || (monitorLoopFullCount == 0)) {
					// Increment relevant counters
					monitorLoopFullCount++;
					fullScanFrequencyLoopCount++;
					monitorLogOutputLoopCount++;
					
					// If full scan at a specific frequency enabled?
					if (fullScanFrequency > 0) {
						// Full Scan set for some 'frequency' - do we flag to perform a full scan of the online data?
						if (fullScanFrequencyLoopCount > fullScanFrequency) {
							// set full scan trigger for true up
							if (debugLogging) {addLogEntry("Enabling Full Scan True Up (fullScanFrequencyLoopCount > fullScanFrequency), resetting fullScanFrequencyLoopCount = 1", ["debug"]);}
							fullScanFrequencyLoopCount = 1;
							appConfig.fullScanTrueUpRequired = true;
						} else {
							// unset full scan trigger for true up
							if (debugLogging) {addLogEntry("Disabling Full Scan True Up", ["debug"]);}
							appConfig.fullScanTrueUpRequired = false;
						}
					} else {
						// No it is disabled - ensure this is false
						appConfig.fullScanTrueUpRequired = false;
					}
					
					// Loop Start
					if (debugLogging) {
						addLogEntry(loopStartOutputMessage, ["debug"]);
						addLogEntry("Total Run-Time Loop Number:     " ~ to!string(monitorLoopFullCount), ["debug"]);
						addLogEntry("Full Scan Frequency Loop Number: " ~ to!string(fullScanFrequencyLoopCount), ["debug"]);
					}
					SysTime startFunctionProcessingTime = Clock.currTime();
					if (debugLogging) {addLogEntry("Start Monitor Loop Time:        " ~ to!string(startFunctionProcessingTime), ["debug"]);}
					
					// Do we perform any monitor console logging output suppression?
					// 'monitor_log_frequency' controls how often, in a non-verbose application output mode, how often 
					// the full output of what is occurring is done. This is done to lessen the 'verbosity' of non-verbose 
					// logging, but only when running in --monitor
					if (monitorLogOutputLoopCount > logOutputSuppressionInterval) {
						// re-enable the logging output as required
						monitorLogOutputLoopCount = 1;
						if (debugLogging) {addLogEntry("Allowing initial sync log output", ["debug"]);}
						appConfig.suppressLoggingOutput = false;
					} else {
						// do we suppress the logging output to absolute minimal
						if (monitorLoopFullCount == 1) {
							// application startup with --monitor
							if (debugLogging) {addLogEntry("Allowing initial sync log output", ["debug"]);}
							appConfig.suppressLoggingOutput = false;
						} else {
							// only suppress if we are not doing --verbose or higher
							if (appConfig.verbosityCount == 0) {
								if (debugLogging) {addLogEntry("Suppressing --monitor log output", ["debug"]);}
								appConfig.suppressLoggingOutput = true;
							} else {
								if (debugLogging) {addLogEntry("Allowing log output", ["debug"]);}
								appConfig.suppressLoggingOutput = false;
							}
						}
					}
					
					// How long has the application been running for?
					auto elapsedTime = Clock.currTime() - applicationStartTime;
					if (debugLogging) {addLogEntry("Application run-time thus far: " ~ to!string(elapsedTime), ["debug"]);}
					
					// Need to re-validate that the client is still online for this loop
					if (testInternetReachability(appConfig)) {
						// Starting a sync - we are online
						addLogEntry("Starting a sync with Microsoft OneDrive");
						
						// Attempt to reset syncFailures from any prior loop
						syncEngineInstance.resetSyncFailures();
						
						// Update cached quota details from online as this may have changed online in the background outside of this application
						syncEngineInstance.freshenCachedDriveQuotaDetails();
						
						// Did the user specify --upload-only?
						if (appConfig.getValueBool("upload_only")) {
							// Perform the --upload-only sync process
							performUploadOnlySyncProcess(localPath, filesystemMonitor);
						} else {
							// Perform the standard sync process
							performStandardSyncProcess(localPath, filesystemMonitor);
						}
						
						// Handle any new inotify events
						processInotifyEvents(true);
						
						// Detail the outcome of the sync process
						displaySyncOutcome();
						
						// Cleanup sync process arrays
						syncEngineInstance.cleanupArrays();
						
						// Write WAL and SHM data to file for this loop and release memory used by in-memory processing
						if (debugLogging) {addLogEntry("Merge contents of WAL and SHM files into main database file", ["debug"]);}
						itemDB.performCheckpoint("PASSIVE");
					} else {
						// Not online
						addLogEntry("Microsoft OneDrive service is not reachable at this time. Will re-try on next sync attempt.");
					}
					
					// Output end of loop processing times
					SysTime endFunctionProcessingTime = Clock.currTime();
					if (debugLogging) {
						addLogEntry("End Monitor Loop Time:                " ~ to!string(endFunctionProcessingTime), ["debug"]);
						addLogEntry("Elapsed Monitor Loop Processing Time: " ~ to!string((endFunctionProcessingTime - startFunctionProcessingTime)), ["debug"]);
					}
					
					// Release all the curl instances used during this loop
					// New curl instances will be established on next loop
					if (debugLogging) {addLogEntry("CurlEngine Pool Size PRE Cleanup: " ~ to!string(curlEnginePoolLength()), ["debug"]);}
					releaseAllCurlInstances(); // Release all CurlEngine instances
					if (debugLogging) {addLogEntry("CurlEngine Pool Size POST Cleanup: " ~ to!string(curlEnginePoolLength()) , ["debug"]);}
					
					// Display memory details before garbage collection
					if (displayMemoryUsage) {
						addLogEntry("Monitor Loop Count:   " ~ to!string(monitorLoopFullCount));
						// Get the current time in the local timezone
						auto timeStamp = leftJustify(Clock.currTime().toString(), 28, '0');
						addLogEntry("Timestamp:            " ~ to!string(timeStamp));
						addLogEntry("Application Run Time: " ~ to!string(elapsedTime));
						// Display memory stats before GC cleanup
						displayMemoryUsagePreGC();
					}
					// Perform Garbage Collection
					GC.collect();
					// Return free memory to the OS
					GC.minimize();
					// Display memory details after garbage collection
					if (displayMemoryUsage) displayMemoryUsagePostGC();
					
					// Log that this loop is complete
					if (debugLogging) {addLogEntry(loopStopOutputMessage, ["debug"]);}
					
					// performSync complete, set lastCheckTime to current time
					lastCheckTime = MonoTime.currTime();
					
					// Developer break via config option
					if (appConfig.getValueLong("monitor_max_loop") > 0) {
						// developer set option to limit --monitor loops
						if (monitorLoopFullCount == (appConfig.getValueLong("monitor_max_loop"))) {
							performFileSystemMonitoring = false;
							addLogEntry("Exiting after " ~ to!string(monitorLoopFullCount) ~ " loops due to developer set option");
						}
					}
				}
				
				if (performFileSystemMonitoring) {	
					auto nextCheckTime = lastCheckTime + checkOnlineInterval;
					currentTime = MonoTime.currTime();
					auto sleepTime = nextCheckTime - currentTime;
					if (debugLogging) {addLogEntry("Sleep for " ~ to!string(sleepTime), ["debug"]);}
					
					if (filesystemMonitor.initialised || webhookEnabled) {

						if (filesystemMonitor.initialised) {
							// If local monitor is on and is waiting (previous event was not from webhook)
							
							// Obsidian Editor has been written in such a way that it is constantly writing each and every keystroke to a file.
							// Not only is this really bad application behaviour, for this client, this means the application is constantly writing to disk, thus attempting to upload file changes.
							// Unfortunately Obsidian on Linux does not provide a built-in way to disable atomic saves or switch to a backup-copy method via configuration.
							if (appConfig.getValueBool("delay_inotify_processing")) {
								Thread.sleep(dur!("seconds")(to!int(appConfig.getValueLong("inotify_delay"))));
							}
							
							// Start the filesystem monitor (inotify) worker and wait for inotify event
							if (!notificationReceived) {
								filesystemMonitor.send(true);
							}
						}
						
						// Adjust sleepTime based on webhook/websocket only when NOT upload_only
						if (!appConfig.getValueBool("upload_only")) {
							if (webhookEnabled) {
								Duration nextWebhookCheckDuration = oneDriveWebhook.getNextExpirationCheckDuration();
								if (nextWebhookCheckDuration < sleepTime) sleepTime = nextWebhookCheckDuration;
								notificationReceived = false;
							} else if (!appConfig.getValueBool("disable_websocket_support") && appConfig.curlSupportsWebSockets) {
								Duration nextWebsocketCheckDuration = oneDriveSocketIo.getNextExpirationCheckDuration();
								if (nextWebsocketCheckDuration < sleepTime) sleepTime = nextWebsocketCheckDuration;
							}
						}
						
						// ALWAYS wait for FS worker, but only track webhook/websocket if NOT '--upload-only'
						int res = 1;
						bool onlineSignal = false;

						if (appConfig.getValueBool("upload_only")) {
							receiveTimeout(sleepTime, (int msg) { res = msg; });
						} else {
							receiveTimeout(sleepTime, (int msg) { res = msg; }, (ulong _) { onlineSignal = true; });
						}

						// Debug logging of worker status
						if (debugLogging) {
							addLogEntry("worker status = " ~ to!string(res), ["debug"]);
							if (!appConfig.getValueBool("upload_only")) {
								addLogEntry("notificationReceived = " ~ to!string(onlineSignal), ["debug"]);
							}
						}
						
						// Empirical evidence shows that Microsoft often sends multiple
						// notifications for one single change, so we need a loop to exhaust
						// all signals that were queued up by the webhook. The notifications
						// do not contain any actual changes, and we will always rely do the
						// delta endpoint to sync to latest. Therefore, only one sync run is
						// good enough to catch up for multiple notifications.
						
						// Only process online notifications if NOT '--upload-only'
						if (!appConfig.getValueBool("upload_only") && onlineSignal) {
							int signalCount = 1;
							while (true) {
								auto more = receiveTimeout(dur!"seconds"(-1), (ulong _) {});
								if (more) {
									signalCount++;
								} else {
									auto now = MonoTime.currTime();
									auto sinceLocal = now - lastLocalWrite;
									if (sinceLocal < localEchoDebounce) {
										if (debugLogging) {
											addLogEntry(
												"Debounced online refresh signal (" ~
												to!string(sinceLocal.total!"msecs"()) ~ " ms since local write; threshold " ~
												to!string(localEchoDebounce.total!"msecs"()) ~ " ms)",
												["debug"]
											);
										}
										
										// Ignore this reflection; skip the immediate online scan.
										// Next push or the regular monitor cadence will pick up genuine remote changes.
										break;
									}
									
									// Get the signal timestamp - this is as close as possible to when this was received
									SysTime signalTimeStamp = Clock.currTime();
									signalTimeStamp.fracSecs = Duration.zero;
									
									// Log what signal we received
									if (webhookEnabled) {
										string webhookLogEntry = format("Received %s signal(s) from Webhook handler (%s)", to!string(signalCount), to!string(signalTimeStamp));
										addLogEntry(webhookLogEntry);
									} else {
										string websocketLogEntry = format("Received %s signal(s) from WebSocket handler (%s)", to!string(signalCount), to!string(signalTimeStamp));
										addLogEntry(websocketLogEntry);
									}
									
									// Perform online callback action
									oneDriveOnlineCallback();
									break;
								}
							}
						}

						// Worker failure remains outside '--upload-only' filter
						if (res == -1) {
							addLogEntry("ERROR: Monitor worker failed.");
							monitorFailures = true;
							performFileSystemMonitoring = false;
						}
					} else {
						// no hooks available, nothing to check
						Thread.sleep(sleepTime);
					}
				}
			}
		}
	} else {
		// Exit application as the sync engine could not be initialised
		addLogEntry("Application Sync Engine could not be initialised correctly");
		// Use exit scope
		return EXIT_FAILURE;
	}
	
	// Exit application using exit scope
	if (!syncEngineInstance.syncFailures && !monitorFailures) {
		return EXIT_SUCCESS;
	} else {
		return EXIT_FAILURE;
	}
}

// Set default application threads
void setDefaultApplicationThreads() {
	// Read in system values
	int configuredThreads = to!int(appConfig.getValueLong("threads"));
	int systemCPUs = totalCPUs;
	
	// Warning if configuredThreads is too high
	if (configuredThreads > systemCPUs) {
		addLogEntry();
		addLogEntry("WARNING: Configured 'threads = " ~ to!string(configuredThreads) ~ "' exceeds available CPU cores (" ~ to!string(systemCPUs) ~ ").");
		addLogEntry("         This may lead to reduced performance, CPU contention, and instability. For best results, set 'threads' no higher than the number of physical CPU cores.");
		addLogEntry();
	}
	
	// Set the default threads based on configured option
	defaultPoolThreads(configuredThreads);
}

// Retrieves the maximum inotify watches allowed by the system
string getMaxInotifyWatches() {
	// Predefined Versions
	// https://dlang.org/spec/version.html#predefined-versions
	version (linux) {
		try {
			// Read max inotify watches from procfs on Linux
			return strip(readText("/proc/sys/fs/inotify/max_user_watches"));
		} catch (Exception e) {
			return "Unknown (Error reading /proc/sys/fs/inotify/max_user_watches)";
		}
	} else version (FreeBSD) {
		// FreeBSD uses kqueue instead of inotify, no direct equivalent
		return "N/A (uses kqueue)";
	} else version (OpenBSD) {
		// OpenBSD uses kqueue instead of inotify, no direct equivalent
		return "N/A (uses kqueue)";
	} else {
		return "Unsupported platform";
	}
}

// Print error message when --sync or --monitor has not been used and no valid 'no-sync' operation was requested
void printMissingOperationalSwitchesError() {
	// notify the user that --sync or --monitor were missing
	addLogEntry();
	addLogEntry("Your command line input is missing either the '--sync' or '--monitor' switches. Please include one (but not both) of these switches in your command line, or refer to 'onedrive --help' for additional guidance.");
	addLogEntry();
	addLogEntry("It is important to note that you must include one of these two arguments in your command line for the application to perform a synchronisation with Microsoft OneDrive");
	addLogEntry();
}

// Function used for WebSocket or Webhook callbacks to perform specific activities
void oneDriveOnlineCallback() {
	// If we are in a --download-only method of operation, there is no filesystem monitoring, so no inotify events to check
	if (!appConfig.getValueBool("download_only")) {
		// Handle inotify events
		processInotifyEvents(true);
	}

	// Sync any online change down to the local disk
	// If we are doing --upload-only however .. we need to 'ignore' online change
	if (!appConfig.getValueBool("upload_only")) {
		// We are not doing an --upload-only scenario .. sync online change --> local
		syncEngineInstance.syncOneDriveAccountToLocalDisk();
	}
	if (appConfig.getValueBool("monitor")) {
		// Handle inotify events
		processInotifyEvents(true);
	}
}

// Perform only an upload of data when using --upload-only
void performUploadOnlySyncProcess(string localPath, Monitor filesystemMonitor = null) {
	// Perform the local database consistency check, picking up locally modified data and uploading this to OneDrive
	syncEngineInstance.performDatabaseConsistencyAndIntegrityCheck();
	if (appConfig.getValueBool("monitor")) {
		// Handle any inotify events whilst the DB was being scanned
		processInotifyEvents(true);
	}
	
	// Scan the configured 'sync_dir' for new data to upload
	syncEngineInstance.scanLocalFilesystemPathForNewData(localPath);
	if (appConfig.getValueBool("monitor")) {
		// Handle any new inotify events whilst the local filesystem was being scanned
		processInotifyEvents(true);
	}
}

// Perform the normal application sync process
void performStandardSyncProcess(string localPath, Monitor filesystemMonitor = null) {
	// If we are performing log suppression, output this message so the user knows what is happening
	if (appConfig.suppressLoggingOutput) {
		addLogEntry("Syncing changes from Microsoft OneDrive ...");
	}
	
	// Zero out these arrays
	syncEngineInstance.fileDownloadFailures = [];
	syncEngineInstance.fileUploadFailures = [];
	
	// Which way do we sync first?
	// OneDrive first then local changes (normal operational process that uses OneDrive as the source of truth)
	// Local First then OneDrive changes (alternate operation process to use local files as source of truth)
	if (appConfig.getValueBool("local_first")) {
		// Local data first 
		// Perform the local database consistency check, picking up locally modified data and uploading this to OneDrive
		syncEngineInstance.performDatabaseConsistencyAndIntegrityCheck();
		if (appConfig.getValueBool("monitor")) {
			// Handle any inotify events whilst the DB was being scanned
			processInotifyEvents(true);
		}
		
		// Scan the configured 'sync_dir' for new data to upload to OneDrive
		syncEngineInstance.scanLocalFilesystemPathForNewData(localPath);
		if (appConfig.getValueBool("monitor")) {
			// Handle any new inotify events whilst the local filesystem was being scanned
			processInotifyEvents(true);
		}
		
		// Download data from OneDrive last
		syncEngineInstance.syncOneDriveAccountToLocalDisk();
		if (appConfig.getValueBool("monitor")) {
			// Cancel out any inotify events from downloading data
			processInotifyEvents(false);
		}
		
		// At this point, we have done a sync from:
		// local  -> online
		// online -> local
		//
		// Everything now should be 'in sync' and the database correctly populated with data
		// If --resync was used, we need to unset this as sync.d performs certain queries depending on if 'resync' is set or not
		if (appConfig.getValueBool("resync")) {
			// unset 'resync' now that everything has been performed
			appConfig.setValueBool("resync" , false);
		}
	} else {
		// Normal sync process
		// Download data from OneDrive first
		syncEngineInstance.syncOneDriveAccountToLocalDisk();
		if (appConfig.getValueBool("monitor")) {
			// Cancel out any inotify events from downloading data
			processInotifyEvents(false);
		}
		
		// Perform the local database consistency check, picking up locally modified data and uploading this to OneDrive
		syncEngineInstance.performDatabaseConsistencyAndIntegrityCheck();
		if (appConfig.getValueBool("monitor")) {
			// Handle any inotify events whilst the DB was being scanned
			processInotifyEvents(true);
		}
			
		// Is --download-only NOT configured?
		if (!appConfig.getValueBool("download_only")) {
		
			// Scan the configured 'sync_dir' for new data to upload to OneDrive
			syncEngineInstance.scanLocalFilesystemPathForNewData(localPath);
			if (appConfig.getValueBool("monitor")) {
				// Handle any new inotify events whilst the local filesystem was being scanned
				processInotifyEvents(true);
			}
			
			// If we are not doing a 'force_children_scan' perform a true-up
			// 'force_children_scan' is used when using /children rather than /delta and it is not efficient to re-run this exact same process twice
			if (!appConfig.getValueBool("force_children_scan")) {
				// Perform the final true up scan to ensure we have correctly replicated the current online state locally
				if (!appConfig.suppressLoggingOutput) {
					addLogEntry("Performing a last examination of the most recent online data within Microsoft OneDrive to complete the reconciliation process");
				}
				// We pass in the 'appConfig.fullScanTrueUpRequired' value which then flags do we use the configured 'deltaLink'
				// If 'appConfig.fullScanTrueUpRequired' is true, we do not use the 'deltaLink' if we are in --monitor mode, thus forcing a full scan true up
				syncEngineInstance.syncOneDriveAccountToLocalDisk();
				if (appConfig.getValueBool("monitor")) {
					// Cancel out any inotify events from downloading data
					processInotifyEvents(false);
				}
			}
		}
		
		// At this point, we have done a sync from:
		// online -> local
		// local  -> online (if not doing --download-only)
		// online -> local (if not doing --download-only)
		//
		// Everything now should be 'in sync' and the database correctly populated with data
		// If --resync was used, we need to unset this as sync.d performs certain queries depending on if 'resync' is set or not
		if (appConfig.getValueBool("resync")) {
			// unset 'resync' now that everything has been performed
			appConfig.setValueBool("resync" , false);
		}
	}
}

// Process any inotify events
void processInotifyEvents(bool updateFlag) {
	// Attempt to process or cancel inotify events
	// filesystemMonitor.update will throw this, thus needs to be caught
	//   monitor.MonitorException@src/monitor.d(549): inotify queue overflow: some events may be lost (Interrupted system call)
	try {
		// Process any inotify events or cancel events based on flag value
		// True = process
		// False = cancel
		filesystemMonitor.update(updateFlag);
	} catch (MonitorException e) {
		// Catch any exceptions thrown by inotify / monitor engine
		addLogEntry("ERROR: The following inotify error was generated: " ~ e.msg);
	}
}

// Display the sync outcome
void displaySyncOutcome() {
	// Detail any download or upload transfer failures
	syncEngineInstance.displaySyncFailures();
	
	// Sync is either complete or partially complete
	if (!syncEngineInstance.syncFailures) {
		// No download or upload issues
		if (!appConfig.getValueBool("monitor")) addLogEntry(); // Add an additional line break so that this is clear when using --sync
		addLogEntry("Sync with Microsoft OneDrive is complete");
	} else {
		addLogEntry();
		addLogEntry("Sync with Microsoft OneDrive has completed, however there are items that failed to sync.");
		// Due to how the OneDrive API works 'changes' such as add new files online, rename files online, delete files online are only sent once when using the /delta API call.
		// That we failed to download it, we need to track that, and then issue a --resync to download any of these failed files .. unfortunate, but there is no easy way here
		if (!syncEngineInstance.fileDownloadFailures.empty) {
			addLogEntry("To fix any download failures you may need to perform a --resync to ensure this system is correctly synced with your Microsoft OneDrive Account");
		}
		if (!syncEngineInstance.fileUploadFailures.empty) {
			addLogEntry("To fix any upload failures you may need to perform a --resync to ensure this system is correctly synced with your Microsoft OneDrive Account");
		}
		// So that from a logging perspective these messages are clear, add a line break in
		addLogEntry();
	}
}

// Perform database file removal
void processResyncDatabaseRemoval(string databaseFilePathToRemove) {
	// Log what we are doing
	if (debugLogging) {addLogEntry("Testing if we have exclusive access to local database file", ["debug"]);}
	
	// Are we the only running instance? Test that we can open the database file path
	itemDB = new ItemDatabase(databaseFilePathToRemove);
	
	// did we successfully initialise the database class?
	if (!itemDB.isDatabaseInitialised()) {
		// no .. destroy class
		itemDB = null;
		// exit application - void function, force exit this way
		exit(EXIT_FAILURE);
	}
	
	// If we have exclusive access we will not have exited
	// destroy access test
	itemDB = null;
	// delete application sync state
	addLogEntry("Deleting the saved application sync status ...");
	if (!dryRun) {
		safeRemove(databaseFilePathToRemove);
	} else {
		// --dry-run scenario ... technically we should not be making any local file changes .......
		addLogEntry("DRY-RUN: Not removing the saved application sync status");
	}
}

// Clean up the local database files
void cleanupDatabaseFiles(string activeDatabaseFileName) {
	// Temp variables
	string databaseShmFile = activeDatabaseFileName ~ "-shm";
	string databaseWalFile = activeDatabaseFileName ~ "-wal";
	
	// Are we performing a --dry-run?
	if (dryRun) {
		// If the dry run database exists, clean this up
		if (exists(activeDatabaseFileName)) {
			// remove the dry run database file
			if (debugLogging) {addLogEntry("DRY-RUN: Removing items-dryrun.sqlite3 as it still exists for some reason", ["debug"]);}
			safeRemove(activeDatabaseFileName);
		}
	} else {
		// we may have not been using --dry-run, however we may have been running some operations that use a dry-run database, and this needs to be explicitly cleaned up
		if (exists(appConfig.databaseFilePathDryRun)) {
			if (debugLogging) {addLogEntry("Removing items-dryrun.sqlite3 as it still exists for some reason post being used for non-dryrun operations", ["debug"]);}
			safeRemove(appConfig.databaseFilePathDryRun);
		}
	}
	
	// Silent cleanup of -shm file if it exists
	if (exists(databaseShmFile)) {
		// Configure the log message
		string logMessage = "Removing " ~ baseName(databaseShmFile) ~ " as it still exists for some reason";
		// Is this a --dry-run scenario
		if (dryRun) {
			logMessage = "DRY-RUN: " ~ logMessage;
		}
	
		// Remove -shm file
		if (debugLogging) {addLogEntry(logMessage, ["debug"]);}
		safeRemove(databaseShmFile);
	}
	
	// Silent cleanup of wal files if it exists
	if (exists(databaseWalFile)) {
		// Configure the log message
		string logMessage = "Removing " ~ baseName(databaseWalFile) ~ " as it still exists for some reason";
		// Is this a --dry-run scenario
		if (dryRun) {
			logMessage = "DRY-RUN: " ~ logMessage;
		}
		
		// Remove -wal file
		if (debugLogging) {addLogEntry(logMessage, ["debug"]);}
		safeRemove(databaseWalFile);
	}
}

// Perform a check to see if this is a mount point, and if the 'mount' has gone
void checkForNoMountScenario() {
	// If this is a 'mounted' folder, the 'mount point' should have this file to help the application stop any action to preserve data because the drive to mount is not currently mounted
	if (appConfig.getValueBool("check_nomount")) {
		// we were asked to check the mount point for the presence of a '.nosync' file
		if (exists(".nosync")) {
			addLogEntry("ERROR: .nosync file found in directory mount point. Aborting application startup process to safeguard data.", ["info", "notify"]);
			// Perform the shutdown process
			performSynchronisedExitProcess("check_nomount");
			// Exit
			exit(EXIT_FAILURE);
		}
	}
}

// Setup a signal handler for catching SIGINT, SIGTERM and SIGSEGV (CTRL-C and others) during application execution
void setupSignalHandler() {
	sigaction_t action;
	action.sa_handler = &exitViaSignalHandler; // Direct function pointer assignment
	sigemptyset(&action.sa_mask); // Initialize the signal set to empty
	action.sa_flags = 0;
	sigaction(SIGINT, &action, null);  // Interrupt from keyboard
	sigaction(SIGTERM, &action, null); // Termination signal
	sigaction(SIGSEGV, &action, null); // Invalid Memory Access signal
}

// Catch SIGINT (CTRL-C), SIGTERM (kill) and SIGSEGV (invalid memory access), handle rapid repeat CTRL-C presses
extern(C) nothrow @nogc @system void exitViaSignalHandler(int signo) {

	// Update global exitHandlerTriggered flag so that objects that depend on this know we are shutting down
	exitHandlerTriggered = true;
	
	// Catch the generation of SIGSEV post SIGINT or SIGTERM event
    if (signo == SIGSEGV) {
		// Was SIGTERM used?
		if (!sigtermHandlerTriggered) {
			// No .. so most likely SIGINT (CTRL-C)
			printf("Due to a termination signal, internal processing stopped abruptly. The application will now exit in a unclean manner.\n");
			exit(130);
		} else {
			// High probability of being shutdown by systemd, for example: systemctl --user stop onedrive
			// Exit in a manner that does not trigger an exit failure in systemd
			exit(0);
		}
	}
	
	if (signo == SIGTERM) {
		// systemd will use SIGTERM to terminate a running process
		sigtermHandlerTriggered = true;
	}

	if (shutdownInProgress) {
		return;  // Ignore subsequent presses
	} else {
		// Disable logging suppression
		appConfig.suppressLoggingOutput = false;
		// Flag we are shutting down
		shutdownInProgress = true;
		
		try {
			assumeNoGC ( () {
				// Log that a termination signal was caught
				addLogEntry("\nReceived termination signal, attempting to cleanly shutdown application");
				// Try and shutdown in a safe and synchronised manner
				performSynchronisedExitProcess("SIGINT-SIGTERM-HANDLER");
			})();
		} catch (Exception e) {
			// Any output here will cause a GC allocation
			// - Error: `@nogc` function `main.exitHandler` cannot call non-@nogc function `std.stdio.writeln!string.writeln`
			// - Error: cannot use operator `~` in `@nogc` function `main.exitHandler`
			// writeln("Exception during shutdown: " ~ e.msg);
		}
		// Exit the process with the provided exit code
		exit(signo);
	}
}

// Handle application exit
void performSynchronisedExitProcess(string scopeCaller = null) {
	synchronized {
		// Perform cleanup and shutdown of various services and resources
		try {
			// Log who called this function
			if (debugLogging) {addLogEntry("performSynchronisedExitProcess called by: " ~ scopeCaller, ["debug"]);}
			// Remove Desktop integration
			if(performFileSystemMonitoring) {
				// Was desktop integration enabled?
				if (appConfig.getValueBool("display_manager_integration")) {
					// Attempt removal
					attemptFileManagerIntegrationRemoval();
				}
			}
			// Shutdown the OneDrive Webhook instance
			shutdownOneDriveWebhook();
			// Shutdown the OneDrive WebSocket instance
			shutdownOneDriveSocketIo();
			// Shutdown any local filesystem monitoring
			shutdownFilesystemMonitor();
			// Shutdown the sync engine
			if (scopeCaller == "SIGINT-SIGTERM-HANDLER") {
				// Wait for all parallel jobs that depend on the database being available to complete
				addLogEntry("Waiting for any existing upload|download process to complete");
			}
			shutdownSyncEngine();
			// Release all CurlEngine instances
			releaseAllCurlInstances();
			// Shutdown the client side filtering objects
			shutdownSelectiveSync();
			// Shutdown the database
			shutdownDatabase();
			// Shutdown the application configuration objects - nothing should be active now
			shutdownAppConfig();
			// Shutdown application logging
			shutdownApplicationLogging();
		} catch (Exception e) {
            addLogEntry("Error during performStandardExitProcess: " ~ e.toString(), ["error"]);
        }
	}
}

void shutdownOneDriveWebhook() {
    if (oneDriveWebhook !is null) {
		if (debugLogging) {addLogEntry("Shutting down OneDrive Webhook instance", ["debug"]);}
		oneDriveWebhook.stop();
        object.destroy(oneDriveWebhook);
        oneDriveWebhook = null;
		if (debugLogging) {addLogEntry("Shutdown of OneDrive Webhook instance is complete", ["debug"]);}
    }
}

void shutdownOneDriveSocketIo() {
    if (oneDriveSocketIo !is null) {
        if (debugLogging) addLogEntry("Shutting down OneDrive WebSocket instance", ["debug"]);
        oneDriveSocketIo.stop();
        object.destroy(oneDriveSocketIo);
        oneDriveSocketIo = null;
        if (debugLogging) addLogEntry("Shutdown of OneDrive WebSocket instance complete", ["debug"]);
    }
}

void shutdownFilesystemMonitor() {
    if (filesystemMonitor !is null) {
		if (debugLogging) {addLogEntry("Shutting down Filesystem Monitoring instance", ["debug"]);}
		filesystemMonitor.shutdown();
        object.destroy(filesystemMonitor);
        filesystemMonitor = null;
		if (debugLogging) {addLogEntry("Shutdown of Filesystem Monitoring instance is complete", ["debug"]);}
    }
}

void shutdownSelectiveSync() {
    if (selectiveSync !is null) {
		if (debugLogging) {addLogEntry("Shutting down Client Side Filtering instance", ["debug"]);}
		selectiveSync.shutdown();
        object.destroy(selectiveSync);
        selectiveSync = null;
		if (debugLogging) {addLogEntry("Shutdown of Client Side Filtering instance is complete", ["debug"]);}
    }
}

void shutdownSyncEngine() {
    if (syncEngineInstance !is null) {
		if (debugLogging) {addLogEntry("Shutting down Sync Engine instance", ["debug"]);}
		syncEngineInstance.shutdown(); // Make sure any running thread completes first
        object.destroy(syncEngineInstance);
        syncEngineInstance = null;
		if (debugLogging) {addLogEntry("Shutdown Sync Engine instance is complete", ["debug"]);}
    }
}

void shutdownDatabase() {
    if (itemDB !is null && itemDB.isDatabaseInitialised()) {
		if (debugLogging) {addLogEntry("Shutting down Database instance", ["debug"]);}
		
		// Write WAL and SHM data to file
		if (debugLogging) {addLogEntry("Merge contents of WAL and SHM files into main database file before shutting down database", ["debug"]);}
		itemDB.performCheckpoint("TRUNCATE");
		
		// Do we perform a database vacuum?
		if (performDatabaseVacuum) {
			// Logging to attempt this is denoted from performVacuum() - so no need to confirm here
			itemDB.performVacuum();
			// If this completes, it is denoted from performVacuum() - so no need to confirm here
		}
		
		 // Close the DB File Handle
		itemDB.closeDatabaseFile();
		object.destroy(itemDB);
		cleanupDatabaseFiles(runtimeDatabaseFile);
		itemDB = null;
		if (debugLogging) {addLogEntry("Shutdown of Database instance is complete", ["debug"]);}
    }
}

void shutdownAppConfig() {
    if (appConfig !is null) {
		if (debugLogging) {addLogEntry("Shutting down Application Configuration instance", ["debug"]);}
		object.destroy(appConfig);
        appConfig = null;
		if (debugLogging) {addLogEntry("Shutdown of Application Configuration instance is complete", ["debug"]);}
    }
}

void shutdownApplicationLogging() {
	// Log that we are exiting
	if (loggingStillInitialised()) {
		if (loggingActive()) {
			// join all threads
			thread_joinAll();
			if (debugLogging) {addLogEntry("Application is exiting", ["debug"]);}
			addLogEntry("#######################################################################################################################################", ["logFileOnly"]);
			// Destroy the shared logging buffer which flushes any remaining logs
			if (debugLogging) {addLogEntry("Shutting down Application Logging instance", ["debug"]);}
			// Allow any logging complete before we exit
			Thread.sleep(dur!("msecs")(500));
			// Shutdown Logging which also sets logBuffer to null
			shutdownLogging();
		}
	}
}

string compilerDetails() {
	version(DigitalMars) enum compiler = "DMD";
	else version(LDC)    enum compiler = "LDC";
	else version(GNU)    enum compiler = "GDC";
	else enum compiler = "Unknown compiler";
	string compilerString = compiler ~ " " ~ to!string(__VERSION__);
	return compilerString;
}

void attemptFileManagerIntegration() {
	// Are we running under a Desktop Manager (GNOME or KDE)?
	if (appConfig.isGuiSessionDetected()) {
		// Generate desktop hints
		auto hints = appConfig.detectDesktop();
		
		// GNOME Desktop File Manager integration
		if (hints.gnome) {
			// Attempt integration
			appConfig.addGnomeBookmark();
			appConfig.setOneDriveFolderIcon();
			return;
		}
		
		// KDE Desktop File Manager integration
		if (hints.kde) {
			// Attempt integration
			appConfig.addKDEPlacesEntry();
			return;
		}
	}
}

void attemptFileManagerIntegrationRemoval() {
	// Are we running under a Desktop Manager (GNOME or KDE)?
	if (appConfig.isGuiSessionDetected()) {
		// Generate desktop hints
		auto hints = appConfig.detectDesktop();
		
		// GNOME Desktop File Manager integration removal
		if (hints.gnome) {
			// Attempt integration removal
			appConfig.removeGnomeBookmark();
			appConfig.removeOneDriveFolderIcon();
			return;
		}
		
		// KDE Desktop File Manager integration removal
		if (hints.kde) {
			// Attempt integration removal
			appConfig.removeKDEPlacesEntry();
			return;
		}	
	}
}
