// What is this module called?
module config;

// What does this module require to function?
import core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit;
import std.array;
import std.stdio;
import std.process;
import std.regex;
import std.string;
import std.algorithm;
import std.algorithm.searching;
import std.algorithm.sorting;
import std.file;
import std.conv;
import std.path;
import std.getopt;
import std.format;
import std.ascii;
import std.datetime;
import std.exception;
import core.sys.posix.unistd : geteuid, getuid;
import std.process : spawnProcess, wait;

// What other modules that we have created do we need to import?
import log;
import util;

class ApplicationConfig {
	// Application default values - these do not change
	// - Compile time regex
	immutable auto configRegex = ctRegex!(`^(\w+)\s*=\s*"(.*)"\s*$`);
	// - Default directory to store data
	immutable string defaultSyncDir = "~/OneDrive";
	// - Default Directory Permissions
	immutable long defaultDirectoryPermissionMode = 700;
	// - Default File Permissions
	immutable long defaultFilePermissionMode = 600;
	// - Default types of files to skip
	// v2.0.x - 2.4.x: ~*|.~*|*.tmp
	// v2.5.x  		 : ~*|.~*|*.tmp|*.swp|*.partial
	immutable string defaultSkipFile = "~*|.~*|*.tmp|*.swp|*.partial";
	// - Default directories to skip (default is skip none)
	immutable string defaultSkipDir = "";
	// - Default application logging directory
	immutable string defaultLogFileDir = "/var/log/onedrive";
	// - Default configuration directory
	immutable string defaultConfigDirName = "~/.config/onedrive";
	// - Default 'OneDrive Business Shared Files' Folder Name
	immutable string defaultBusinessSharedFilesDirectoryName = "Files Shared With Me";
	// - Default file fragment size for uploads
	immutable long defaultFileFragmentSize = 10; // in MiB
	immutable long defaultMaxFileFragmentSize = 60; // in MiB
	immutable long defaultMonitorInterval = 300; // 5 minutes
	
	// Microsoft Requirements 
	// - Default Application ID (abraunegg)
	immutable string defaultApplicationId = "d50ca740-c83f-4d1b-b616-12c519384f0c";
		
	// - Microsoft User Agent ISV Tag
	immutable string isvTag = "ISV";
	// - Microsoft User Agent Company name
	immutable string companyName = "abraunegg";
	// - Microsoft Application name as per Microsoft Azure application registration
	immutable string appTitle = "OneDrive Client for Linux";
	// Comply with OneDrive traffic decoration requirements
	// https://docs.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online
	// - Identify as ISV and include Company Name, App Name separated by a pipe character and then adding Version number separated with a slash character
	immutable string defaultUserAgent = isvTag ~ "|" ~ companyName ~ "|" ~ appTitle ~ "/" ~ strip(import("version"));
	
	// HTTP Struct items, used for configuring HTTP()
	// Curl Timeout Handling
	// libcurl dns_cache_timeout timeout
	immutable int defaultDnsTimeout = 60; // in seconds
	// Connect timeout for HTTP|HTTPS connections
	// Controls CURLOPT_CONNECTTIMEOUT
	immutable int defaultConnectTimeout = 10; // in seconds
	// Default data timeout for HTTP operations
	// curl.d has a default of: _defaultDataTimeout = dur!"minutes"(2);
	immutable int defaultDataTimeout = 60; // in seconds
	// Maximum total time (in seconds) that any transfer operation is allowed to take.
	// This maps directly to libcurl's CURLOPT_TIMEOUT.
	//
	// IMPORTANT:
	//   • CURLOPT_TIMEOUT applies to the *entire* operation — DNS lookup, TCP connect,
	//     TLS negotiation, and the full data transfer.
	//   • If this timeout is reached, libcurl will abort the request even if data is
	//     flowing normally.
	//   • For large file downloads, especially on slower links, setting a non-zero
	//     timeout can cause the transfer to be killed prematurely.
	//
	// Behaviour:
	//   • A value of 0 disables the limit entirely (libcurl’s default behaviour).
	//   • It is strongly recommended to keep this at 0 unless a hard global cap is
	//     explicitly required by the user or their environment.
	immutable int defaultOperationTimeout = 0; // 0 = no timeout (safe for extremely large file downloads)
	// Specify what IP protocol version should be used when communicating with OneDrive
	immutable int defaultIpProtocol = 0; // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only
	// Specify how many redirects should be allowed
	immutable int defaultMaxRedirects = 5;
		
	// Azure Active Directory & Graph Explorer Endpoints
	// - Global & Default
	immutable string globalAuthEndpoint = "https://login.microsoftonline.com";
	immutable string globalGraphEndpoint = "https://graph.microsoft.com";
	// - US Government L4
	immutable string usl4AuthEndpoint = "https://login.microsoftonline.us";
	immutable string usl4GraphEndpoint = "https://graph.microsoft.us";
	// - US Government L5
	immutable string usl5AuthEndpoint = "https://login.microsoftonline.us";
	immutable string usl5GraphEndpoint = "https://dod-graph.microsoft.us";
	// - Germany
	immutable string deAuthEndpoint = "https://login.microsoftonline.de";
	immutable string deGraphEndpoint = "https://graph.microsoft.de";
	// - China
	immutable string cnAuthEndpoint = "https://login.chinacloudapi.cn";
	immutable string cnGraphEndpoint = "https://microsoftgraph.chinacloudapi.cn";
	
	// Application Version
	immutable string applicationVersion = "onedrive " ~ strip(import("version"));
	
	// Application items that depend on application run-time environment, thus cannot be immutable
	// Public variables
	
	// Logging verbosity count
	long verbosityCount = 0;
	
	// Was the application just authorised - paste of response URI
	bool applicationAuthoriseResponseURIReceived = false;
	
	// Store the refreshToken for use within the application
	const(char)[] refreshToken;
	// Store the current accessToken for use within the application
	const(char)[] accessToken;
	// Store the 'refresh_token' file path
	string refreshTokenFilePath = "";
	// Store the accessTokenExpiration for use within the application
	SysTime accessTokenExpiration;
	// Store the 'session_upload.UNIQUE_STRING' file path
	string uploadSessionFilePath = "";
	// Store the 'resume_download.UNIQUE_STRING' file path
	string resumeDownloadFilePath = "";
	// Store the Intune account information
	string intuneAccountDetails;
	// Store the Intune account information on disk for reuse
	string intuneAccountDetailsFilePath = "";
	
	// API initialisation flags
	bool apiWasInitialised = false;
	bool syncEngineWasInitialised = false;
	
	// Important Account Details
	string accountType;
	string defaultDriveId;
	string defaultRootId;
		
	// Sync Operations
	bool fullScanTrueUpRequired = false;
	bool suppressLoggingOutput = false;
	
	// WebSocket Operations
	bool curlSupportsWebSockets = false;
	bool websocketSupportCheckDone = false;
	bool websocketNotificationUrlAvailable = false;
	string websocketEndpointResponse;
	string websocketNotificationUrl;
	string websocketUrlExpiry;
	
	// Default number of concurrent threads when downloading and uploading data
	ulong defaultConcurrentThreads = 8;
	
	// Default number of seconds inotify actions will be delayed by
	ulong defaultInotifyDelay = 5;
		
	// All application run-time paths are formulated from this as a set of defaults
	// - What is the home path of the actual 'user' that is running the application
	string defaultHomePath = "";
	// - What is the config path for the application. By default, this is ~/.config/onedrive but can be overridden by using --confdir
	string configDirName = defaultConfigDirName;
	// - In case we have to use a system config directory such as '/etc/onedrive' or similar, store that path in this variable
	private string systemConfigDirName = "";
	// - Store the configured converted octal value for directory permissions
	private int configuredDirectoryPermissionMode;
	// - Store the configured converted octal value for file permissions
	private int configuredFilePermissionMode;
	// - Store the 'delta_link' file path
	private string deltaLinkFilePath = "";
	// - Store the 'items.sqlite3' file path
	string databaseFilePath = "";
	// - Store the 'items-dryrun.sqlite3' file path
	string databaseFilePathDryRun = "";
	// - Store the user 'config' file path
	private string userConfigFilePath = "";
	// - Store the system 'config' file path
	private string systemConfigFilePath = "";
	// - What is the 'config' file path that will be used?
	private string applicableConfigFilePath = "";
	// - Store the 'sync_list' file path
	string syncListFilePath = "";
	
	// OneDrive Business Shared File handling - what directory will be used?
	string configuredBusinessSharedFilesDirectoryName = "";

	// Hash files so that we can detect when the configuration has changed, in items that will require a --resync
	private string configHashFile = "";
	private string configBackupFile = "";
	private string syncListHashFile = "";
	
	// Store the actual 'runtime' hash
	private string currentConfigHash = "";
	private string currentSyncListHash = "";
	
	// Store the previous config files hash values (file contents)
	private string previousConfigHash = "";
	private string previousSyncListHash = "";
		
	// Store items that come in from the 'config' file, otherwise these need to be set the defaults
	private string configFileSyncDir = defaultSyncDir;
	private string configFileSkipFile = ""; // Default for now, if post reading in any user configuration, if still empty, default will be used
	private bool configFileSkipFileReadIn = false; // If we actually read in something from 'config' file, this gets set to true
	private string configFileSkipDir = ""; // Default here is no directories are skipped
	private string configFileDriveId = ""; // Default here is that no drive id is specified
	private bool configFileCheckNoSync = false;
	private bool configFileSkipDotfiles = false;
	private bool configFileSkipSymbolicLinks = false;
	private bool configFileSkipSize = false;
	private bool configFileSyncBusinessSharedItems = false;
	
	// File permission values (set via initialise function)
	private int convertedPermissionValue;
	
	// Array of values that are the actual application runtime configuration
	// The values stored in these array's are the actual application configuration which can then be accessed by getValue & setValue
	string[string] stringValues;
	long[string] longValues;
	bool[string] boolValues;
	bool shellEnvironmentSet = false;
	
	// GUI Notification Environment variables
	bool xdg_exists = false;
	bool dbus_exists = false;
	
	// Recycle Bin Configuration
	// These paths are used by the application, if 'use_recycle_bin' is enabled
	string recycleBinParentPath;
	string recycleBinFilePath;
	string recycleBinInfoPath;
	
	// Runtime 'sync_dir' as initialised
	string runtimeSyncDirectory;
		
	// Initialise the application configuration
	bool initialise(string confdirOption, bool helpRequested) {
		
		// Default runtime configuration - entries in config file ~/.config/onedrive/config or derived from variables above
		// An entry here means it can be set via the config file if there is a corresponding entry, read from config and set via update_from_args()
		// The below becomes the 'default' application configuration before config file and/or cli options are overlaid on top
		
		// - Set the required default values
		stringValues["application_id"] = defaultApplicationId;
		stringValues["log_dir"] = defaultLogFileDir;
		stringValues["skip_dir"] = defaultSkipDir;
		stringValues["skip_file"] = defaultSkipFile;
		stringValues["sync_dir"] = defaultSyncDir;
		stringValues["user_agent"] = defaultUserAgent;
		// - The 'drive_id' is used when we specify a specific OneDrive ID when attempting to sync Shared Folders and SharePoint items
		stringValues["drive_id"] = "";
		// Support National Azure AD endpoints as per https://docs.microsoft.com/en-us/graph/deployments
		// By default, if empty, use standard Azure AD URL's
		// Will support the following options:
		// - USL4
		//     AD Endpoint:    https://login.microsoftonline.us
		//     Graph Endpoint: https://graph.microsoft.us
		// - USL5
		//     AD Endpoint:    https://login.microsoftonline.us
		//     Graph Endpoint: https://dod-graph.microsoft.us
		// - DE
		//     AD Endpoint:    https://portal.microsoftazure.de
		//     Graph Endpoint: 	https://graph.microsoft.de
		// - CN
		//     AD Endpoint:    https://login.chinacloudapi.cn
		//     Graph Endpoint: 	https://microsoftgraph.chinacloudapi.cn
		stringValues["azure_ad_endpoint"] = "";
		// Support single-tenant applications that are not able to use the "common" multiplexer
		stringValues["azure_tenant_id"] = "";
		
		// Support synchronising files based on user desire
		// - default = whatever order these came in as, processed essentially FIFO
		// - size_asc = file size ascending
		// - size_dsc = file size descending
		// - name_asc = file name ascending
		// - name_dsc = file name descending
		stringValues["transfer_order"] = "default";
				
		// Recycle Bin Configuration
		// Enable|Disable feature
		boolValues["use_recycle_bin"] = false;
		// Recycle Bin Folder - empty string as a default
		stringValues["recycle_bin_path"] = "";
		
		// - Store how many times was --verbose added
		longValues["verbose"] = verbosityCount; 
		// - The amount of time (seconds) between monitor sync loops
		longValues["monitor_interval"] = defaultMonitorInterval;
		// - What size of file should be skipped?
		longValues["skip_size"] = 0;
		// - How many 'loops' when using --monitor, before we print out high frequency recurring items?
		longValues["monitor_log_frequency"] = 12;
		// - Number of N sync runs before performing a full local scan of sync_dir
		//   By default 12 which means every ~60 minutes a full disk scan of sync_dir will occur 
		//   'monitor_interval' * 'monitor_fullscan_frequency' = 3600 = 1 hour
		longValues["monitor_fullscan_frequency"] = 12;
		// - Number of children in a path that is locally removed which will be classified as a 'big data delete'
		longValues["classify_as_big_delete"] = 1000;
		// - Configure the default folder permission attributes for newly created folders
		longValues["sync_dir_permissions"] = defaultDirectoryPermissionMode;
		// - Configure the default file permission attributes for newly created file
		longValues["sync_file_permissions"] = defaultFilePermissionMode;
		// - Configure download / upload rate limits
		longValues["rate_limit"] = 0;
		// - To ensure we do not fill up the load disk, how much disk space should be reserved by default
		longValues["space_reservation"] = 50 * 2^^20; // 50 MB as Bytes
		// - How large should our file fragments be when uploading as an 'upload session' ?
		longValues["file_fragment_size"] = defaultFileFragmentSize; // whole number, treated as MB, will be converted to bytes within performSessionFileUpload(). Default is 10.
		
		// HTTPS & CURL Operation Settings
		// - Maximum time an operation is allowed to take
		//   This includes dns resolution, connecting, data transfer, etc - controls CURLOPT_TIMEOUT
		// CURLOPT_TIMEOUT: This option sets the maximum time in seconds that you allow the libcurl transfer operation to take. 
		// This is useful for controlling how long a specific transfer should take before it is considered too slow and aborted. However, it does not directly control the keep-alive time of a socket.
		longValues["operation_timeout"] = defaultOperationTimeout;
		// libcurl dns_cache_timeout timeout
		longValues["dns_timeout"] = defaultDnsTimeout;
		// Timeout for HTTPS connections - controls CURLOPT_CONNECTTIMEOUT
		// CURLOPT_CONNECTTIMEOUT: This option sets the timeout, in seconds, for the connection phase. It is the maximum time allowed for the connection to be established.
		longValues["connect_timeout"] = defaultConnectTimeout;
		// Timeout for activity on a HTTPS connection
		longValues["data_timeout"] = defaultDataTimeout;
		// What IP protocol version should be used when communicating with OneDrive
		longValues["ip_protocol_version"] = defaultIpProtocol; // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only
		// What is the default age that a curl engine should be left idle for, before being destroyed
		longValues["max_curl_idle"] = 120;
		
		// Number of concurrent threads
		longValues["threads"] = defaultConcurrentThreads; // Default is 8, user can increase to max of 16 or decrease
		
		// Do we wish to upload only?
		boolValues["upload_only"] = false;
		// Do we need to check for the .nomount file on the mount point?
		boolValues["check_nomount"] = false;
		// Do we need to check for the .nosync file anywhere?
		boolValues["check_nosync"] = false;
		// Do we wish to download only?
		boolValues["download_only"] = false;
		// Do we disable notifications?
		boolValues["disable_notifications"] = false;
		// Do we bypass all the download validation? 
		// - This is critically important not to disable, but because of SharePoint 'feature' can be highly desirable to enable
		boolValues["disable_download_validation"] = false;
		// Do we bypass all the upload validation? 
		// - This is critically important not to disable, but because of SharePoint 'feature' can be highly desirable to enable
		boolValues["disable_upload_validation"] = false;
		// Do we enable logging?
		boolValues["enable_logging"] = false;
		// Do we force HTTP 1.1 for connections to the OneDrive API
		// - By default we use the curl library default, which should be HTTP2 for most operations governed by the OneDrive API
		boolValues["force_http_11"] = false;
		// Do we treat the local file system as the source of truth for our data?
		boolValues["local_first"] = false;
		// Do we ignore local file deletes, so that all files are retained online?
		boolValues["no_remote_delete"] = false;
		// Do we skip symbolic links?
		boolValues["skip_symlinks"] = false;
		// Do we enable debugging for all HTTPS flows. Critically important for debugging API issues.
		boolValues["debug_https"] = false;
		// Do we skip .files and .folders?
		boolValues["skip_dotfiles"] = false;
		// Do we perform a 'dry-run' with no local or remote changes actually being performed?
		boolValues["dry_run"] = false;
		// Do we sync all the files in the 'sync_dir' root?
		boolValues["sync_root_files"] = false;
		// Do we delete source file after successful transfer?
		boolValues["remove_source_files"] = false;
		// Do we delete source folders after successful transfer?
		boolValues["remove_source_folders"] = false;
		// Do we perform strict matching for skip_dir?
		boolValues["skip_dir_strict_match"] = false;
		// Do we perform a --resync?
		boolValues["resync"] = false;
		// 'resync' now needs to be acknowledged based on the 'risk' of using it
		boolValues["resync_auth"] = false;
		// Ignore data safety checks and overwrite local data rather than preserve & rename
		// - This is a config file option ONLY
		boolValues["bypass_data_preservation"] = false;
		// Allow enable / disable of the syncing of OneDrive Business Shared items (files & folders) via configuration file
		boolValues["sync_business_shared_items"] = false;
		// Log to application output running configuration values
		boolValues["display_running_config"] = false;
		// Configure read-only authentication scope
		boolValues["read_only_auth_scope"] = false;
		// Flag to cleanup local files when using --download-only
		boolValues["cleanup_local_files"] = false;
		// Perform a permanentDelete on deletion activities
		boolValues["permanent_delete"] = false;
		
		// Controls how the application handles the Microsoft SharePoint 'feature' of modifying all PDF, MS Office & HTML files with added XML content post upload
		// - There are 2 ways to solve this:
		//     1. Download the modified file immediately after upload as per v2.4.x (default)
		//     2. Create a new online version of the file, which then contributes to the users 'quota'
		boolValues["create_new_file_version"] = false;
		
		// Some Linux editors (vi|vim|nvim|emacs|LibreOffice) use use a safe file-save strategy designed to avoid data corruption. As such, as part of this Process
		// they 'track' the last modified timestamp of the 'new' file that they create on file save (regardless of new file, modified file)
		// If *any* other application in the background then 'updates' this timestamp, these Linux editors complain saying that the file has changed:
		//
		// 		WARNING: The file has been changed since reading it!!!
		//		Do you really want to write to it (y/n)?
		//
		// This is simply because they are looking at the timestamp and *not* if the content has actually changed .... a poor design on those editors
		//
		// This option, when enabled, forces the client to use a 'session' upload, which, when the 'file' is uploaded by the session, this includes the local timestamp of the file
		// and Microsoft OneDrive should be respecting this timestamp as the timestamp to use|set when storing that file online
		boolValues["force_session_upload"] = false;
		
		// 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.
		// This flag tells the 'onedrive' inotify monitor to 'sleep' for this period of time, so that constant system writes are not creating instant data uploads
		boolValues["delay_inotify_processing"] = false;
		longValues["inotify_delay"] = defaultInotifyDelay; // default of 5 seconds
		
		// Webhook Feature Options
		boolValues["webhook_enabled"] = false;
		stringValues["webhook_public_url"] = "";
		stringValues["webhook_listening_host"] = "";
		longValues["webhook_listening_port"] = 8888;
		longValues["webhook_expiration_interval"] = 600;
		longValues["webhook_renewal_interval"] = 300;
		longValues["webhook_retry_interval"] = 60;
		
		// WebSocket Feature Options
		boolValues["disable_websocket_support"] = false;
		
		// GUI File Transfer and Deletion Notifications
		boolValues["notify_file_actions"] = false;
		
		// Display file transfer metrics
		// - Enable the calculation of transfer metrics (duration,speed) for the transfer of a file
		boolValues["display_transfer_metrics"] = false;
		
		// Enable writing extended attributes about a file to xattr values
		// - file creator
		// - file last modifier
		boolValues["write_xattr_data"] = false;

		// Diable setting the permissions for directories and files, using the inherited permissions
		boolValues["disable_permission_set"] = false;
		
		// Use authentication via Intune SSO via Microsoft Identity Broker (microsoft-identity-broker) dbus session
		boolValues["use_intune_sso"] = false;
		
		// Use authentication via OAuth2 Device Authorisation Flow
		boolValues["use_device_auth"] = false;
		
		// GUI | Display Manager Integration
		boolValues["display_manager_integration"] = false;
		
		// Disable GitHub Version check
		boolValues["disable_version_check"] = false;
				
		// EXPAND USERS HOME DIRECTORY
		// Determine the users home directory.
		// Need to avoid using ~ here as expandTilde() below does not interpret correctly when running under init.d or systemd scripts
		// Check for HOME environment variable
		if (environment.get("HOME") != ""){
			// Use HOME environment variable
			if (debugLogging) {addLogEntry("runtime_environment: HOME environment variable detected, expansion of '~' should be possible", ["debug"]);}
			defaultHomePath = environment.get("HOME");
			shellEnvironmentSet = true;
		} else {
			if ((environment.get("SHELL") == "") && (environment.get("USER") == "")){
				// No shell is set or username - observed case when running as systemd service under CentOS 7.x
				if (debugLogging) {addLogEntry("runtime_environment: No HOME, SHELL or USER environment variable configuration detected. Expansion of '~' not possible", ["debug"]);}
				defaultHomePath = "/root";
				shellEnvironmentSet = false;
			} else {
				// A shell & valid user is set, but no HOME is set, use ~ which can be expanded
				if (debugLogging) {addLogEntry("runtime_environment: SHELL and USER environment variable detected, expansion of '~' should be possible", ["debug"]);}
				defaultHomePath = "~";
				shellEnvironmentSet = true;
			}
		}
		
		// Outcome of setting 'defaultHomePath'
		if (debugLogging) {addLogEntry("runtime_environment: Calculated defaultHomePath: " ~ defaultHomePath, ["debug"]);}
		
		// Configure the default path for the Recycle Bin
		// Both GNOME and KDE use '~/.local/share/Trash/' as the default path
		// ~/.local/share/Trash/
		// ├── files/   # The actual trashed files
		// └── info/    # .trashinfo metadata about each file (original path, deletion date)
		setValueString("recycle_bin_path", defaultHomePath ~ "/.local/share/Trash/");
		recycleBinParentPath = getValueString("recycle_bin_path");
		
		// DEVELOPER OPTIONS
		// display_memory = true | false
		//  - It may be desirable to display the memory usage of the application to assist with diagnosing memory issues with the application
		//  - This is especially beneficial when debugging or performing memory tests with Valgrind
		boolValues["display_memory"] = false;
		// monitor_max_loop = long value
		//  - It may be desirable to, when running in monitor mode, force monitor mode to 'quit' after X number of loops
		//  - This is especially beneficial when debugging or performing memory tests with Valgrind
		longValues["monitor_max_loop"] = 0;
		// display_sync_options = true | false
		// - It may be desirable to see what options are being passed into performSync() without enabling the full verbose debug logging
		boolValues["display_sync_options"] = false;
		// force_children_scan = true | false
		// - Force client to use /children rather than /delta to query changes on OneDrive
		// - This option flags nationalCloudDeployment as true, forcing the client to act like it is using a National Cloud Deployment model
		boolValues["force_children_scan"] = false;
		// display_processing_time = true | false
		// - Enabling this option will add function processing times to the console output
		// - This then enables tracking of where the application is spending most amount of time when processing data when users have questions re performance
		boolValues["display_processing_time"] = false;
		
		// Function variables
		string configDirBase;
		string systemConfigDirBase = "/etc";
		bool configurationInitialised = false;
		
		// Initialise the application configuration, using the provided --confdir option was passed in
		if (!confdirOption.empty) {
			// A CLI 'confdir' was passed in
			// Clean up any stray " .. these should not be there for correct process handling of the configuration option
			confdirOption = strip(confdirOption,"\"");
			if (debugLogging) {addLogEntry("configDirName: CLI override to set configDirName to: " ~ confdirOption, ["debug"]);}
			
			// For the passed in --confdir option ..
			if (canFind(confdirOption,"~")) {
				// A ~ was found
				if (debugLogging) {addLogEntry("configDirName: A '~' was found in configDirName, using the calculated 'defaultHomePath' to replace '~'", ["debug"]);}
				configDirName = defaultHomePath ~ strip(confdirOption,"~","~");
			} else {
				configDirName = confdirOption;
			}
		} else {
			// Determine the base directory relative to which user specific configuration files should be stored
			if (environment.get("XDG_CONFIG_HOME") != ""){
				if (debugLogging) {addLogEntry("configDirBase: XDG_CONFIG_HOME environment variable set", ["debug"]);}
				configDirBase = environment.get("XDG_CONFIG_HOME");
			} else {
				// XDG_CONFIG_HOME does not exist on systems where X11 is not present - ie - headless systems / servers
				if (debugLogging) {addLogEntry("configDirBase: WARNING - no XDG_CONFIG_HOME environment variable set", ["debug"]);}
				configDirBase = buildNormalizedPath(buildPath(defaultHomePath, ".config"));
			}
			
			// Output configDirBase calculation
			if (debugLogging) {
				addLogEntry("configDirBase: " ~ configDirBase, ["debug"]);
				// Set the calculated application configuration directory
				addLogEntry("configDirName: Configuring application to use calculated config path", ["debug"]);
			}
			// configDirBase contains the correct path so we do not need to check for presence of '~'
			configDirName = buildNormalizedPath(buildPath(configDirBase, "onedrive"));
		}
		
		// systemConfigDirBase contains the correct path, build the correct path for the system config file
		systemConfigDirName = buildNormalizedPath(buildPath(systemConfigDirBase, "onedrive"));
		
		// Configuration directory should now have been correctly identified
		if (!exists(configDirName)) {
			// Attempt path creation
			try {
				// create the configuration directory
				mkdirRecurse(configDirName);
				// Configure the applicable permissions for the folder
				configDirName.setAttributes(returnRequiredDirectoryPermissions());
			} catch (std.file.FileException e) {
				// Creating the configuration directory failed
				addLogEntry("ERROR: Unable to create the required application configuration directory: " ~ e.msg, ["info", "notify"]);
				// Use exit scopes to shutdown API
				return EXIT_FAILURE;
			}
		} else {
			// The config path exists
			// The path that exists must be a directory, not a file
			if (!isDir(configDirName)) {
				if (!confdirOption.empty) {
					// the configuration path was passed in by the user .. user error
					addLogEntry("ERROR: --confdir entered value is an existing file instead of an existing directory");
				} else {
					// other error
					addLogEntry("ERROR: " ~ confdirOption ~ " is a file rather than a directory");
				}
				// Must exit
				exit(EXIT_FAILURE);	
			}
		}
		
		// Update application set variables based on configDirName
		// - What is the full path for the 'refresh_token'
		refreshTokenFilePath = buildNormalizedPath(buildPath(configDirName, "refresh_token"));
		// - What is the full path for the 'intune_account'
		intuneAccountDetailsFilePath = buildNormalizedPath(buildPath(configDirName, "intune_account"));
		// - What is the full path for the 'delta_link'
		deltaLinkFilePath = buildNormalizedPath(buildPath(configDirName, "delta_link"));
		// - What is the full path for the 'items.sqlite3' - the database cache file
		databaseFilePath = buildNormalizedPath(buildPath(configDirName, "items.sqlite3"));
		// - What is the full path for the 'items-dryrun.sqlite3' - the dry-run database cache file
		databaseFilePathDryRun = buildNormalizedPath(buildPath(configDirName, "items-dryrun.sqlite3"));
		// - What is the full path for the 'resume_upload'
		uploadSessionFilePath = buildNormalizedPath(buildPath(configDirName, "session_upload"));
		// - What is the full path for the resume 'resume_download' file
		resumeDownloadFilePath = buildNormalizedPath(buildPath(configDirName, "resume_download"));
		// - What is the full path for the 'sync_list' file
		syncListFilePath = buildNormalizedPath(buildPath(configDirName, "sync_list"));
		// - What is the full path for the 'config' - the user file to configure the application
		userConfigFilePath = buildNormalizedPath(buildPath(configDirName, "config"));
		// - What is the full path for the system 'config' file if it is required
		systemConfigFilePath = buildNormalizedPath(buildPath(systemConfigDirName, "config"));
		
		// To determine if any configuration items has changed, where a --resync would be required, we need to have a hash file for the following items
		// - 'config.backup' file
		// - applicable 'config' file
		// - 'sync_list' file
		// - 'business_shared_items' file
		configBackupFile = buildNormalizedPath(buildPath(configDirName, ".config.backup"));
		configHashFile = buildNormalizedPath(buildPath(configDirName, ".config.hash"));
		syncListHashFile = buildNormalizedPath(buildPath(configDirName, ".sync_list.hash"));
						
		// Debug Output for application set variables based on configDirName
		if (debugLogging) {
			addLogEntry("refreshTokenFilePath =         " ~ refreshTokenFilePath, ["debug"]);
			addLogEntry("intuneAccountDetailsFilePath = " ~ intuneAccountDetailsFilePath, ["debug"]);
			addLogEntry("deltaLinkFilePath =            " ~ deltaLinkFilePath, ["debug"]);
			addLogEntry("databaseFilePath =             " ~ databaseFilePath, ["debug"]);
			addLogEntry("databaseFilePathDryRun =       " ~ databaseFilePathDryRun, ["debug"]);
			addLogEntry("uploadSessionFilePath =        " ~ uploadSessionFilePath, ["debug"]);
			addLogEntry("userConfigFilePath =           " ~ userConfigFilePath, ["debug"]);
			addLogEntry("syncListFilePath =             " ~ syncListFilePath, ["debug"]);
			addLogEntry("systemConfigFilePath =         " ~ systemConfigFilePath, ["debug"]);
			addLogEntry("configBackupFile =             " ~ configBackupFile, ["debug"]);
			addLogEntry("configHashFile =               " ~ configHashFile, ["debug"]);
			addLogEntry("syncListHashFile =             " ~ syncListHashFile, ["debug"]);
		}
		
		// Configure the Hash and Backup File Permission Value
		string valueToConvert = to!string(defaultFilePermissionMode);
		auto convertedValue = parse!long(valueToConvert, 8);
		convertedPermissionValue = to!int(convertedValue);
		
		// Do not try and load any user configuration file if --help was used
		if (helpRequested) {
			return true;
		} else {
			// Initialise the application using the configuration file if it exists
			if (!exists(userConfigFilePath)) {
				// 'user' configuration file does not exist .. but did the user specify a custom configuration directory via --confdir ?
				if (confdirOption.empty) {
					// No --confdir entry
					// Is there a system configuration file?
					if (!exists(systemConfigFilePath)) {
						// 'system' configuration file does not exist
						if (verboseLogging) {addLogEntry("No user or system config file found, using application defaults", ["verbose"]);}
						applicableConfigFilePath = userConfigFilePath;
						configurationInitialised = true;
					} else {
						// 'system' configuration file exists
						// can we load the configuration file without error?
						if (loadConfigFile(systemConfigFilePath)) {
							// configuration file loaded without error
							addLogEntry("System configuration file successfully loaded");
							
							// Set 'applicableConfigFilePath' to equal the 'config' we loaded
							applicableConfigFilePath = systemConfigFilePath;
							// Update the configHashFile path value to ensure we are using the system 'config' file for the hash
							configHashFile = buildNormalizedPath(buildPath(systemConfigDirName, ".config.hash"));
							configurationInitialised = true;
						} else {
							// there was a problem loading the configuration file
							addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering
							addLogEntry("System configuration file has errors - please check your configuration");
						}
					}
				} else {
					// Set 'applicableConfigFilePath' to equal the 'config' path specified via --confdir
					applicableConfigFilePath = userConfigFilePath;
					configurationInitialised = true;
				}
			} else {
				// 'user' configuration file exists in the specified path
				// can we load the configuration file without error?
				if (loadConfigFile(userConfigFilePath)) {
					// configuration file loaded without error
					addLogEntry("Configuration file successfully loaded");
					
					// Set 'applicableConfigFilePath' to equal the 'config' we loaded
					applicableConfigFilePath = userConfigFilePath;
					configurationInitialised = true;
				} else {
					// there was a problem loading the configuration file
					addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering
					addLogEntry("Configuration file has errors - please check your configuration");
				}
			}
			
			// Advise the user path that we will use for the application state data
			if (canFind(applicableConfigFilePath, configDirName)) {
				if (verboseLogging) {addLogEntry("Using 'user' configuration path for application config and state data: " ~ configDirName, ["verbose"]);}
			} else {				
				if (canFind(applicableConfigFilePath, systemConfigDirName)) {
					if (verboseLogging) {
						addLogEntry("Using 'system' configuration path for application config data: " ~ systemConfigDirName, ["verbose"]);
						addLogEntry("Using 'user' configuration path for application state data:    " ~ configDirName, ["verbose"]);
					}
				}
			}
		}
		
		// return if the configuration was initialised
		return configurationInitialised;
	}
	
	// Create a backup of the 'config' file if it does not exist
	void createBackupConfigFile() {
		// Set this function name
		string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({})));
	
		if (!getValueBool("dry_run")) {
			// Is there a backup of the config file if the config file exists?
			if (exists(applicableConfigFilePath)) {
				if (debugLogging) {addLogEntry("Creating a backup of the applicable config file", ["debug"]);}
				// create backup copy of current config file
				try {
					std.file.copy(applicableConfigFilePath, configBackupFile);
					// File Copy should only be readable by the user who created it - 0600 permissions needed
					configBackupFile.setAttributes(convertedPermissionValue);
				} catch (FileException e) {
					// filesystem error
					displayFileSystemErrorMessage(e.msg, thisFunctionName, configBackupFile, FsErrorSeverity.warning);
				}
			}
		} else {
			// --dry-run scenario ... technically we should not be making any local file changes .......
			addLogEntry("DRY-RUN: Not creating backup config file as --dry-run has been used");
		}
	}
	
	// Return a given string value based on the provided key
	string getValueString(string key) {
		auto p = key in stringValues;
		if (p) {
			return *p;
		} else {
			throw new Exception("Missing config value: " ~ key);
		}
	}

	// Return a given long value based on the provided key
	long getValueLong(string key) {
		auto p = key in longValues;
		if (p) {
			return *p;
		} else {
			throw new Exception("Missing config value: " ~ key);
		}
	}

	// Return a given bool value based on the provided key
	bool getValueBool(string key) {
		auto p = key in boolValues;
		if (p) {
			return *p;
		} else {
			throw new Exception("Missing config value: " ~ key);
		}
	}
	
	// Set a given string value based on the provided key
	void setValueString(string key, string value) {
		stringValues[key] = value;
	}

	// Set a given long value based on the provided key
	void setValueLong(string key, long value) {
		longValues[key] = value;
	}

	// Set a given long value based on the provided key
	void setValueBool(string key, bool value) {
		boolValues[key] = value;
	}
	
	// Configure the directory octal permission value
	void configureRequiredDirectoryPermissions() {
		// return the directory permission mode required
		// - return octal!defaultDirectoryPermissionMode; ... cant be used .. which is odd
		// Error: variable defaultDirectoryPermissionMode cannot be read at compile time
		if (getValueLong("sync_dir_permissions") != defaultDirectoryPermissionMode) {
			// return user configured permissions as octal integer
			string valueToConvert = to!string(getValueLong("sync_dir_permissions"));
			auto convertedValue = parse!long(valueToConvert, 8);
			configuredDirectoryPermissionMode = to!int(convertedValue);
		} else {
			// return default as octal integer
			string valueToConvert = to!string(defaultDirectoryPermissionMode);
			auto convertedValue = parse!long(valueToConvert, 8);
			configuredDirectoryPermissionMode = to!int(convertedValue);
		}
	}

	// Configure the file octal permission value
	void configureRequiredFilePermissions() {
		// return the file permission mode required
		// - return octal!defaultFilePermissionMode; ... cant be used .. which is odd
		// Error: variable defaultFilePermissionMode cannot be read at compile time
		if (getValueLong("sync_file_permissions") != defaultFilePermissionMode) {
			// return user configured permissions as octal integer
			string valueToConvert = to!string(getValueLong("sync_file_permissions"));
			auto convertedValue = parse!long(valueToConvert, 8);
			configuredFilePermissionMode = to!int(convertedValue);
		} else {
			// return default as octal integer
			string valueToConvert = to!string(defaultFilePermissionMode);
			auto convertedValue = parse!long(valueToConvert, 8);
			configuredFilePermissionMode = to!int(convertedValue);
		}
	}

	// Read the configuredDirectoryPermissionMode and return
	int returnRequiredDirectoryPermissions() {
		if (configuredDirectoryPermissionMode == 0) {
			// the configured value is zero, this means that directories would get
			// values of d---------
			configureRequiredDirectoryPermissions();
		}
		return configuredDirectoryPermissionMode;
	}

	// Read the configuredFilePermissionMode and return
	int returnRequiredFilePermissions() {
		if (configuredFilePermissionMode == 0) {
			// the configured value is zero
			configureRequiredFilePermissions();
		}
		return configuredFilePermissionMode;
	}
	
	// Set file permissions for 'refresh_token' and 'intune_account' to 0600
	int returnSecureFilePermission() {
		string valueToConvert = to!string(defaultFilePermissionMode);
		auto convertedValue = parse!long(valueToConvert, 8);
		return to!int(convertedValue);
	}
	
	// Load a configuration file from the provided filename
	private bool loadConfigFile(string filename) {
		try {
			addLogEntry("Reading configuration file: " ~ filename);
			readText(filename);
		} catch (std.file.FileException e) {
			addLogEntry("ERROR: Unable to access " ~ e.msg);
			return false;
		}
		
		auto file = File(filename, "r");
		string lineBuffer;
		
		scope(exit) {
			file.close();
			object.destroy(file);
			object.destroy(lineBuffer);
		}
		
		scope(failure) {
			file.close();
			object.destroy(file);
			object.destroy(lineBuffer);
		}
		
		foreach (line; file.byLine()) {
			lineBuffer = stripLeft(line).to!string;
			if (lineBuffer.empty || lineBuffer[0] == ';' || lineBuffer[0] == '#') continue;
			auto c = lineBuffer.matchFirst(configRegex);
			if (c.empty) {
				addLogEntry("Malformed config line: " ~ lineBuffer);
				addLogEntry();
				addLogEntry("Please review the documentation on how to correctly configure this application.");
				forceExit();
			}

			c.popFront(); // skip the whole match
			string key = c.front.dup;
			c.popFront();

			// Handle deprecated keys
			switch (key) {
				case "min_notify_changes":
				case "force_http_2":
					addLogEntry("The option '" ~ key ~ "' has been deprecated and will be ignored. Please read the updated documentation and update your client configuration to remove this option.");
					continue;
				case "sync_business_shared_folders":
					addLogEntry();
					addLogEntry("The option 'sync_business_shared_folders' has been deprecated and the process for synchronising Microsoft OneDrive Business Shared Folders has changed.");
					addLogEntry("Please review the revised documentation on how to correctly configure this application feature.");
					addLogEntry("You must update your client configuration and make changes to your local filesystem and online data to use this capability.");
					return false;
				default:
					break;
			}

			// Process other keys
			if (key in boolValues) {
				// Strip quotes and whitespace
				string rawValue = to!string(c.front.dup);
				// Evaluate rawValue
				if (rawValue == "true") {
					setValueBool(key, true);
					// Additional config-specific flags for specific keys
					if (key == "check_nosync") configFileCheckNoSync = true;
					if (key == "skip_dotfiles") configFileSkipDotfiles = true;
					if (key == "skip_symlinks") configFileSkipSymbolicLinks = true;
					if (key == "sync_business_shared_items") configFileSyncBusinessSharedItems = true;
				} else if (rawValue == "false") {
					setValueBool(key, false);
				} else {
					addLogEntry("Invalid boolean value for key in config file: " ~ key ~ " = " ~ to!string(c.front.dup));
					addLogEntry("ERROR: Only 'true' or 'false' are accepted for this setting.");
					forceExit();
				}
			} else if (key in stringValues) {
				string value = c.front.dup;
				setValueString(key, value);
				if (key == "sync_dir") {
					if (!strip(value).empty) {
						configFileSyncDir = value;
					} else {
						addLogEntry();
						addLogEntry("Invalid value for key in config file: " ~ key);
						addLogEntry("ERROR: sync_dir in config file cannot be empty - this is a fatal error and must be corrected");
						addLogEntry();
						forceExit();
					}
				} else if (key == "skip_file") {
					// Flag this as true
					configFileSkipFileReadIn = true;
					// Merge safely, removing empty entries and de-duplicating
					configFileSkipFile = mergePipeDelimitedRulesDedup(configFileSkipFile, to!string(c.front.dup));
					// Update stored config value
					setValueString("skip_file", configFileSkipFile);
				} else if (key == "skip_dir") {
					// Merge safely, removing empty entries and de-duplicating
					configFileSkipDir = mergePipeDelimitedRulesDedup(configFileSkipDir, to!string(c.front.dup));
					// Update stored config value
					setValueString("skip_dir", configFileSkipDir);
				} else if (key == "single_directory") {
					// --single-directory Strip quotation marks from path 
					// This is an issue when using ONEDRIVE_SINGLE_DIRECTORY with Docker
					string configFileSingleDirectory = strip(to!string(c.front.dup), "\"");
					setValueString("single_directory", configFileSingleDirectory);
				} else if (key == "azure_ad_endpoint") {
					switch (value) {
						case "":
							addLogEntry("Using default config option for Global Azure AD Endpoints");
							break;
						case "USL4":
							addLogEntry("Using config option for Azure AD for US Government Endpoints");
							break;
						case "USL5":
							addLogEntry("Using config option for Azure AD for US Government Endpoints (DOD)");
							break;
						case "DE":
							addLogEntry("Using config option for Azure AD Germany");
							break;
						case "CN":
							addLogEntry("Using config option for Azure AD China operated by VNET");
							break;
						default:
							addLogEntry("Unknown Azure AD Endpoint - using Global Azure AD Endpoints");
					}
				} else if (key == "transfer_order") {
					switch (value) {
						case "size_asc":
							addLogEntry("Files will be transferred sorted by ascending size (smallest first)");
							break;
						case "size_dsc":
							addLogEntry("Files will be transferred sorted by descending size (largest first)");
							break;
						case "name_asc":
							addLogEntry("Files will be transferred sorted by ascending name (A -> Z)");
							break;
						case "name_dsc":
							addLogEntry("Files will be transferred sorted by descending name (Z -> A)");
							break;
						default:
							addLogEntry("Files will be transferred in original order that they were received (FIFO)");
					}
				} else if (key == "application_id") {
					string tempApplicationId = strip(value);
					if (tempApplicationId.empty) {
						addLogEntry("Invalid value for key in config file - using default value: " ~ key);
						if (debugLogging) {addLogEntry("application_id in config file cannot be empty - using default application_id", ["debug"]);}
						setValueString("application_id", defaultApplicationId);
					}
				} else if (key == "drive_id") {
					string tempDriveId = strip(value);
					if (tempDriveId.empty) {
						addLogEntry();
						addLogEntry("Invalid value for key in config file: " ~ key);
						if (debugLogging) {addLogEntry("drive_id in config file cannot be empty - this is a fatal error and must be corrected by removing this entry from your config file.", ["debug"]);}
						addLogEntry();
						forceExit();
					} else {
						configFileDriveId = tempDriveId;
					}
				} else if (key == "log_dir") {
					string tempLogDir = strip(value);
					if (tempLogDir.empty) {
						addLogEntry("Invalid value for key in config file - using default value: " ~ key);
						if (debugLogging) {addLogEntry("log_dir in config file cannot be empty - using default log_dir", ["debug"]);}
						setValueString("log_dir", defaultLogFileDir);
					}
				}
			} else if (key in longValues) {
				ulong thisConfigValue;
				try {
					thisConfigValue = to!ulong(c.front.dup);
				} catch (std.conv.ConvException) {
					addLogEntry("Invalid value for key in config file: " ~ key);
					return false;
				}
				setValueLong(key, thisConfigValue);
				if (key == "monitor_interval") { // if key is 'monitor_interval' the value must be 300 or greater
					ulong tempValue = thisConfigValue;
					// the temp value needs to be 300 or greater
					if (tempValue < defaultMonitorInterval) {
						addLogEntry("Invalid value for key in config file - using default value: " ~ key);
						tempValue = defaultMonitorInterval;
					}
					setValueLong("monitor_interval", tempValue);
				} else if (key == "monitor_fullscan_frequency") { // if key is 'monitor_fullscan_frequency' the value must be 12 or greater
					ulong tempValue = thisConfigValue;
					// the temp value needs to be 12 or greater
					if (tempValue < 12) {
						// If this is not set to zero (0) then we are not disabling 'monitor_fullscan_frequency'
						if (tempValue != 0) {
							// invalid value
							addLogEntry("Invalid value for key in config file - using default value: " ~ key);
							tempValue = 12;
						}
					}
					setValueLong("monitor_fullscan_frequency", tempValue);
				} else if (key == "space_reservation") { // if key is 'space_reservation' we have to calculate MB -> bytes
					ulong tempValue = thisConfigValue;
					// a value of 0 needs to be made at least 1MB .. 
					if (tempValue == 0) {
						addLogEntry("Invalid value for key in config file - using 1MB: " ~ key);
						tempValue = 1;
					}
					setValueLong("space_reservation", tempValue * 2^^20);
				} else if (key == "ip_protocol_version") {
					ulong tempValue = thisConfigValue;
					if (tempValue > 2) {
						addLogEntry("Invalid value for key in config file - using default value: " ~ key);
						tempValue = defaultIpProtocol;
					}
					setValueLong("ip_protocol_version", tempValue);
				} else if (key == "threads") {
					ulong tempValue = thisConfigValue;
					if (tempValue > 16) {
						addLogEntry("Invalid value for key in config file - using default value: " ~ key);
						tempValue = defaultConcurrentThreads;
					}
					setValueLong("threads", tempValue);
				} else if (key == "inotify_delay") {
					ulong tempValue = thisConfigValue;
					if ((tempValue < 5)||(tempValue > 15)) {
						addLogEntry("Invalid value for key in config file - using default value: " ~ key);
						tempValue = defaultInotifyDelay;
					}
					setValueLong("inotify_delay", tempValue);
				} else if (key == "skip_size") {
					// Flag this for triggering --resync requirement
					configFileSkipSize = true;
					ulong tempValue = thisConfigValue;
					// If set, this must be greater than 0
					if (tempValue <= 0) {
						addLogEntry("Invalid value for key in config file - using default value: " ~ key);
						tempValue = 0;
					}
					setValueLong("skip_size", tempValue);
				} else if (key == "file_fragment_size") {
					ulong tempValue = thisConfigValue;
					// If set, this must be greater than the default, but also aligning to Microsoft upper limit of 60 MiB
					// Enforce lower bound (must be greater than default)
					if (tempValue < defaultFileFragmentSize) {
						addLogEntry("Invalid value for key in config file (too low) - using default value: " ~ key);
						tempValue = defaultFileFragmentSize;
					}
					// Enforce upper bound (safe maximum)
					else if (tempValue > defaultMaxFileFragmentSize) {
						addLogEntry("Invalid value for key in config file (too high) - using maximum safe value: " ~ key);
						tempValue = defaultMaxFileFragmentSize;
					}
					setValueLong("file_fragment_size", tempValue);
				}
			} else {
				addLogEntry("Unknown key in config file: " ~ key);
				return false;
			}
		}
		
		// If we read in 'skip_file' from the 'config' file, this will be 'true'
		if (configFileSkipFileReadIn) {
			// The user added entries, are the application defaults included or were these discarded / discounted?
			// Check for temporary and/or transient files to skip (application defaults)
			checkForSkipFileDefaults();
		}
		
		// Return that we were able to read in the config file and parse the options without issue
		return true;
	}
	
	// Perform a check on 'skip_file' configuration post reading from 'config' file
	void checkForSkipFileDefaults() {
		// Split both the default and user values
		auto defaultEntries = defaultSkipFile.split('|').map!(a => a.strip).array;
		auto userEntries = configFileSkipFile.split('|').map!(a => a.strip).array;

		string[] missingDefaults;

		// Check if all defaults exist in user config
		foreach (defaultEntry; defaultEntries) {
			if (!userEntries.canFind(defaultEntry)) {
				missingDefaults ~= defaultEntry;
			}
		}

		// Display warning message about missing default entries for temporary and/or transient files that should be skipped
		if (!missingDefaults.empty) {
			addLogEntry();
			addLogEntry("WARNING: Your 'skip_file' configuration is missing important default entries. Temporary and/or transient files that would normally be skipped may now be included in syncing.", ["info", "notify"]);
			addLogEntry();
			if (verboseLogging) {
				addLogEntry("By default, the following types of temporary and/or transient files are skipped:", ["verbose"]);
				addLogEntry("  Files that start with '~' (Temporary or backup files that are not intended to be saved permanently)", ["verbose"]);
				addLogEntry("  Files that start with '.~' (e.g., LibreOffice lock files)", ["verbose"]);
				addLogEntry("  Files that end with '.tmp' (Generic temporary files created by applications like browsers, editors, installers)", ["verbose"]);
				addLogEntry("  Files that end with '.swp' (Transient files created by editors such as vim and vi)", ["verbose"]);
				addLogEntry("  Files that end with '.partial' (Partially downloaded files, incomplete by nature, should not be synced)", ["verbose"]);
				addLogEntry();
				addLogEntry("  Missing the following important 'skip_file' entries: " ~ missingDefaults.join(", "), ["verbose"]);
				addLogEntry();
				addLogEntry("Reference: https://github.com/abraunegg/onedrive/blob/master/docs/application-config-options.md#skip_file", ["verbose"]);
				addLogEntry();
			}
		}
	}

	// Update the application configuration based on CLI passed in parameters
	void updateFromArgs(string[] cliArgs) {
		// Add additional CLI options that are NOT configurable via config file
		stringValues["create_directory"] = "";
		stringValues["create_share_link"] = "";
		stringValues["destination_directory"] = "";
		stringValues["get_file_link"] = "";
		stringValues["modified_by"] = "";
		stringValues["sharepoint_library_name"] = "";
		stringValues["remove_directory"] = "";
		stringValues["single_directory"] = "";
		stringValues["source_directory"] = "";
		stringValues["auth_files"] = "";
		stringValues["auth_response"] = "";
		stringValues["share_password"] = "";
		stringValues["download_single_file"] = "";
		boolValues["display_config"] = false;
		boolValues["display_sync_status"] = false;
		boolValues["display_quota"] = false;
		boolValues["print_token"] = false;
		boolValues["logout"] = false;
		boolValues["reauth"] = false;
		boolValues["monitor"] = false;
		boolValues["synchronize"] = false;
		boolValues["force"] = false;
		boolValues["list_business_shared_items"] = false;
		boolValues["sync_business_shared_files"] = false;
		boolValues["force_sync"] = false;
		boolValues["with_editing_perms"] = false;
		
		// Specific options for CLI input handling
		stringValues["sync_dir_cli"] = "";
		
		// Application Startup option validation
		try {
			string tmpStr;
			bool tmpBol;
			long tmpVerb;
			// duplicated from main.d to get full help output!
			auto opt = getopt(

				cliArgs,
				std.getopt.config.bundling,
				std.getopt.config.caseSensitive,
				"auth-files",
					"Perform authentication via files rather than an interactive dialogue. The application reads/writes the required values from/to the specified files",
					&stringValues["auth_files"],
				"auth-response",
					"Perform authentication via a supplied response URL rather than an interactive dialogue",
					&stringValues["auth_response"],
				"check-for-nomount",
					"Check for the presence of .nosync in the syncdir root. If found, do not perform sync",
					&boolValues["check_nomount"],
				"check-for-nosync",
					"Check for the presence of .nosync in each directory. If found, skip directory from sync",
					&boolValues["check_nosync"],
				"classify-as-big-delete",
					"Number of children in a path that is locally removed which will be classified as a 'big data delete'",
					&longValues["classify_as_big_delete"],
				"cleanup-local-files",
					"Clean up additional local files when using --download-only. This will remove local data",
					&boolValues["cleanup_local_files"],	
				"create-directory",
					"Create a directory on OneDrive. No synchronisation will be performed",
					&stringValues["create_directory"],
				"create-share-link",
					"Create a shareable link for an existing file on OneDrive",
					&stringValues["create_share_link"],
				"debug-https",
					"Debug OneDrive HTTPS communication.",
					&boolValues["debug_https"],
				"destination-directory",
					"Destination directory for renamed or moved items on OneDrive. No synchronisation will be performed",
					&stringValues["destination_directory"],
				"disable-notifications",
					"Do not use desktop notifications in monitor mode",
					&boolValues["disable_notifications"],
				"disable-download-validation",
					"Disable download validation when downloading from OneDrive",
					&boolValues["disable_download_validation"],
				"disable-upload-validation",
					"Disable upload validation when uploading to OneDrive",
					&boolValues["disable_upload_validation"],
				"display-config",
					"Display what options the client will use as currently configured. No synchronisation will be performed",
					&boolValues["display_config"],
				"display-running-config",
					"Display what options the client has been configured to use on application startup",
					&boolValues["display_running_config"],
				"display-sync-status",
					"Display the sync status of the client. No synchronisation will be performed",
					&boolValues["display_sync_status"],
				"display-quota",
					"Display the quota status of the client. No synchronisation will be performed",
					&boolValues["display_quota"],
				"download-only",
					"Replicate the OneDrive online state locally, by only downloading changes from OneDrive. Do not upload local changes to OneDrive",
					&boolValues["download_only"],
				"download-file",
					"Download a single file from Microsoft OneDrive",
					&stringValues["download_single_file"],
				"dry-run",
					"Perform a trial sync with no changes made",
					&boolValues["dry_run"],
				"enable-logging",
					"Enable client activity to a separate log file",
					&boolValues["enable_logging"],
				"file-fragment-size",
					"Specify the file fragment size for large file uploads (in MB)",
					&longValues["file_fragment_size"],
				"force-http-11",
					"Force the use of HTTP 1.1 for all operations",
					&boolValues["force_http_11"],
				"force",
					"Force the deletion of data when a 'big delete' is detected",
					&boolValues["force"],
				"force-sync",
					"Force a synchronisation of a specific folder, only when using --sync --single-directory and ignore all non-default skip_dir and skip_file rules",
					&boolValues["force_sync"],
				"get-file-link",
					"Display the file link of a synced file",
					&stringValues["get_file_link"],
				"get-sharepoint-drive-id",
					"Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library",
					&stringValues["sharepoint_library_name"],
				"get-O365-drive-id",
					"Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library (DEPRECATED)",
					&stringValues["sharepoint_library_name"],
				"list-shared-items",
					"List OneDrive Business Shared Items",
					&boolValues["list_business_shared_items"],
				"sync-shared-files",
					"Sync OneDrive Business Shared Files to the local filesystem",
					&boolValues["sync_business_shared_files"],
				"local-first",
					"Synchronise from the local directory source first, before downloading changes from OneDrive",
					&boolValues["local_first"],
				"log-dir",
					"Directory where logging output is saved to, needs to end with a slash",
					&stringValues["log_dir"],
				"logout",
					"Log out the current user",
					&boolValues["logout"],
				"modified-by",
					"Display the last modified by details of a given path",
					&stringValues["modified_by"],
				"monitor|m",
					"Keep monitoring for local and remote changes",
					&boolValues["monitor"],
				"monitor-interval",
					"Number of seconds by which each sync operation is undertaken when idle under monitor mode",
					&longValues["monitor_interval"],
				"monitor-fullscan-frequency",
					"Number of sync runs before performing a full local scan of the synced directory",
					&longValues["monitor_fullscan_frequency"],
				"monitor-log-frequency",
					"Frequency of logging in monitor mode",
					&longValues["monitor_log_frequency"],
				"no-remote-delete",
					"Do not delete local file 'deletes' from OneDrive when using --upload-only",
					&boolValues["no_remote_delete"],
				"print-access-token",
					"Print the access token, useful for debugging",
					&boolValues["print_token"],
				"reauth",
					"Reauthenticate the client with OneDrive",
					&boolValues["reauth"],
				"resync",
					"Forget the last saved state, perform a full sync",
					&boolValues["resync"],
				"resync-auth",
					"Approve the use of performing a --resync action",
					&boolValues["resync_auth"],
				"remove-directory",
					"Remove a directory on OneDrive. No synchronisation will be performed",
					&stringValues["remove_directory"],
				"remove-source-files",
					"Remove source file after successful transfer to OneDrive when using --upload-only",
					&boolValues["remove_source_files"],
				"remove-source-folders",
					"Remove the local directory structure post successful file transfer to Microsoft OneDrive when using --upload-only --remove-source-files",
					&boolValues["remove_source_folders"],
				"single-directory",
					"Specify a single local directory within the OneDrive root to sync",
					&stringValues["single_directory"],
				"skip-dot-files",
					"Skip dot files and folders from syncing",
					&boolValues["skip_dotfiles"],
				"skip-file",
					"Skip any files that match this pattern from syncing",
					&stringValues["skip_file"],
				"skip-dir",
					"Skip any directories that match this pattern from syncing",
					&stringValues["skip_dir"],
				"skip-size",
					"Skip new files larger than this size (in MB)",
					&longValues["skip_size"],
				"skip-dir-strict-match",
					"When matching skip_dir directories, only match explicit matches",
					&boolValues["skip_dir_strict_match"],
				"skip-symlinks",
					"Skip syncing of symlinks",
					&boolValues["skip_symlinks"],
				"source-directory",
					"Source directory to rename or move on OneDrive. No synchronisation will be performed",
					&stringValues["source_directory"],
				"space-reservation",
					"The amount of disk space to reserve (in MB) to avoid 100% disk space utilisation",
					&longValues["space_reservation"],
				"syncdir",
					"Specify the local directory used for synchronisation to OneDrive",
					&stringValues["sync_dir_cli"],
				"share-password",
					"Require a password to access the shared link when used with --create-share-link <file>",
					&stringValues["share_password"],
				"sync|s",
					"Perform a synchronisation with Microsoft OneDrive",
					&boolValues["synchronize"],
				"synchronize",
					"Perform a synchronisation with Microsoft OneDrive (DEPRECATED)",
					&boolValues["synchronize"],
				"sync-root-files",
					"Sync all files in sync_dir root when using sync_list",
					&boolValues["sync_root_files"],
				"threads",
					"Specify a value for the number of worker threads used for parallel upload and download operations",
					&longValues["threads"],
				"upload-only",
					"Replicate the locally configured sync_dir state to OneDrive, by only uploading local changes to OneDrive. Do not download changes from OneDrive",
					&boolValues["upload_only"],
				"confdir",
					"Set the directory used to store the configuration files",
					&tmpStr,
				"verbose|v+",
					"Print more details, useful for debugging (repeat for extra debugging)",
					&tmpVerb,
				"version",
					"Print the version and exit",
					&tmpBol,
				"with-editing-perms",
					"Create a read-write shareable link for an existing file on OneDrive when used with --create-share-link <file>",
					&boolValues["with_editing_perms"]
			);
			
			// Was --syncdir specified?
			if (!getValueString("sync_dir_cli").empty) {
				// Build the line we need to update and/or write out
				string newConfigOptionSyncDirLine = "sync_dir = \"" ~ getValueString("sync_dir_cli") ~ "\"";
				
				// Does a 'config' file exist?
				if (!exists(applicableConfigFilePath)) {
					// No existing 'config' file exists, create it, and write the 'sync_dir' configuration to it
					if (!getValueBool("dry_run")) {
						std.file.write(applicableConfigFilePath, newConfigOptionSyncDirLine);
						// Config file should only be readable by the user who created it - 0600 permissions needed
						applicableConfigFilePath.setAttributes(convertedPermissionValue);
					}
				} else {
					// an existing config file exists .. so this now becomes tricky
					// string replace 'sync_dir' if it exists, in the existing 'config' file, but only if 'sync_dir' (already read in) is different from 'sync_dir_cli'
					if ( (getValueString("sync_dir")) != (getValueString("sync_dir_cli")) ) {
						// values are different
						File applicableConfigFilePathFileHandle = File(applicableConfigFilePath, "r");
						string lineBuffer;
						string[] newConfigFileEntries;
						
						// read applicableConfigFilePath line by line
						auto range = applicableConfigFilePathFileHandle.byLine();
						
						// for each 'config' file line
						foreach (line; range) {
							lineBuffer = stripLeft(line).to!string;
							if (lineBuffer.length == 0 || lineBuffer[0] == ';' || lineBuffer[0] == '#') {
								newConfigFileEntries ~= [lineBuffer];
							} else {
								auto c = lineBuffer.matchFirst(configRegex);
								if (!c.empty) {
									c.popFront(); // skip the whole match
									string key = c.front.dup;
									if (key == "sync_dir") {
										// lineBuffer is the line we want to keep
										newConfigFileEntries ~= [newConfigOptionSyncDirLine];
									} else {
										newConfigFileEntries ~= [lineBuffer];
									}
								}
							}
						}
						
						// close original 'config' file if still open
						if (applicableConfigFilePathFileHandle.isOpen()) {
							// close open file
							applicableConfigFilePathFileHandle.close();
						}
						
						// free memory from file open
						object.destroy(applicableConfigFilePathFileHandle);
						
						// Update the existing item in the file line array
						if (!getValueBool("dry_run")) {
							// Open the file with write access using 'w' mode to overwrite existing content
							File applicableConfigFilePathFileHandleWrite = File(applicableConfigFilePath, "w");

							// Write each line from the 'newConfigFileEntries' array to the file
							foreach (line; newConfigFileEntries) {
								applicableConfigFilePathFileHandleWrite.writeln(line);
							}
							
							// Is this a running as a container
							if (entrypointExists) {
								// write this to the config file so that when config options are checked again, this matches on next run
								applicableConfigFilePathFileHandleWrite.writeln(newConfigOptionSyncDirLine);
							}

							// Flush and close the file handle to ensure all data is written
							if (applicableConfigFilePathFileHandleWrite.isOpen()) {
							applicableConfigFilePathFileHandleWrite.flush();
								applicableConfigFilePathFileHandleWrite.close();
							}

							// free memory from file open
							object.destroy(applicableConfigFilePathFileHandleWrite);
						}
					}
				}
				
				// Final - configure sync_dir with the value of sync_dir_cli so that it can be used as part of the application configuration and detect change
				setValueString("sync_dir", getValueString("sync_dir_cli"));
			}
			
			// Was --monitor-interval specified and now set to a value below minimum requirement?
			if (getValueLong("monitor_interval") < defaultMonitorInterval ) {
				addLogEntry("Invalid value for --monitor-interval - using default value: " ~ to!string(defaultMonitorInterval));
				setValueLong("monitor_interval", defaultMonitorInterval);
			}
			
			// Was --file-fragment-size specified and now set to a value below or above maximum?
			// Enforce lower bound (must be greater than default) for 'file_fragment_size'
			if (getValueLong("file_fragment_size") < defaultFileFragmentSize) {
				addLogEntry("Invalid value for --file-fragment-size (too low) - using default value: " ~ to!string(defaultFileFragmentSize));
				setValueLong("file_fragment_size", defaultFileFragmentSize);
			}
			// Enforce upper bound (safe maximum) for 'file_fragment_size'
			if (getValueLong("file_fragment_size") > defaultMaxFileFragmentSize) {
				addLogEntry("Invalid value for --file-fragment-size (too high) - using maximum safe value: " ~ to!string(defaultMaxFileFragmentSize));
				setValueLong("file_fragment_size", defaultMaxFileFragmentSize);
			}
			
			// Was --auth-files used?
			if (!getValueString("auth_files").empty) {
				// --auth-files used, need to validate that '~' was not used as a path identifier, and if yes, perform the correct expansion
				string[] tempAuthFiles = getValueString("auth_files").split(":");
				string tempAuthUrl = tempAuthFiles[0];
				string tempResponseUrl = tempAuthFiles[1];
				string newAuthFilesString;
				
				// shell expansion if required
				if (!shellEnvironmentSet){
					// No shell environment is set, no automatic expansion of '~' if present is possible
					// Does the 'currently configured' tempAuthUrl include a ~
					if (canFind(tempAuthUrl, "~")) {	
						// A ~ was found in auth_files(authURL)
						if (debugLogging) {addLogEntry("auth_files: A '~' was found in 'auth_files(authURL)', using the calculated 'homePath' to replace '~' as no SHELL or USER environment variable set", ["debug"]);}
						tempAuthUrl = buildNormalizedPath(buildPath(defaultHomePath, strip(tempAuthUrl, "~")));
					}
					
					// Does the 'currently configured' tempAuthUrl include a ~
					if (canFind(tempResponseUrl, "~")) {
						// A ~ was found in auth_files(authURL)
						if (debugLogging) {addLogEntry("auth_files: A '~' was found in 'auth_files(tempResponseUrl)', using the calculated 'homePath' to replace '~' as no SHELL or USER environment variable set", ["debug"]);}
						tempResponseUrl = buildNormalizedPath(buildPath(defaultHomePath, strip(tempResponseUrl, "~")));
					}
				} else {
					// Shell environment is set, automatic expansion of '~' if present is possible
					// Does the 'currently configured' tempAuthUrl include a ~
					if (canFind(tempAuthUrl, "~")) {
						// A ~ was found in auth_files(authURL)
						if (debugLogging) {addLogEntry("auth_files: A '~' was found in the configured 'auth_files(authURL)', automatically expanding as SHELL and USER environment variable is set", ["debug"]);}
						tempAuthUrl = expandTilde(tempAuthUrl);
					}
					
					// Does the 'currently configured' tempAuthUrl include a ~
					if (canFind(tempResponseUrl, "~")) {
						// A ~ was found in auth_files(authURL)
						if (debugLogging) {addLogEntry("auth_files: A '~' was found in the configured 'auth_files(tempResponseUrl)', automatically expanding as SHELL and USER environment variable is set", ["debug"]);}
						tempResponseUrl = expandTilde(tempResponseUrl);
					}
				}
				
				// Build new string
				newAuthFilesString = tempAuthUrl ~ ":" ~ tempResponseUrl;
				if (debugLogging) {addLogEntry("auth_files - updated value: " ~ newAuthFilesString, ["debug"]);}
				setValueString("auth_files", newAuthFilesString);
			}
			
			if (opt.helpWanted) {
				outputLongHelp(opt.options);
				// Shutdown logging, which also flushes all logging buffers
				shutdownLogging();
				// Exit as successful
				exit(EXIT_SUCCESS);
			}
		} catch (GetOptException e) {
			// getOpt error - must use writeln() here
			writeln(e.msg);
			writeln("Try 'onedrive -h' for more information");
			// Shutdown logging, which also flushes all logging buffers
			shutdownLogging();
			// Exit as failure
			exit(EXIT_FAILURE);
		} catch (Exception e) {
			// general error - must use writeln() here
			writeln(e.msg);
			writeln("Try 'onedrive -h' for more information");
			// Shutdown logging, which also flushes all logging buffers
			shutdownLogging();
			// Exit as failure
			exit(EXIT_FAILURE);
		}
	}
	
	// Check the arguments passed in for any that will be deprecated
	void checkDeprecatedOptions(string[] cliArgs) {
	
		bool deprecatedCommandsFound = false;
	
		foreach (cliArg; cliArgs) {
			// Check each CLI arg for items that have been deprecated
			
			// --synchronize deprecated in v2.5.0, will be removed in future version
			if (cliArg == "--synchronize") {
				addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering
				addLogEntry("DEPRECIATION WARNING: --synchronize has been deprecated in favour of --sync or -s");
				deprecatedCommandsFound = true;
			}
			
			// --get-O365-drive-id deprecated in v2.5.0, will be removed in future version
			if (cliArg == "--get-O365-drive-id") {
				addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering
				addLogEntry("DEPRECIATION WARNING: --get-O365-drive-id has been deprecated in favour of --get-sharepoint-drive-id");
				deprecatedCommandsFound = true;
			}
		}
	
		if (deprecatedCommandsFound) {
			addLogEntry("DEPRECIATION WARNING: Deprecated commands will be removed in a future release.");
			addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering
		}
	}
	
	// Display the applicable application configuration
	void displayApplicationConfiguration() {
		if (getValueBool("display_running_config")) {
			addLogEntry("--------------- Application Runtime Configuration ---------------");
		}
		
		// Display application version
		addLogEntry("Application version                          = " ~ applicationVersion);
		addLogEntry("Compiled with                                = " ~ compilerDetails());
		addLogEntry("Curl version                                 = " ~ getCurlVersionString());
		
		// Display all of the pertinent configuration options
		addLogEntry("User Application Config path                 = " ~ configDirName);
		addLogEntry("System Application Config path               = " ~ systemConfigDirName);
		
		// Does a config file exist or are we using application defaults
		addLogEntry("Applicable Application 'config' location     = " ~ applicableConfigFilePath);
		
		string configFileStatusMessage;
		if (exists(applicableConfigFilePath)) {
			configFileStatusMessage = "true - using 'config' file values to override application defaults";
		} else {
			configFileStatusMessage = "false - using application defaults";
		}
		addLogEntry("Configuration file found in config location  = " ~ configFileStatusMessage);
		
		// Display where various files should live
		// - items.sqlite3
		// - sync_list
		// If using the 'system' directory, (/etc/onedrive) for the config file, these should always live in the 'users' home directory
		addLogEntry("Applicable 'sync_list' location              = " ~ syncListFilePath);
		addLogEntry("Applicable 'items.sqlite3' location          = " ~ databaseFilePath);
		
		// Is config option drive_id configured?
		addLogEntry("Config option 'drive_id'                     = " ~ getValueString("drive_id"));
		
		// Config Options as per 'config' file
		addLogEntry("Config option 'sync_dir'                     = " ~ getValueString("sync_dir"));
		
		// authentication
		addLogEntry("Config option 'use_intune_sso'               = " ~ to!string(getValueBool("use_intune_sso")));
		addLogEntry("Config option 'use_device_auth'              = " ~ to!string(getValueBool("use_device_auth")));
				
		// logging and notifications
		addLogEntry("Config option 'enable_logging'               = " ~ to!string(getValueBool("enable_logging")));
		addLogEntry("Config option 'log_dir'                      = " ~ getValueString("log_dir"));
		addLogEntry("Config option 'disable_notifications'        = " ~ to!string(getValueBool("disable_notifications")));
		
		// skip files and directory and 'matching' policy
		addLogEntry("Config option 'skip_dir'                     = " ~ getValueString("skip_dir"));
		addLogEntry("Config option 'skip_dir_strict_match'        = " ~ to!string(getValueBool("skip_dir_strict_match")));
		addLogEntry("Config option 'skip_file'                    = " ~ getValueString("skip_file"));
		addLogEntry("Config option 'skip_dotfiles'                = " ~ to!string(getValueBool("skip_dotfiles")));
		addLogEntry("Config option 'skip_symlinks'                = " ~ to!string(getValueBool("skip_symlinks")));
		addLogEntry("Config option 'skip_size'                    = " ~ to!string(getValueLong("skip_size")));
		
		// --monitor sync process options
		addLogEntry("Config option 'monitor_interval'             = " ~ to!string(getValueLong("monitor_interval")));
		addLogEntry("Config option 'monitor_log_frequency'        = " ~ to!string(getValueLong("monitor_log_frequency")));
		addLogEntry("Config option 'monitor_fullscan_frequency'   = " ~ to!string(getValueLong("monitor_fullscan_frequency")));
		addLogEntry("Config option 'disable_websocket_support'    = " ~ to!string(getValueBool("disable_websocket_support")));
		
		// sync process and method
		addLogEntry("Config option 'read_only_auth_scope'         = " ~ to!string(getValueBool("read_only_auth_scope")));
		addLogEntry("Config option 'dry_run'                      = " ~ to!string(getValueBool("dry_run")));
		addLogEntry("Config option 'upload_only'                  = " ~ to!string(getValueBool("upload_only")));
		addLogEntry("Config option 'download_only'                = " ~ to!string(getValueBool("download_only")));
		addLogEntry("Config option 'local_first'                  = " ~ to!string(getValueBool("local_first")));
		addLogEntry("Config option 'check_nosync'                 = " ~ to!string(getValueBool("check_nosync")));
		addLogEntry("Config option 'check_nomount'                = " ~ to!string(getValueBool("check_nomount")));
		addLogEntry("Config option 'resync'                       = " ~ to!string(getValueBool("resync")));
		addLogEntry("Config option 'resync_auth'                  = " ~ to!string(getValueBool("resync_auth")));
		addLogEntry("Config option 'cleanup_local_files'          = " ~ to!string(getValueBool("cleanup_local_files")));
		addLogEntry("Config option 'disable_permission_set'       = " ~ to!string(getValueBool("disable_permission_set")));
		addLogEntry("Config option 'transfer_order'               = " ~ getValueString("transfer_order"));
		addLogEntry("Config option 'delay_inotify_processing'     = " ~ to!string(getValueBool("delay_inotify_processing")));
		addLogEntry("Config option 'inotify_delay'                = " ~ to!string(getValueLong("inotify_delay")));
		addLogEntry("Config option 'display_transfer_metrics'     = " ~ to!string(getValueBool("display_transfer_metrics")));
		addLogEntry("Config option 'force_session_upload'         = " ~ to!string(getValueBool("force_session_upload")));
		addLogEntry("Config option 'file_fragment_size'           = " ~ to!string(getValueLong("file_fragment_size")));
		
		// data integrity
		addLogEntry("Config option 'classify_as_big_delete'       = " ~ to!string(getValueLong("classify_as_big_delete")));
		addLogEntry("Config option 'disable_upload_validation'    = " ~ to!string(getValueBool("disable_upload_validation")));
		addLogEntry("Config option 'disable_download_validation'  = " ~ to!string(getValueBool("disable_download_validation")));
		addLogEntry("Config option 'bypass_data_preservation'     = " ~ to!string(getValueBool("bypass_data_preservation")));
		addLogEntry("Config option 'no_remote_delete'             = " ~ to!string(getValueBool("no_remote_delete")));
		addLogEntry("Config option 'remove_source_files'          = " ~ to!string(getValueBool("remove_source_files")));
		addLogEntry("Config option 'sync_dir_permissions'         = " ~ to!string(getValueLong("sync_dir_permissions")));
		addLogEntry("Config option 'sync_file_permissions'        = " ~ to!string(getValueLong("sync_file_permissions")));
		addLogEntry("Config option 'space_reservation'            = " ~ to!string(getValueLong("space_reservation")));
		addLogEntry("Config option 'permanent_delete'             = " ~ to!string(getValueBool("permanent_delete")));
		addLogEntry("Config option 'write_xattr_data'             = " ~ to!string(getValueBool("write_xattr_data")));
		addLogEntry("Config option 'create_new_file_version'      = " ~ to!string(getValueBool("create_new_file_version")));
		
		// curl operations
		addLogEntry("Config option 'application_id'               = " ~ getValueString("application_id"));
		addLogEntry("Config option 'azure_ad_endpoint'            = " ~ getValueString("azure_ad_endpoint"));
		addLogEntry("Config option 'azure_tenant_id'              = " ~ getValueString("azure_tenant_id"));
		addLogEntry("Config option 'user_agent'                   = " ~ getValueString("user_agent"));
		addLogEntry("Config option 'force_http_11'                = " ~ to!string(getValueBool("force_http_11")));
		addLogEntry("Config option 'debug_https'                  = " ~ to!string(getValueBool("debug_https")));
		addLogEntry("Config option 'rate_limit'                   = " ~ to!string(getValueLong("rate_limit")));
		addLogEntry("Config option 'operation_timeout'            = " ~ to!string(getValueLong("operation_timeout")));
		addLogEntry("Config option 'dns_timeout'                  = " ~ to!string(getValueLong("dns_timeout")));
		addLogEntry("Config option 'connect_timeout'              = " ~ to!string(getValueLong("connect_timeout")));
		addLogEntry("Config option 'data_timeout'                 = " ~ to!string(getValueLong("data_timeout")));
		addLogEntry("Config option 'ip_protocol_version'          = " ~ to!string(getValueLong("ip_protocol_version")));
		addLogEntry("Config option 'threads'                      = " ~ to!string(getValueLong("threads")));
		addLogEntry("Config option 'max_curl_idle'                = " ~ to!string(getValueLong("max_curl_idle")));
		
		// GUI notifications
		version(Notifications) {
			addLogEntry("Environment var 'XDG_RUNTIME_DIR'            = " ~ to!string(xdg_exists));
			addLogEntry("Environment var 'DBUS_SESSION_BUS_ADDRESS'   = " ~ to!string(dbus_exists));
			addLogEntry("Config option 'notify_file_actions'          = " ~ to!string(getValueBool("notify_file_actions")));
		} else {
			addLogEntry("Compile time option --enable-notifications   = false");
		}
		
		// Recycle Bin 
		addLogEntry("Config option 'use_recycle_bin'              = " ~ to!string(getValueBool("use_recycle_bin")));
		addLogEntry("Config option 'recycle_bin_path'             = " ~ getValueString("recycle_bin_path"));
				
		// Is sync_list configured and contains entries?
		if (exists(syncListFilePath) && getSize(syncListFilePath) > 0) {
			addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering
			addLogEntry("Selective sync 'sync_list' configured        = true");
			addLogEntry("sync_list config option 'sync_root_files'    = " ~ to!string(getValueBool("sync_root_files")));
			addLogEntry("sync_list contents:");
			// Output the sync_list contents
			auto syncListFile = File(syncListFilePath, "r");
			auto range = syncListFile.byLine();
			addLogEntry("------------------------------'sync_list'------------------------------");
			foreach (line; range) {
				addLogEntry(to!string(line));
			}
			addLogEntry("-----------------------------------------------------------------------");
			
			// Close reading the 'sync_list' file
			syncListFile.close();
		} else {
			// file does not exist or file size is not greater than 0
			addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering
			if (exists(syncListFilePath) && getSize(syncListFilePath) == 0) {
				// 'sync_list' file exists, no entries
				addLogEntry("Selective sync 'sync_list' configured        = file exists but contains zero data");
			} else {
				// no 'sync_list' file
				addLogEntry("Selective sync 'sync_list' configured        = false");
			}
		}
		
		// Is sync_business_shared_items enabled and configured ?
		addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering
		addLogEntry("Config option 'sync_business_shared_items'   = " ~ to!string(getValueBool("sync_business_shared_items")));
		if (getValueBool("sync_business_shared_items")) {
			// display what the shared files directory will be
			addLogEntry("Config option 'Shared Files Directory'       = " ~ configuredBusinessSharedFilesDirectoryName);
		}
		
		// Are webhooks enabled?
		addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering
		addLogEntry("Config option 'webhook_enabled'              = " ~ to!string(getValueBool("webhook_enabled")));
		if (getValueBool("webhook_enabled")) {
			addLogEntry("Config option 'webhook_public_url'           = " ~ getValueString("webhook_public_url"));
			addLogEntry("Config option 'webhook_listening_host'       = " ~ getValueString("webhook_listening_host"));
			addLogEntry("Config option 'webhook_listening_port'       = " ~ to!string(getValueLong("webhook_listening_port")));
			addLogEntry("Config option 'webhook_expiration_interval'  = " ~ to!string(getValueLong("webhook_expiration_interval")));
			addLogEntry("Config option 'webhook_renewal_interval'     = " ~ to!string(getValueLong("webhook_renewal_interval")));
			addLogEntry("Config option 'webhook_retry_interval'       = " ~ to!string(getValueLong("webhook_retry_interval")));
		}
		
		if (getValueBool("display_running_config")) {
			addLogEntry();
			addLogEntry("--------------------DEVELOPER_OPTIONS----------------------------");
			addLogEntry("Config option 'force_children_scan'          = " ~ to!string(getValueBool("force_children_scan")));
			addLogEntry();
		}
		
		if (getValueBool("display_running_config")) {
			addLogEntry("-----------------------------------------------------------------");
		}
	}
	
	// Prompt the user to accept the risk of using --resync
	bool displayResyncRiskForAcceptance() {
		// what is the user risk acceptance?
		bool userRiskAcceptance = false;
		
		// Did the user use --resync-auth or 'resync_auth' in the config file to negate presenting this message?
		if (!getValueBool("resync_auth")) {
			// need to prompt user
			char response;
			
			// --resync warning message
			addLogEntry("", ["consoleOnly"]); // new line, console only
			addLogEntry("WARNING: You have asked the client to perform a --resync operation.", ["consoleOnly"]);
			addLogEntry("", ["consoleOnly"]);
			addLogEntry("         This operation will delete the client’s local state database and rebuild it entirely from the current online OneDrive state.", ["consoleOnly"]);
			addLogEntry("", ["consoleOnly"]);
			addLogEntry("         Because the previous sync state will no longer be available, the following may occur:", ["consoleOnly"]);
			addLogEntry("         * Local files that also exist in OneDrive may have local changes overwritten by the cloud version if a conflict cannot be safely resolved.", ["consoleOnly"]);
			addLogEntry("         * Local files may be renamed or duplicated locally as part of conflict resolution and data-preservation handling.", ["consoleOnly"]);
			addLogEntry("         * The initial synchronisation pass may involve a large number of file uploads and downloads.", ["consoleOnly"]);
			addLogEntry("         * The increased activity against the Microsoft Graph API may trigger HTTP 429 (throttling) responses during the synchronisation process.", ["consoleOnly"]);
			addLogEntry("", ["consoleOnly"]);
			addLogEntry("         For safest operation:", ["consoleOnly"]);
			addLogEntry("         * Ensure you have a current backup of your sync_dir.", ["consoleOnly"]);
			addLogEntry("         * Run this command first with --dry-run to confirm all planned actions.", ["consoleOnly"]);
			addLogEntry("         * Enable 'use_recycle_bin' so that online deletion events from OneDrive are moved to your system Trash rather than deleted from your local disk.", ["consoleOnly"]);
			addLogEntry("", ["consoleOnly"]);
			addLogEntry("If in doubt, stop now and back up your local data before continuing.", ["consoleOnly"]);
			addLogEntry("", ["consoleOnly"]);
			addLogEntry("Are you sure you wish to proceed with --resync? [Y/N] ", ["consoleOnlyNoNewLine"]);
						
			try {
				// Attempt to read user response
				string input = readln().strip;
				if (input.length > 0) {
					response = std.ascii.toUpper(input[0]);
				}
			} catch (std.format.FormatException e) {
				userRiskAcceptance = false;
				// Caught an error
				return EXIT_FAILURE;
			}
			
			// What did the user enter?
			if (debugLogging) {addLogEntry("--resync warning User Response Entered: " ~ to!string(response), ["debug"]);}
			
			// Evaluate user response
			if ((to!string(response) == "y") || (to!string(response) == "Y")) {
				// User has accepted --resync risk to proceed
				userRiskAcceptance = true;
				// Are you sure you wish .. does not use writeln();
				write("\n");
			}
		} else {
			// resync_auth is true
			userRiskAcceptance = true;
		}
		
		// Return the --resync acceptance or not
		return userRiskAcceptance;
	}
	
	// Prompt the user to accept the risk of using --force-sync
	bool displayForceSyncRiskForAcceptance() {
		// what is the user risk acceptance?
		bool userRiskAcceptance = false;
		
		// need to prompt user
		char response;
		
		// --force-sync warning message
		addLogEntry("", ["consoleOnly"]); // new line, console only
		addLogEntry("The use of --force-sync will reconfigure the application to use defaults. This may have untold and unknown future impacts.", ["consoleOnly"]);
		addLogEntry("By proceeding in using this option you accept any impacts including any data loss that may occur as a result of using --force-sync.", ["consoleOnly"]);
		addLogEntry("", ["consoleOnly"]); // new line, console only
		addLogEntry("Are you sure you wish to proceed with --force-sync [Y/N] ", ["consoleOnlyNoNewLine"]);
				
		try {
			// Attempt to read user response
			string input = readln().strip;
			if (input.length > 0) {
				response = std.ascii.toUpper(input[0]);
			}
		} catch (std.format.FormatException e) {
			userRiskAcceptance = false;
			// Caught an error
			return EXIT_FAILURE;
		}
		
		// What did the user enter?
		if (debugLogging) {addLogEntry("--force-sync warning User Response Entered: " ~ to!string(response), ["debug"]);}
		
		// Evaluate user response
		if ((to!string(response) == "y") || (to!string(response) == "Y")) {
			// User has accepted --force-sync risk to proceed
			userRiskAcceptance = true;
			// Are you sure you wish .. does not use writeln();
			write("\n");
		}
		
		// Return the --resync acceptance or not
		return userRiskAcceptance;
	}
	
	// Check the application configuration for any changes that need to trigger a --resync
	// This function is only called if --resync is not present
	bool applicationChangeWhereResyncRequired() {
		// Set this function name
		string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({})));
	
		// Default is that no resync is required
		bool resyncRequired = false;

		// Consolidate the flags for different configuration changes
		bool[11] configOptionsDifferent;

		// Handle multiple entries of skip_file
		string backupConfigFileSkipFile;
		
		// Handle multiple entries of skip_dir
		string backupConfigFileSkipDir;
		
		// Create and read the required initial hash files
		createRequiredInitialConfigurationHashFiles();
		
		// Read in the existing hash file values
		readExistingConfigurationHashFiles();
		
		// can we read the backup config file
		bool failedToReadBackupConfig = false;

		// Helper lambda for logging and setting the difference flag
		auto logAndSetDifference = (string message, size_t index) {
			if (debugLogging) {addLogEntry(message, ["debug"]);}
			configOptionsDifferent[index] = true;
		};

		// Check for changes in the sync_list and business_shared_items files
		if (currentSyncListHash != previousSyncListHash)
			logAndSetDifference("sync_list file has been updated, --resync needed", 0);

		// Check for updates in the config file
		if (currentConfigHash != previousConfigHash) {
			addLogEntry("Application configuration file has been updated, checking if --resync needed");
			if (debugLogging) {addLogEntry("Using this configBackupFile: " ~ configBackupFile, ["debug"]);}

			if (exists(configBackupFile)) {
				string[string] backupConfigStringValues;
				backupConfigStringValues["check_nosync"] = "";
				backupConfigStringValues["drive_id"] = "";
				backupConfigStringValues["sync_dir"] = "";
				backupConfigStringValues["skip_file"] = "";
				backupConfigStringValues["skip_dir"] = "";
				backupConfigStringValues["skip_dotfiles"] = "";
				backupConfigStringValues["skip_size"] = "";
				backupConfigStringValues["skip_symlinks"] = "";
				backupConfigStringValues["sync_business_shared_items"] = "";

				bool check_nosync_present = false;
				bool drive_id_present = false;
				bool sync_dir_present = false;
				bool skip_file_present = false;
				bool skip_dir_present = false;
				bool skip_dotfiles_present = false;
				bool skip_size_present = false;
				bool skip_symlinks_present = false;
				bool sync_business_shared_items_present = false;

				string configOptionModifiedMessage = " was modified since the last time the application was successfully run, --resync required";
				File configBackupFileHandle;
				
				try {
					configBackupFileHandle = File(configBackupFile, "r");
				} catch (FileException e) {
					// filesystem error
					failedToReadBackupConfig = true;
					displayFileSystemErrorMessage(e.msg, thisFunctionName, configBackupFile, FsErrorSeverity.warning);
				} catch (std.exception.ErrnoException e) {
					// filesystem error
					failedToReadBackupConfig = true;
					displayFileSystemErrorMessage(e.msg, thisFunctionName, configBackupFile, FsErrorSeverity.warning);
				}
				
				scope(exit) {
					if (configBackupFileHandle.isOpen()) {
						configBackupFileHandle.close();
					}
				}
				
				if (!failedToReadBackupConfig) {
					// backup config file was able to be read
					string lineBuffer;
					auto range = configBackupFileHandle.byLine();
					foreach (line; range) {
						lineBuffer = stripLeft(line).to!string;
						if (lineBuffer.length == 0 || lineBuffer[0] == ';' || lineBuffer[0] == '#') continue;
						auto c = lineBuffer.matchFirst(configRegex);
						if (!c.empty) {
							c.popFront(); // skip the whole match
							string key = c.front.dup;
							if (debugLogging) {addLogEntry("Backup Config Key: " ~ key, ["debug"]);}

							auto p = key in backupConfigStringValues;
							if (p) {
								c.popFront();
								string value = c.front.dup;
								// Compare each key value with current config
								if (key == "drive_id") {
									drive_id_present = true;
									if (value != getValueString("drive_id")) {
										logAndSetDifference(key ~ configOptionModifiedMessage, 2);
									}
								}
								if (key == "sync_dir") {
									sync_dir_present = true;
									if (value != getValueString("sync_dir")) {
										logAndSetDifference(key ~ configOptionModifiedMessage, 3);
									}
								}
								
								// skip_file handling
								if (key == "skip_file") {
									skip_file_present = true;
									// Merge safely, removing empty entries and de-duplicating
									backupConfigFileSkipFile = mergePipeDelimitedRulesDedup(backupConfigFileSkipFile, to!string(c.front.dup));
								}
								
								// skip_dir handling
								if (key == "skip_dir") {
									skip_dir_present = true;
									// Merge safely, removing empty entries and de-duplicating
									backupConfigFileSkipDir = mergePipeDelimitedRulesDedup(backupConfigFileSkipDir, to!string(c.front.dup));
								}
								
								if (key == "skip_dotfiles") {
									skip_dotfiles_present = true;
									if (value != to!string(getValueBool("skip_dotfiles"))) {
										logAndSetDifference(key ~ configOptionModifiedMessage, 6);
									}
								}
								if (key == "skip_symlinks") {
									skip_symlinks_present = true;
									if (value != to!string(getValueBool("skip_symlinks"))) {
										logAndSetDifference(key ~ configOptionModifiedMessage, 7);
									}
								}
								if (key == "sync_business_shared_items") {
									sync_business_shared_items_present = true;
									if (value != to!string(getValueBool("sync_business_shared_items"))) {
										logAndSetDifference(key ~ configOptionModifiedMessage, 8);
									}
								}
							}
						}
					}
					
					// Debug logging
					if (debugLogging) {
						addLogEntry("skip_file in actual config = " ~ to!string(configFileSkipFileReadIn), ["debug"]);
						addLogEntry("skip_file in backup config = " ~ to!string(skip_file_present), ["debug"]);
						addLogEntry("defaultSkipFile value = " ~ to!string(defaultSkipFile), ["debug"]);
						addLogEntry("configFileSkipFile value = " ~ to!string(configFileSkipFile), ["debug"]);
						addLogEntry("backupConfigFileSkipFile value = " ~ to!string(backupConfigFileSkipFile), ["debug"]);
					}
				
					// skip_file can be specified multiple times
					if (skip_file_present && backupConfigFileSkipFile != configFileSkipFile) logAndSetDifference("skip_file" ~ configOptionModifiedMessage, 4);
					
					// skip_file can also be an empty string, thus when removed, as an empty string, we are going back to application defaults
					if (skip_file_present && backupConfigFileSkipFile != defaultSkipFile) logAndSetDifference("skip_file" ~ configOptionModifiedMessage, 4);
					
					// skip_dir can be specified multiple times
					if (skip_dir_present && backupConfigFileSkipDir != configFileSkipDir) logAndSetDifference("skip_dir" ~ configOptionModifiedMessage, 5);
					
					// Check for newly added configuration options to the 'config' file vs being present in the 'backup' config file
					if (!drive_id_present && configFileDriveId != "") logAndSetDifference("drive_id newly added ... --resync needed", 2);
					if (!sync_dir_present && configFileSyncDir != defaultSyncDir) logAndSetDifference("sync_dir newly added ... --resync needed", 3);
					if (configFileSkipFileReadIn) {
						// We actually read a 'skip_file' configuration line from the 'config' file
						if (!skip_file_present && configFileSkipFile != defaultSkipFile) logAndSetDifference("skip_file newly added ... --resync needed", 4);
					}
					
					// Other options
					if (!skip_dir_present && configFileSkipDir != "") logAndSetDifference("skip_dir newly added ... --resync needed", 5);
					if (!skip_dotfiles_present && configFileSkipDotfiles) logAndSetDifference("skip_dotfiles newly added ... --resync needed", 6);
					if (!skip_symlinks_present && configFileSkipSymbolicLinks) logAndSetDifference("skip_symlinks newly added ... --resync needed", 7);
					if (!sync_business_shared_items_present && configFileSyncBusinessSharedItems) logAndSetDifference("sync_business_shared_items newly added ... --resync needed", 8);
					if (!check_nosync_present && configFileCheckNoSync) logAndSetDifference("check_nosync newly added ... --resync needed", 9);
					if (!skip_size_present && configFileSkipSize) logAndSetDifference("skip_size newly added ... --resync needed", 10);
				} else {
					// failed to read backup config file
					addLogEntry("WARNING: unable to read backup config, unable to validate if any changes made");
				}
			} else {
				addLogEntry("WARNING: no backup config file was found, unable to validate if any changes made");
			}
		}
		
		// config file set options can be changed via CLI input, specifically these will impact sync and a --resync will be needed:
		//  --syncdir ARG
		//  --skip-file ARG
		//  --skip-dir ARG
		//  --skip-dot-files
		//  --skip-symlinks
		//  --check-for-nosync
		//  --skip-size ARG

		// Check CLI options
		if (exists(applicableConfigFilePath)) {
			if (configFileSyncDir != "" && configFileSyncDir != getValueString("sync_dir")) {
				// config file was set and CLI input changed this
				// Is this potentially running as a Docker container?
				if (entrypointExists) {
					// entrypoint.sh exists
					if (debugLogging) {addLogEntry("sync_dir: CLI override of config file option, however entrypoint.sh exists, thus most likely running as a container", ["debug"]);}
				} else {
					// Not a Docker container, raise that --resync needed due to configuration change
					logAndSetDifference("sync_dir: CLI override of config file option, --resync needed", 3);
				}
			}
			
			if (configFileSkipFile != "" && configFileSkipFile != getValueString("skip_file")) logAndSetDifference("skip_file: CLI override of config file option, --resync needed", 4);
			if (configFileSkipDir != "" && configFileSkipDir != getValueString("skip_dir")) logAndSetDifference("skip_dir: CLI override of config file option, --resync needed", 5);
			if (!configFileSkipDotfiles && getValueBool("skip_dotfiles")) logAndSetDifference("skip_dotfiles: CLI override of config file option, --resync needed", 6);
			if (!configFileSkipSymbolicLinks && getValueBool("skip_symlinks")) logAndSetDifference("skip_symlinks: CLI override of config file option, --resync needed", 7);
			if (!configFileCheckNoSync && getValueBool("check_nosync")) logAndSetDifference("check_nosync: CLI override of config file option, --resync needed", 9);
			if (!configFileSkipSize && (getValueLong("skip_size") > 0)) logAndSetDifference("skip_size: CLI override of config file option, --resync needed", 10);
		}

		// Aggregate the result to determine if a resync is required
		if (!failedToReadBackupConfig) {
			foreach (optionDifferent; configOptionsDifferent) {
				if (optionDifferent) {
					resyncRequired = true;
					break;
				}
			}
		}
		
		// Final override
		// In certain situations, regardless of config 'resync' needed status, ignore this so that the application can display 'non-syncable' information
		// Options that should now be looked at are:
		// --list-shared-items
		if (getValueBool("list_business_shared_items")) resyncRequired = false;
		
		// Return the calculated boolean
		return resyncRequired;
	}
	
	// Cleanup hash files that require to be cleaned up when a --resync is issued
	void cleanupHashFilesDueToResync() {
		if (!getValueBool("dry_run")) {
			// cleanup hash files
			if (debugLogging) {addLogEntry("Cleaning up configuration hash files", ["debug"]);}
			safeRemove(configHashFile);
			safeRemove(syncListHashFile);
		} else {
			// --dry-run scenario ... technically we should not be making any local file changes .......
			addLogEntry("DRY-RUN: Not removing hash files as --dry-run has been used");
		}
	}
	
	// For each of the config files, update the hash data in the hash files
	void updateHashContentsForConfigFiles() {
		// Set this function name
		string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({})));
	
		// Are we in a --dry-run scenario?
		if (!getValueBool("dry_run")) {
			// Not a dry-run scenario, update the applicable files
			// Update applicable 'config' files
			if (exists(applicableConfigFilePath)) {
				// Update the hash of the applicable config file
				if (debugLogging) {addLogEntry("Updating applicable config file hash", ["debug"]);}
				try {
					std.file.write(configHashFile, computeQuickXorHash(applicableConfigFilePath));
					// Hash file should only be readable by the user who created it - 0600 permissions needed
					configHashFile.setAttributes(convertedPermissionValue);
				} catch (FileException e) {
					// filesystem error
					displayFileSystemErrorMessage(e.msg, thisFunctionName, configHashFile, FsErrorSeverity.warning);
				}
			}
			// Update 'sync_list' files
			if (exists(syncListFilePath)) {
				// update sync_list hash
				if (debugLogging) {addLogEntry("Updating sync_list hash", ["debug"]);}
				try {
					std.file.write(syncListHashFile, computeQuickXorHash(syncListFilePath));
					// Hash file should only be readable by the user who created it - 0600 permissions needed
					syncListHashFile.setAttributes(convertedPermissionValue);
				} catch (FileException e) {
					// filesystem error
					displayFileSystemErrorMessage(e.msg, thisFunctionName, syncListHashFile, FsErrorSeverity.warning);
				}
			}
		} else {
			// --dry-run scenario ... technically we should not be making any local file changes .......
			addLogEntry("DRY-RUN: Not updating hash files as --dry-run has been used");
		}
	}
	
	// Create any required hash files for files that help us determine if the configuration has changed since last run
	void createRequiredInitialConfigurationHashFiles() {
		// Set this function name
		string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({})));
	
		// Does a 'config' file exist with a valid hash file
		if (exists(applicableConfigFilePath)) {
			if (!exists(configHashFile)) {
				// no existing hash file exists
				try {
					std.file.write(configHashFile, "initial-hash");
					// Hash file should only be readable by the user who created it - 0600 permissions needed
					configHashFile.setAttributes(convertedPermissionValue);
				} catch (FileException e) {
					// filesystem error
					displayFileSystemErrorMessage(e.msg, thisFunctionName, configHashFile, FsErrorSeverity.warning);
				}
			}
			// Generate the runtime hash for the 'config' file
			currentConfigHash = computeQuickXorHash(applicableConfigFilePath);
		}
		
		// Does a 'sync_list' file exist with a valid hash file
		if (exists(syncListFilePath)) {
			if (!exists(syncListHashFile)) {
				// no existing hash file exists
				try {
					std.file.write(syncListHashFile, "initial-hash");
					// Hash file should only be readable by the user who created it - 0600 permissions needed
					syncListHashFile.setAttributes(convertedPermissionValue);
				} catch (FileException e) {
					// filesystem error
					displayFileSystemErrorMessage(e.msg, thisFunctionName, syncListHashFile, FsErrorSeverity.warning);
				}
			}
			// Generate the runtime hash for the 'sync_list' file
			currentSyncListHash = computeQuickXorHash(syncListFilePath);
		}
	}
	
	// Read in the text values of the previous configurations
	int readExistingConfigurationHashFiles() {
		if (exists(configHashFile)) {
			try {
				previousConfigHash = readText(configHashFile);
			} catch (std.file.FileException e) {
				// Unable to access required hash file
				addLogEntry("ERROR: Unable to access " ~ e.msg);
				// Use exit scopes to shutdown API
				return EXIT_FAILURE;
			}
		}
		
		if (exists(syncListHashFile)) {
			try {
				previousSyncListHash = readText(syncListHashFile);
			} catch (std.file.FileException e) {
				// Unable to access required hash file
				addLogEntry("ERROR: Unable to access " ~ e.msg);
				// Use exit scopes to shutdown API
				return EXIT_FAILURE;
			}
		}
		
		return 0;
	}
	
	// Check for basic option conflicts - flags that should not be used together and/or flag combinations that conflict with each other
	bool checkForBasicOptionConflicts() {
	
		bool operationalConflictDetected = false;
		
		// What are the permission that have been set for the application?
		// These are relevant for:
		// - The ~/OneDrive parent folder or 'sync_dir' configured item
		// - Any new folder created under ~/OneDrive or 'sync_dir'
		// - Any new file created under ~/OneDrive or 'sync_dir'
		// valid permissions are 000 -> 777 - anything else is invalid
		long syncDirPermissions = getValueLong("sync_dir_permissions");
		long syncFilePermissions = getValueLong("sync_file_permissions");
		bool invalidPermissions = false;
		
		// Check 'sync_dir_permissions'
		if (syncDirPermissions < 0 || syncDirPermissions > 777) {
			addLogEntry("ERROR: Invalid 'User|Group|Other' permissions set for 'sync_dir_permissions' within your config file. Please check your configuration");
			invalidPermissions = true;
		}
		
		// Check 'sync_file_permissions'
		if (syncFilePermissions < 0 || syncFilePermissions > 777) {
			addLogEntry("ERROR: Invalid 'User|Group|Other' permissions set for 'sync_file_permissions' within your config file. Please check your configuration");
			invalidPermissions = true;
		}
		
		// Invalid permissions detected?
		if (invalidPermissions) {
			operationalConflictDetected = true;
		} else {
			// Debug log output what permissions are being set to
			if (debugLogging) {addLogEntry("Configuring default new folder permissions as: " ~ to!string(getValueLong("sync_dir_permissions")), ["debug"]);}
			configureRequiredDirectoryPermissions();
			if (debugLogging) {addLogEntry("Configuring default new file permissions as: " ~ to!string(getValueLong("sync_file_permissions")), ["debug"]);}
			configureRequiredFilePermissions();
		}
		
		// --upload-only and --download-only cannot be used together
		if ((getValueBool("upload_only")) && (getValueBool("download_only"))) {
			addLogEntry("ERROR: --upload-only and --download-only cannot be used together. Use one, not both at the same time");
			operationalConflictDetected = true;
		}
		
		// --sync and --monitor cannot be used together
		if ((getValueBool("synchronize")) && (getValueBool("monitor"))) {
			addLogEntry("ERROR: --sync and --monitor cannot be used together. Only use one of these options, not both at the same time");
			operationalConflictDetected = true;
		}
		
		// --no-remote-delete can ONLY be enabled when --upload-only is used
		if ((getValueBool("no_remote_delete")) && (!getValueBool("upload_only"))) {
			addLogEntry("ERROR: --no-remote-delete can only be used with --upload-only");
			operationalConflictDetected = true;
		}
		
		// --remove-source-files can ONLY be enabled when --upload-only is used
		if ((getValueBool("remove_source_files")) && (!getValueBool("upload_only"))) {
			addLogEntry("ERROR: --remove-source-files can only be used with --upload-only");
			operationalConflictDetected = true;
		}
		
		// --cleanup-local-files can ONLY be enabled when --download-only is used
		if ((getValueBool("cleanup_local_files")) && (!getValueBool("download_only"))) {
			addLogEntry("ERROR: --cleanup-local-files can only be used with --download-only");
			operationalConflictDetected = true;
		}
		
		// --list-shared-folders cannot be used with --resync and/or --resync-auth
		if ((getValueBool("list_business_shared_items")) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) {
			addLogEntry("ERROR: --list-shared-items cannot be used with --resync or --resync-auth");
			operationalConflictDetected = true;
		}
		
		// --list-shared-folders cannot be used with --sync or --monitor
		if ((getValueBool("list_business_shared_items")) && ((getValueBool("synchronize")) || (getValueBool("monitor")))) {
			addLogEntry("ERROR: --list-shared-items cannot be used with --sync or --monitor");
			operationalConflictDetected = true;
		}
		
		// --sync-shared-files can ONLY be used with sync_business_shared_items
		if ((getValueBool("sync_business_shared_files")) && (!getValueBool("sync_business_shared_items"))) {
			addLogEntry("ERROR: The --sync-shared-files option can only be utilised if the 'sync_business_shared_items' configuration setting is enabled.");
			operationalConflictDetected = true;
		}
				
		// --display-sync-status cannot be used with --resync and/or --resync-auth
		if ((getValueBool("display_sync_status")) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) {
			addLogEntry("ERROR: --display-sync-status cannot be used with --resync or --resync-auth");
			operationalConflictDetected = true;
		}
		
		// --modified-by cannot be used with --resync and/or --resync-auth
		if ((!getValueString("modified_by").empty) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) {
			addLogEntry("ERROR: --modified-by cannot be used with --resync or --resync-auth");
			operationalConflictDetected = true;
		}
		
		// --get-file-link cannot be used with --resync and/or --resync-auth
		if ((!getValueString("get_file_link").empty) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) {
			addLogEntry("ERROR: --get-file-link cannot be used with --resync or --resync-auth");
			operationalConflictDetected = true;
		}
		
		// --create-share-link cannot be used with --resync and/or --resync-auth
		if ((!getValueString("create_share_link").empty) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) {
			addLogEntry("ERROR: --create-share-link cannot be used with --resync or --resync-auth");
			operationalConflictDetected = true;
		}
		
		// --get-sharepoint-drive-id cannot be used with --resync and/or --resync-auth
		if ((!getValueString("sharepoint_library_name").empty) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) {
			addLogEntry("ERROR: --get-sharepoint-drive-id cannot be used with --resync or --resync-auth");
			operationalConflictDetected = true;
		}
		
		// --monitor and --display-sync-status cannot be used together
		if ((getValueBool("monitor")) && (getValueBool("display_sync_status"))) {
			addLogEntry("ERROR: --monitor and --display-sync-status cannot be used together");
			operationalConflictDetected = true;
		}
		
		// --sync and --display-sync-status cannot be used together
		if ((getValueBool("synchronize")) && (getValueBool("display_sync_status"))) {
			addLogEntry("ERROR: --sync and --display-sync-status cannot be used together");
			operationalConflictDetected = true;
		}
		
		// --monitor and --display-quota cannot be used together
		if ((getValueBool("monitor")) && (getValueBool("display_quota"))) {
			addLogEntry("ERROR: --monitor and --display-quota cannot be used together");
			operationalConflictDetected = true;
		}
		
		// --sync and --display-quota cannot be used together
		if ((getValueBool("synchronize")) && (getValueBool("display_quota"))) {
			addLogEntry("ERROR: --sync and --display-quota cannot be used together");
			operationalConflictDetected = true;
		}
				
		// --force-sync can only be used when using --sync and --single-directory
		if (getValueBool("force_sync")) {
		
			bool conflict = false;
			// Should not be used with --monitor
			if (getValueBool("monitor")) conflict = true;
			// single_directory must not be empty
			if (getValueString("single_directory").empty) conflict = true;
			if (conflict) {
				addLogEntry("ERROR: --force-sync can only be used with --sync --single-directory");
				operationalConflictDetected = true;
			}
		}
		
		// When using 'azure_ad_endpoint', 'azure_tenant_id' cannot be empty
		if ((!getValueString("azure_ad_endpoint").empty) && (getValueString("azure_tenant_id").empty)) {
			addLogEntry("ERROR: config option 'azure_tenant_id' cannot be empty when 'azure_ad_endpoint' is configured");
			operationalConflictDetected = true;
		}
		
		// When using --enable-logging the 'log_dir' cannot be empty
		if ((getValueBool("enable_logging")) && (getValueString("log_dir").empty)) {
			addLogEntry("ERROR: config option 'log_dir' cannot be empty when 'enable_logging' is configured");
			operationalConflictDetected = true;
		}
		
		// When using --syncdir, the value cannot be empty.
		if (strip(getValueString("sync_dir")).empty) {
			addLogEntry("ERROR: --syncdir value cannot be empty");
			operationalConflictDetected = true;
		}
		
		// --monitor and --create-directory cannot be used together
		if ((getValueBool("monitor")) && (!getValueString("create_directory").empty)) {
			addLogEntry("ERROR: --monitor and --create-directory cannot be used together");
			operationalConflictDetected = true;
		}
		
		// --sync and --create-directory cannot be used together
		if ((getValueBool("synchronize")) && (!getValueString("create_directory").empty)) {
			addLogEntry("ERROR: --sync and --create-directory cannot be used together");
			operationalConflictDetected = true;
		}
		
		// --monitor and --remove-directory cannot be used together
		if ((getValueBool("monitor")) && (!getValueString("remove_directory").empty)) {
			addLogEntry("ERROR: --monitor and --remove-directory cannot be used together");
			operationalConflictDetected = true;
		}
		
		// --sync and --remove-directory cannot be used together
		if ((getValueBool("synchronize")) && (!getValueString("remove_directory").empty)) {
			addLogEntry("ERROR: --sync and --remove-directory cannot be used together");
			operationalConflictDetected = true;
		}
		
		// --monitor and --source-directory cannot be used together
		if ((getValueBool("monitor")) && (!getValueString("source_directory").empty)) {
			addLogEntry("ERROR: --monitor and --source-directory cannot be used together");
			operationalConflictDetected = true;
		}
		
		// --sync and --source-directory cannot be used together
		if ((getValueBool("synchronize")) && (!getValueString("source_directory").empty)) {
			addLogEntry("ERROR: --sync and --source-directory cannot be used together");
			operationalConflictDetected = true;
		}
		
		// --monitor and --destination-directory cannot be used together
		if ((getValueBool("monitor")) && (!getValueString("destination_directory").empty)) {
			addLogEntry("ERROR: --monitor and --destination-directory cannot be used together");
			operationalConflictDetected = true;
		}
		
		// --sync and --destination-directory cannot be used together
		if ((getValueBool("synchronize")) && (!getValueString("destination_directory").empty)) {
			addLogEntry("ERROR: --sync and --destination-directory cannot be used together");
			operationalConflictDetected = true;
		}
		
		// --download-only and --local-first cannot be used together
		if ((getValueBool("download_only")) && (getValueBool("local_first"))) {
			addLogEntry("ERROR: --download-only cannot be used with --local-first");
			operationalConflictDetected = true;
		}
		
		// Test that '--modified-by <arg>' has a valid argument and not another directive
		if (getValueString("modified_by") != "") {
			// Does the string start with '--' ?
			if (getValueString("modified_by").startsWith("--")) {
				addLogEntry("ERROR: --modified-by missing a valid entry");
				operationalConflictDetected = true;
			}
		}
		
		// Test that '--get-file-link <arg>' has a valid argument and not another directive
		if (getValueString("get_file_link") != "") {
			// Does the string start with '--' ?
			if (getValueString("get_file_link").startsWith("--")) {
				addLogEntry("ERROR: --get-file-link missing a valid entry");
				operationalConflictDetected = true;
			}
		}
		
		// Test that '--create-share-link <arg>' has a valid argument and not another directive
		if (getValueString("create_share_link") != "") {
			// Does the string start with '--' ?
			if (getValueString("create_share_link").startsWith("--")) {
				addLogEntry("ERROR: --create-share-link missing a valid entry");
				operationalConflictDetected = true;
			}
		}
		
		// Test that '--create-directory <arg>' has a valid argument and not another directive
		if (getValueString("create_directory") != "") {
			// Does the string start with '--' ?
			if (getValueString("create_directory").startsWith("--")) {
				addLogEntry("ERROR: --create-directory missing a valid entry");
				operationalConflictDetected = true;
			}
		}
		
		// Test that '--remove-directory <arg>' has a valid argument and not another directive
		if (getValueString("remove_directory") != "") {
			// Does the string start with '--' ?
			if (getValueString("remove_directory").startsWith("--")) {
				addLogEntry("ERROR: --remove-directory missing a valid entry");
				operationalConflictDetected = true;
			}
		}
		
		// Test that '--source-directory <arg>' has a valid argument and not another directive
		if (getValueString("source_directory") != "") {
			// Does the string start with '--' ?
			if (getValueString("source_directory").startsWith("--")) {
				addLogEntry("ERROR: --source-directory missing a valid entry");
				operationalConflictDetected = true;
			}
		}
		
		// Test that '--destination-directory <arg>' has a valid argument and not another directive
		if (getValueString("destination_directory") != "") {
			// Does the string start with '--' ?
			if (getValueString("destination_directory").startsWith("--")) {
				addLogEntry("ERROR: --destination-directory missing a valid entry");
				operationalConflictDetected = true;
			}
		}
		
		// 'use_intune_sso' and 'use_device_auth' cannot be used together
		if ((getValueBool("use_intune_sso")) && (getValueBool("use_device_auth"))) {
			addLogEntry("ERROR: 'use_intune_sso' and 'use_device_auth' cannot be used together");
			operationalConflictDetected = true;
		}
		
		// --force and --resync cannot be used together as --resync blows away the database, thus there is no way to calculate large local deletes
		if ((getValueBool("force")) && (getValueBool("resync"))) {
			addLogEntry("ERROR: --force and --resync cannot be used together as there is zero way to determine that a big delete has occurred");
			operationalConflictDetected = true;
		}
				
		// Return bool value indicating if we have an operational conflict
		return operationalConflictDetected;
	}
	
	// Reset skip_file and skip_dir to application defaults when --force-sync is used
	void resetSkipToDefaults() {
		// skip_file
		if (debugLogging) {
			addLogEntry("original skip_file: " ~ getValueString("skip_file"), ["debug"]);
			addLogEntry("resetting skip_file to application defaults", ["debug"]);
		}
		setValueString("skip_file", defaultSkipFile);
		if (debugLogging) {addLogEntry("reset skip_file: " ~ getValueString("skip_file"), ["debug"]);}
		
		// skip_dir
		if (debugLogging) {
			addLogEntry("original skip_dir: " ~ getValueString("skip_dir"), ["debug"]);
			addLogEntry("resetting skip_dir to application defaults", ["debug"]);
		}
		setValueString("skip_dir", defaultSkipDir);
		if (debugLogging) {addLogEntry("reset skip_dir: " ~ getValueString("skip_dir"), ["debug"]);}
	}
	
	// Initialise the correct 'sync_dir' expanding any '~' if present
	string initialiseRuntimeSyncDirectory() {
		// Log what we are doing
		if (debugLogging) {addLogEntry("sync_dir: Setting runtimeSyncDirectory from config value 'sync_dir'", ["debug"]);}
		
		if (!shellEnvironmentSet){
			if (debugLogging) {addLogEntry("sync_dir: No SHELL or USER environment variable configuration detected", ["debug"]);}
			
			// No shell or user set, so expandTilde() will fail - usually headless system running under init.d / systemd or potentially Docker
			// Does the 'currently configured' sync_dir include a ~
			if (canFind(getValueString("sync_dir"), "~")) {
				// A ~ was found in sync_dir
				if (debugLogging) {addLogEntry("sync_dir: A '~' was found in 'sync_dir', using the calculated 'homePath' to replace '~' as no SHELL or USER environment variable set", ["debug"]);}
				runtimeSyncDirectory = buildNormalizedPath(buildPath(defaultHomePath, strip(getValueString("sync_dir"), "~")));
			} else {
				// No ~ found in sync_dir, use as is
				if (debugLogging) {addLogEntry("sync_dir: Using configured 'sync_dir' path as-is as no SHELL or USER environment variable configuration detected", ["debug"]);}
				runtimeSyncDirectory = getValueString("sync_dir");
			}
		} else {
			// A shell and user environment variable is set, expand any ~ as this will be expanded correctly if present
			if (canFind(getValueString("sync_dir"), "~")) {
				if (debugLogging) {addLogEntry("sync_dir: A '~' was found in the configured 'sync_dir', automatically expanding as SHELL and USER environment variable is set", ["debug"]);}
				runtimeSyncDirectory = expandTilde(getValueString("sync_dir"));
			} else {
				// No ~ found in sync_dir, does the path begin with a '/' ?
				if (debugLogging) {addLogEntry("sync_dir: Using configured 'sync_dir' path as-is as however SHELL or USER environment variable configuration detected - should be placed in USER home directory", ["debug"]);}
				if (!startsWith(getValueString("sync_dir"), "/")) {
					if (debugLogging) {addLogEntry("Configured 'sync_dir' does not start with a '/' or '~/' - adjusting configured 'sync_dir' to use User Home Directory as base for 'sync_dir' path", ["debug"]);}
					string updatedPathWithHome = "~/" ~ getValueString("sync_dir");
					runtimeSyncDirectory = expandTilde(updatedPathWithHome);
				} else {
					if (debugLogging) {addLogEntry("use 'sync_dir' as is - no touch", ["debug"]);}
					runtimeSyncDirectory = getValueString("sync_dir");
				}
			}
		}
		
		// What will runtimeSyncDirectory be actually set to?
		if (debugLogging) {addLogEntry("sync_dir: runtimeSyncDirectory set to: " ~ runtimeSyncDirectory, ["debug"]);}
		
		// Configure configuredBusinessSharedFilesDirectoryName
		configuredBusinessSharedFilesDirectoryName = buildNormalizedPath(buildPath(runtimeSyncDirectory, defaultBusinessSharedFilesDirectoryName));
		
		return runtimeSyncDirectory;
	}
	
	// Initialise the correct 'log_dir' when application logging to a separate file is enabled with 'enable_logging' and expanding any '~' if present
	string calculateLogDirectory() {
		string configuredLogDirPath;

		if (debugLogging) {addLogEntry("log_dir: Setting runtime application log from config value 'log_dir'", ["debug"]);}

		if (getValueString("log_dir") != defaultLogFileDir) {
			// User modified 'log_dir' to be used with 'enable_logging'
			// if 'log_dir' contains a '~' this needs to be expanded correctly
			if (canFind(getValueString("log_dir"), "~")) {
				// ~ needs to be expanded correctly
				if (!shellEnvironmentSet) {
					// No shell or user environment variable set, so expandTilde() will fail - usually headless system running under init.d / systemd or potentially Docker
					if (debugLogging) {addLogEntry("log_dir: A '~' was found in 'log_dir' however '~' as no SHELL or USER environment variable set", ["debug"]);}
					configuredLogDirPath = buildNormalizedPath(buildPath(defaultHomePath, strip(getValueString("log_dir"), "~")));
				} else {
					// We have a SHELL or USER environment variable set, so expandTilde() should work correctly
					if (debugLogging) {addLogEntry("log_dir: A '~' was found in the configured 'log_dir', automatically expanding as SHELL and USER environment variable is set", ["debug"]);}
					configuredLogDirPath = buildNormalizedPath(expandTilde(getValueString("log_dir")));
				}
			} else {
				// No '~' present - use as-is, but normalise
				configuredLogDirPath = buildNormalizedPath(getValueString("log_dir"));
			}
		} else {
			// Default 'log_dir' to be used with 'enable_logging'
			configuredLogDirPath = defaultLogFileDir;
		}

		// Attempt to create 'configuredLogDirPath' if this does not exist, otherwise we need to fall back to the users home directory
		if (!exists(configuredLogDirPath)) {
			// 'configuredLogDirPath' path does not exist - try and create it
			try {
				mkdirRecurse(configuredLogDirPath);
			} catch (std.file.FileException e) {
				// We got an error when attempting to create the directory ..
				addLogEntry();
				addLogEntry("ERROR: Unable to create " ~ configuredLogDirPath);
				addLogEntry("ERROR: Please manually create '" ~ configuredLogDirPath ~ "' and ensure that the permissions allow write access for your user to this location.");
				addLogEntry("ERROR: The requested client activity log will instead be located in your users home directory");
				addLogEntry();

				// Reconfigure 'configuredLogDirPath' to use environment.get("HOME") value, which we have already calculated
				configuredLogDirPath = defaultHomePath;
			}
		}

		// Verify that we can actually write in configuredLogDirPath
		// If we cannot, fall back to the user's home directory instead of later crashing
		try {
			auto testFile = buildNormalizedPath(buildPath(configuredLogDirPath, ".onedrive_log_write_test"));
			// Try to append a zero-length string – this will create the file if possible
			std.file.append(testFile, "");
			// Clean up the test file
			std.file.remove(testFile);
		} catch (std.file.FileException e) {
			addLogEntry();
			addLogEntry("ERROR: Unable to write to " ~ configuredLogDirPath);
			addLogEntry("ERROR: Please manually adjust permissions or choose a different 'log_dir' in the configuration file.");
			addLogEntry("ERROR: The requested client activity log will instead be located in your users home directory");
			addLogEntry();

			// Reconfigure 'configuredLogDirPath' to use environment.get("HOME") value, which we have already calculated
			configuredLogDirPath = defaultHomePath;
		}

		// Return the initialised application log path
		return configuredLogDirPath;
	}

	// What IP protocol is going to be used to access Microsoft OneDrive
	void displayIPProtocol() {
		if (getValueLong("ip_protocol_version") == 0) addLogEntry("Using IPv4 and IPv6 (if configured) for all network operations");
		if (getValueLong("ip_protocol_version") == 1) addLogEntry("Forcing client to use IPv4 connections only");
		if (getValueLong("ip_protocol_version") == 2) addLogEntry("Forcing client to use IPv6 connections only");
	}
	
	// Has a 'no-sync' task been requested?
	bool hasNoSyncOperationBeenRequested() {
		// Are we performing some sort of 'no-sync' task?
		// - Are we performing a logout?
		// - Are we performing a reauth?
		// - 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?
		// - Are we downloading a single file?
		bool noSyncOperation = false;
		
		// Return a true|false if any of these have been set, so that we use the 'dry-run' DB copy, to execute these tasks, in case the client is currently operational
		
		// --logout
		if (getValueBool("logout")) {
			// flag that a no sync operation has been requested
			noSyncOperation = true;
		}
		
		// --reauth
		if (getValueBool("reauth")) {
			// flag that a no sync operation has been requested
			noSyncOperation = true;
		}
		
		// --get-sharepoint-drive-id - Get the SharePoint Library drive_id
		if (getValueString("sharepoint_library_name") != "") {
			// flag that a no sync operation has been requested
			noSyncOperation = true;
		}
		
		// --display-sync-status - Query the sync status
		if (getValueBool("display_sync_status")) {
			// flag that a no sync operation has been requested
			noSyncOperation = true;
		}
		
		// --get-file-link - Get the URL path for a synced file?
		if (getValueString("get_file_link") != "") {
			// flag that a no sync operation has been requested
			noSyncOperation = true;
		}
		
		// --modified-by - Are we listing the modified-by details of a provided path?
		if (getValueString("modified_by") != "") {
			// flag that a no sync operation has been requested
			noSyncOperation = true;
		}
		
		// --list-shared-items - Are we listing OneDrive Business Shared Items
		if (getValueBool("list_business_shared_items")) {
			// flag that a no sync operation has been requested
			noSyncOperation = true;
		}
		
		// --create-share-link - Are we creating a shareable link for an existing file on OneDrive?
		if (getValueString("create_share_link") != "") {
			// flag that a no sync operation has been requested
			noSyncOperation = true;
		}
		
		// --create-directory - Are we just creating a directory online, without any sync being performed?
		if ((getValueString("create_directory") != "")) {
			// flag that a no sync operation has been requested
			noSyncOperation = true;
		}
		
		// --remove-directory - Are we just deleting a directory online, without any sync being performed?
		if ((getValueString("remove_directory") != "")) {
			// flag that a no sync operation has been requested
			noSyncOperation = true;
		}
		
		// Are we renaming or moving a directory online?
		// 	onedrive --source-directory 'path/as/source/' --destination-directory 'path/as/destination'
		if ((getValueString("source_directory") != "") && (getValueString("destination_directory") != "")) {
			// flag that a no sync operation has been requested
			noSyncOperation = true;
		}
		
		// Are we displaying the quota information?
		if (getValueBool("display_quota")) {
			// flag that a no sync operation has been requested
			noSyncOperation = true;
		}
		
		// Are we downloading a single file?
		if ((getValueString("download_single_file") != "")) {
			// flag that a no sync operation has been requested
			noSyncOperation = true;
		}
		
		// Return result
		return noSyncOperation;
	}
	
	// Are the required GUI logging environment variables for this user available?
	// Specifically these must be available:
	// - XDG_RUNTIME_DIR
	// - DBUS_SESSION_BUS_ADDRESS
	bool validateGUINotificationEnvironmentVariables() {
	
		bool variablesAvailable = false;
		string xdg_value;
		string dbus_value;
		
		version(Notifications) {
			// Check XDG_RUNTIME_DIR environment variable
			try {
				xdg_value = environment["XDG_RUNTIME_DIR"];
				xdg_exists = true;
			} catch (Exception e) {
				xdg_exists = false;
			}
			
			// Check DBUS_SESSION_BUS_ADDRESS environment variable
			try {
				dbus_value = environment["DBUS_SESSION_BUS_ADDRESS"];
				dbus_exists = true;
			} catch (Exception e) {
				dbus_exists = false;
			}
			
			// Output the result
			if (xdg_exists) {
				if (debugLogging) {addLogEntry("runtime_environment: XDG_RUNTIME_DIR exists with value: " ~ xdg_value , ["debug"]);}
			} else {
				if (debugLogging) {addLogEntry("runtime_environment: XDG_RUNTIME_DIR missing from runtime user environment", ["debug"]);}
			}
			
			if (dbus_exists) {
				if (debugLogging) {addLogEntry("runtime_environment: DBUS_SESSION_BUS_ADDRESS exists with value: " ~ dbus_value, ["debug"]);}
			} else {
				if (debugLogging) {addLogEntry("runtime_environment: DBUS_SESSION_BUS_ADDRESS missing from runtime user environment", ["debug"]);}
			}

			// Determine result
			if (xdg_exists && dbus_exists) {
				variablesAvailable = true;
			} else {
				addLogEntry("WARNING: Required environment variables required to enable GUI Notifications are not present");
				variablesAvailable = false;
			}	
		}

		// Return result
		return variablesAvailable;
	}
	
	// Set the Recycle Bin Paths
	void setRecycleBinPaths() {
		string configured = getValueString("recycle_bin_path");
		string basePath;
		string dirSeparatorString = "/";

		// Handle the "no shell / no user" case similarly to sync_dir
		if (!shellEnvironmentSet) {
			// No SHELL or USER means expandTilde() will fail if '~' is present
			if (canFind(configured, "~")) {
				// Replace '~' with defaultHomePath explicitly
				basePath = buildNormalizedPath(
					buildPath(defaultHomePath, strip(configured, "~"))
				);
			} else {
				basePath = configured;
			}
		} else {
			// Normal case: shell + user are set; we can rely on expandTilde()
			if (canFind(configured, "~")) {
				basePath = expandTilde(configured);
			} else {
				basePath = configured;
			}
		}

		// Make sure it's normalised and has a trailing '/'
		basePath = buildNormalizedPath(basePath);
		if (!basePath.endsWith(dirSeparatorString)) {
			basePath ~= dirSeparatorString;
		}
		
		// Update Recycle Bin paths
		recycleBinParentPath = basePath;
		recycleBinFilePath = basePath ~ "files" ~ dirSeparatorString;
		recycleBinInfoPath = basePath ~ "info"  ~ dirSeparatorString;
	}
	
	// Is 'recycleBinParentPath' a child path of the configured 'runtimeSyncDirectory'?
	bool checkRecycleBinPathAsChildOfSyncDir() {
		// Configure the variables to check
		string syncRoot = runtimeSyncDirectory;
		string recycleBin = recycleBinParentPath;
		string sep = "/";
		
		// Make prefix check robust – ensure syncRoot ends with separator
		if (!syncRoot.endsWith(sep)) {
			syncRoot ~= sep;
		}
		
		// Make prefix check robust – ensure recycleBin ends with separator
		if (!recycleBin.endsWith(sep)) {
			recycleBin ~= sep;
		}
		
		// Perform the check and return the evaluation
		return startsWith(recycleBin, syncRoot);
	}
	
	// Is the client running under a GUI session?
	// - GNOME
	// - KDE
	bool isGuiSessionDetected() {
	
		bool hasDisplay = false;
		bool hasRuntime = false;
		bool uidMatches = false;
		bool homeOK = false;
		string xdgType;

		try {
			xdgType = environment.get("XDG_SESSION_TYPE", "");
		} catch (Exception e) {
			xdgType = "";
		}
		
		try {
			hasDisplay = environment.get("WAYLAND_DISPLAY", "").length > 0 || environment.get("DISPLAY", "").length > 0;
		} catch (Exception e) {
			hasDisplay = false;
		}
		
		try {
			hasRuntime = environment.get("XDG_RUNTIME_DIR", "").length > 0;
		} catch (Exception e) {
			hasRuntime = false;
		}
		
		try {
			uidMatches = (geteuid() == getuid());
		} catch (Exception e) {
			uidMatches = false;
		}
		
		try {
			homeOK = environment.get("HOME", "").length > 0;
		} catch (Exception e) {
			homeOK = false;
		}
		
		bool hasGuiElements = hasDisplay || (xdgType == "wayland" || xdgType == "x11");
		
		return hasGuiElements && hasRuntime && uidMatches && homeOK;
	}
	
	// Attempt to detect the running display manager
	DesktopHints detectDesktop() {
		string all = (  environment.get("XDG_CURRENT_DESKTOP","") ~ ":" ~
						environment.get("XDG_SESSION_DESKTOP","") ~ ":" ~
						environment.get("DESKTOP_SESSION","") ~ ":" ~
						environment.get("GDMSESSION","") ~ ":" ~
						environment.get("KDE_FULL_SESSION","")).toLower();
		
		DesktopHints hints;
		hints.gnome = all.canFind("gnome");
		hints.kde   = all.canFind("kde") || all.canFind("plasma");
		return hints;
	}
	
	// Generate the correct file:// URI for display manager integration
	string fileUriFor(string absPath) {
		// Basic, safe URI for local file
		return "file://" ~ expandTilde(absPath);
	}
	
	// Add GNOME Bookmark
	void addGnomeBookmark() {
		// Configure required variables
		string uri = fileUriFor(getValueString("sync_dir"));
		string bookmarksPath = buildPath(expandTilde(environment.get("HOME", "")), ".config", "gtk-3.0", "bookmarks");
		
		// Ensure the bookmarks path exists
		try {
			// Attempt bookmarks path creation
			mkdirRecurse(dirName(bookmarksPath));
		} catch (std.file.FileException e) {
			// Creating the bookmarks path failed
			addLogEntry("ERROR: Unable to create the GNOME bookmark directory: " ~ e.msg, ["info", "notify"]);
			return;
		}
		
		// Does the bookmark already exist?
		string content = exists(bookmarksPath) ? readText(bookmarksPath) : "";
		bool present = false;
		foreach (line; content.splitLines()) {
			if (line.strip == uri) { present = true; break; }
		}
		if (present) return;
		
		// Append newline if needed, then our URI
		string newline = content.length && !content.endsWith("\n") ? "\n" : "";
		string updated = content ~ newline ~ uri ~ "\n";

		// Atomic write
		string tmp = bookmarksPath ~ ".tmp";
		std.file.write(tmp, updated);
		rename(tmp, bookmarksPath);
		
		// Log outcome
		addLogEntry("GNOME Desktop Integration: Bookmark added successfully", ["info"]);
	}
	
	// Set the correct folder icon for the 'sync_dir' path
	void setOneDriveFolderIcon() {
		// Get the sync directory
		string syncDir = expandTilde(getValueString("sync_dir"));
	
		// Build gio command
		string[] gioCmd = [
			"gio",
			"set",
			syncDir,
			"metadata::custom-icon-name",
			"onedrive"
		];
		
		// Try and set folder icon
		try {
			auto p = spawnProcess(gioCmd);
			int status = p.wait();
			if (status == 0) {
				addLogEntry("GNOME Desktop Integration: Set folder icon to 'onedrive' for " ~ syncDir, ["info"]);
			} else {
				addLogEntry("GNOME Desktop Integration: Failed to set folder icon for " ~ syncDir ~ " (gio exit " ~ status.to!string ~ ")", ["info"]);
			}
		} catch (Exception e) {
			addLogEntry("GNOME Desktop Integration: Exception setting folder icon: " ~ e.msg, ["info"]);
		}
	}
	
	// Remove GNOME Bookmark
	void removeGnomeBookmark() {
		// Configure required variables
		string uri = fileUriFor(getValueString("sync_dir"));
		string bookmarksPath = buildPath(expandTilde(environment.get("HOME", "")), ".config", "gtk-3.0", "bookmarks");

		// Does the bookmark path exist?
		if (!exists(bookmarksPath)) {
			return;
		}
		
		// Read existing bookmarks
		string content = readText(bookmarksPath);
		auto lines = content.splitLines();

		bool changed = false;
		string[] kept;
		kept.reserve(lines.length);

		foreach (line; lines) {
			// Remove every line that exactly matches the URI (after stripping whitespace)
			if (line.strip == uri) {
				changed = true;
				continue;
			}
			kept ~= line;
		}

		if (!changed) {
			return;
		}

		// Rebuild file (ensure trailing newline if non-empty)
		string updated = kept.length ? kept.join("\n") ~ "\n" : "";

		// Atomic write
		const string tmp = bookmarksPath ~ ".tmp";
		std.file.write(tmp, updated);
		rename(tmp, bookmarksPath);

		// Log outcome
		addLogEntry("GNOME Desktop Integration: Bookmark removed successfully", ["info"]);
	}
	
	// Remove folder icon
	void removeOneDriveFolderIcon() {
		// Get the sync directory
		string syncDir = expandTilde(getValueString("sync_dir"));
	
		// Build gio command
		string[] gioCmd = [
			"gio",
			"set",
			syncDir,
			"metadata::custom-icon-name",
			"folder"
		];
		
		// Try and set folder icon
		try {
			auto p = spawnProcess(gioCmd);
			int status = p.wait();
			if (status == 0) {
				addLogEntry("GNOME Desktop Integration: Reset folder icon to 'default' for " ~ syncDir, ["info"]);
			} else {
				addLogEntry("GNOME Desktop Integration: Failed to reset folder icon for " ~ syncDir ~ " (gio exit " ~ status.to!string ~ ")", ["info"]);
			}
		} catch (Exception e) {
			addLogEntry("GNOME Desktop Integration: Exception setting folder icon: " ~ e.msg, ["info"]);
		}
	}
	
	// Add KDE Places entry
	void addKDEPlacesEntry() {
		// Configure required variables
		string uri = fileUriFor(getValueString("sync_dir"));
		string xbelPath = buildPath(expandTilde(environment.get("HOME", "")), ".local", "share", "user-places.xbel");
		string content;
		
		// Ensure the xbelPath path exists
		try {
			// Attempt xbelPath creation
			mkdirRecurse(dirName(xbelPath));
		} catch (std.file.FileException e) {
			// Creating the xbelPath path failed
			addLogEntry("ERROR: Unable to create the KDE Places directory: " ~ e.msg, ["info", "notify"]);
			return;
		}
		
		// Does the xbel file exist?		
		if (exists(xbelPath)) {
			// Path exists - read the file
			content = readText(xbelPath);
			
			// Does the 'sync_dir' path exist in the xbel file?
			if (content.canFind(`href="` ~ uri ~ `"`)) {
				return; // already present
			}
		} else {
			// xbel path does not exist, create minimal XBEL skeleton
			content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
					   <xbel version=\"1.0\">
					   </xbel>";
		}

		// Insert xbel bookmark before closing tag
		string bookmark = ` <bookmark href="` ~ uri ~ `">
		<title>OneDrive</title>
		<info>
		  <metadata owner="http://www.freedesktop.org">
			<bookmark:icon name="onedrive"/>
		  </metadata>
		</info>
	  </bookmark>`;
		
		// Update xbel file with Microsoft OneDrive Bookmark
		string updated;
		auto idx = content.lastIndexOf("</xbel>");
		if (idx >= 0) {
			updated = content[0 .. idx] ~ bookmark ~ "\n" ~ content[idx .. $];
		} else {
			// Fallback: append (still valid for many parsers)
			updated = content ~ "\n" ~ bookmark ~ "\n</xbel>\n";
		}

		string tmp = xbelPath ~ ".tmp";
		std.file.write(tmp, updated);
		rename(tmp, xbelPath);
		
		// Log outcome
		addLogEntry("KDE Desktop Integration: KDE/Plasma place added successfully", ["info"]);
	}
		
	// Remove KDE Places entry
	void removeKDEPlacesEntry() {
		// Compute paths/values
		const string uri = fileUriFor(getValueString("sync_dir")); 
		const string xbelPath = buildPath(expandTilde(environment.get("HOME", "")), ".local", "share", "user-places.xbel");

		if (!exists(xbelPath)) {
			return;
		}

		string content = readText(xbelPath);
		auto before = content;

		// Build a regex that matches:
		// <bookmark ... href="URI" ...> ... </bookmark>
		// - tolerate attribute order/whitespace
		// - accept single or double quotes around URI
		// - non-greedy body match
		const esc = regexEscape(uri);
		auto re = regex(`(?s)<bookmark\b[^>]*\bhref\s*=\s*["']` ~ esc ~ `["'][^>]*>.*?</bookmark\s*>`);

		// Remove all matches
		content = replaceAll(content, re, "");

		// Optional: tidy up multiple blank lines left behind
		auto cleanup = regex(`\n{3,}`);
		content = replaceAll(content, cleanup, "\n\n");

		// If nothing changed, exit quietly
		if (content == before) {
			return;
		}

		// Atomic write
		string tmp = xbelPath ~ ".tmp";
		std.file.write(tmp, content);
		rename(tmp, xbelPath);

		addLogEntry("KDE Desktop Integration: KDE/Plasma place removed successfully", ["info"]);
	}
	
	// Safely merge multiple '|'-delimited rule strings by normalising, removing empty entries, trimming whitespace, 
	// and de-duplicating rules so malformed or repeated config entries do not corrupt 'skip_dir' / 'skip_file' processing
	private string mergePipeDelimitedRulesDedup(const string existing, const string incoming) {
		auto resultString = appender!(string[])();

		void addTokens(const string raw) {
			foreach (part; raw.splitter('|')) {
				auto t = part.strip();
				if (t.empty) continue;
				// Keep first occurrence
				if (!resultString.data.canFind(t)) resultString ~= t.idup;
			}
		}

		addTokens(existing);
		addTokens(incoming);

		return resultString.data.joiner("|").to!string;
	}
}

// Output the full application help when --help is passed in
void outputLongHelp(Option[] opt) {
		auto argsNeedingOptions = [
			"--auth-files",
			"--auth-response",
			"--confdir",
			"--create-directory",
			"--classify-as-big-delete",
			"--create-share-link",
			"--destination-directory",
			"--download-file",
			"--get-file-link",
			"--get-O365-drive-id",
			"--get-sharepoint-drive-id",
			"--log-dir",
			"--min-notify-changes",
			"--modified-by",
			"--monitor-interval",
			"--monitor-log-frequency",
			"--monitor-fullscan-frequency",
			"--remove-directory",
			"--single-directory",
			"--skip-dir",
			"--skip-file",
			"--skip-size",
			"--source-directory",
			"--space-reservation",
			"--syncdir",
			"--share-password",
			"--user-agent" ];
		writeln(`onedrive - A client for the Microsoft OneDrive Cloud Service

  Usage:
    onedrive [options] --sync
      Do a one-time synchronisation with Microsoft OneDrive
    onedrive [options] --monitor
      Monitor filesystem and synchronise regularly with Microsoft OneDrive
    onedrive [options] --display-config
      Display the currently used configuration
    onedrive [options] --display-sync-status
      Query OneDrive service and report on pending changes
    onedrive -h | --help
      Show this help screen
    onedrive --version
      Show version

  Options:
	`);
		foreach (it; opt.sort!("a.optLong < b.optLong")) {
			writefln("  %s%s%s%s\n      %s",
					it.optLong,
					it.optShort == "" ? "" : " " ~ it.optShort,
					argsNeedingOptions.canFind(it.optLong) ? " '<path or required value>'" : "",
					it.required ? " (required)" : "", it.help);
		}
		// end with a blank line
		writeln();
}
