// Copyright (C) 2026, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Duplicati.Library.AutoUpdater;
using Duplicati.Library.Certificates;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Crashlog;
using Duplicati.Library.Encryption;
using Duplicati.Library.Interface;
using Duplicati.Library.Logging;
using Duplicati.Library.Main;
using Duplicati.Library.Main.Database;
using Duplicati.Library.Utility;
using Duplicati.Server.Database;
using Duplicati.WebserverCore;
using Duplicati.WebserverCore.Abstractions;
using Duplicati.WebserverCore.Services;
using Microsoft.Extensions.DependencyInjection;

namespace Duplicati.Server
{
    public class Program
    {
        /// <summary>The commandline argument name for parameters file.</summary>
        private const string PARAMETERS_FILE_OPTION = "parameters-file";
        /// <summary>Alternative names for the parameters file option.</summary>
        private static readonly string[] PARAMETERS_FILE_OPTION_EXTRAS = ["parameterfile"];
        /// <summary>The commandline argument name for ping-pong keepalive.</summary>
        private const string PING_PONG_KEEPALIVE_OPTION = "ping-pong-keepalive";
        /// <summary>The commandline argument name for Windows event log.</summary>
        private const string WINDOWS_EVENTLOG_OPTION = "windows-eventlog";
        /// <summary>The commandline argument name for Windows event log level.</summary>
        private const string WINDOWS_EVENTLOG_LEVEL_OPTION = "windows-eventlog-level";
        /// <summary>The commandline argument name for disabling database encryption.</summary>
        private const string DISABLE_DB_ENCRYPTION_OPTION = "disable-db-encryption";
        /// <summary>The commandline argument name for requiring database encryption key.</summary>
        private const string REQUIRE_DB_ENCRYPTION_KEY_OPTION = "require-db-encryption-key";
        /// <summary>The commandline argument name for settings encryption key.</summary>
        private const string SETTINGS_ENCRYPTION_KEY_OPTION = "settings-encryption-key";
        /// <summary>The commandline argument name for disabling update check.</summary>
        private const string DISABLE_UPDATE_CHECK_OPTION = "disable-update-check";
        /// <summary>The commandline argument name for registering remote control.</summary>
        private const string REGISTER_REMOTE_CONTROL_OPTION = "register-remote-control";
        /// <summary>The commandline argument name for forcing remote control reregistration.</summary>
        private const string REGISTER_REMOTE_CONTROL_REREGISTER_OPTION = "register-remote-control-force";
        /// <summary>The commandline argument name for allowed backend modules.</summary>
        private const string ALLOWED_BACKEND_MODULES = "allowed-backend-modules";
        /// <summary>The commandline argument name for allowed encryption modules.</summary>
        private const string ALLOWED_ENCRYPTION_MODULES = "allowed-encryption-modules";
        /// <summary>The commandline argument name for allowed compression modules.</summary>
        private const string ALLOWED_COMPRESSION_MODULES = "allowed-compression-modules";
        /// <summary>The commandline argument name for log file.</summary>
        private const string LOG_FILE_OPTION = "log-file";
        /// <summary> Notification action for CA key stored in unencrypted database.</summary>
        private const string NOTIFICATION_CA_KEY_UNENCRYPTED_DATABASE = "config:issue:ca-key-unencrypted-database";
        /// <summary>Notification action for certificate renewal failure.</summary>
        private const string NOTIFICATION_CERTIFICATE_RENEWAL_FAILED = "config:issue:certificate-renewal-failed";
        /// <summary>The commandline argument name for log level.</summary>
        private const string LOG_LEVEL_OPTION = "log-level";
        /// <summary>The commandline argument name for log console.</summary>
        private const string LOG_CONSOLE_OPTION = "log-console";
        /// <summary>The commandline argument name for temp directory.</summary>
        private const string TEMPDIR_OPTION = "tempdir";
        /// <summary>The commandline argument name for log retention.</summary>
        private const string LOG_RETENTION_OPTION = "log-retention";
        /// <summary>The commandline argument name for help.</summary>
        private const string HELP_OPTION = "help";

        /// <summary>
        /// The name used in the secret provider for the database encryption key
        /// </summary>
        private const string DATABASE_SECRET_VALUE_NAME = "duplicati-server-encryption-key";

#if DEBUG
        private const bool DEBUG_MODE = true;
#else
        private const bool DEBUG_MODE = false;
#endif

        /// <summary>
        /// Options to be ignored when validating the command line
        /// </summary>
        public static readonly HashSet<string> ValidationIgnoredOptions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        /// <summary>
        /// The log tag for messages from this class
        /// </summary>
        private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<Program>();
        /// <summary>
        /// The path to the directory that contains the main executable
        /// </summary>
        public static readonly string StartupPath = Duplicati.Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR;

        /// <summary>
        /// The environment variable prefix
        /// </summary>
        private static readonly string ENV_NAME_PREFIX = AutoUpdateSettings.AppName.ToUpperInvariant();

        /// <summary>
        /// The single instance
        /// </summary>
        public static SingleInstance ApplicationInstance = null;

        /// <summary>
        /// The thread running the ping-pong handler
        /// </summary>
        private static Thread PingPongThread;

        /// <summary>
        /// The controller interface for pause/resume and throttle options
        /// </summary>
        public static LiveControls LiveControl { get => DuplicatiWebserver.Provider.GetRequiredService<LiveControls>(); }

        /// <summary>
        /// Duplicati webserver instance
        /// </summary>
        public static DuplicatiWebserver DuplicatiWebserver { get; set; }

        /// <summary>
        /// The default secret provider
        /// </summary>
        public static ISecretProvider DefaultSecretProvider { get; set; }

        /// <summary>
        /// Callback to shutdown the modern webserver
        /// </summary>
        private static void ShutdownModernWebserver()
        {
            DuplicatiWebserver.Stop().GetAwaiter().GetResult();
        }

        /// <summary>
        /// An event that is set once the server is ready to respond to requests
        /// </summary>
        public static readonly ManualResetEvent ServerStartedEvent = new ManualResetEvent(false);

        /// <summary>
        /// Timer for purging temp files and log data
        /// </summary>
        private static System.Threading.Timer PurgeTempFilesTimer = null;

        /// <summary>
        /// Timer for checking certificate renewal
        /// </summary>
        private static System.Threading.Timer CertificateRenewalTimer = null;

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        /// <param name="_args"> The command line arguments</param>
        [STAThread]
        public static int Main(string[] _args)
            => Main(null, _args);

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        /// <param name="applicationSettings"> The application settings</param>
        /// <param name="_args"> The command line arguments</param>
        [STAThread]
        public static int Main(IApplicationSettings applicationSettings, string[] _args)
        {
            PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, PackageHelper.NamedExecutable.Server, out var preloadDbSettings);

            applicationSettings ??= new ApplicationSettings();

            //If this executable is invoked directly, write to console, otherwise throw exceptions
            var writeToConsoleOnException = applicationSettings.Origin == "Server";

            // Prepared for the future, where we might want to have a silent console mode
            var silentConsole = false;
            var logMessageToConsole = (string message) => { if (!silentConsole) Console.WriteLine(message); };

            //Find commandline options here for handling special startup cases
            var args = new List<string>(_args);
            var optionsWithFilter = FilterCollector.ExtractOptions(new List<string>(args));
            var commandlineOptions = optionsWithFilter.Item1;
            var filter = optionsWithFilter.Item2;

            if (HelpOptionExtensions.IsArgumentAnyHelpString(args))
            {
                return ShowHelp(writeToConsoleOnException);
            }

            if (commandlineOptions.ContainsKey("tempdir") && !string.IsNullOrEmpty(commandlineOptions["tempdir"]))
            {
                SystemContextSettings.DefaultTempPath = commandlineOptions["tempdir"];
            }

            SystemContextSettings.StartSession();

            ApplyEnvironmentVariables(commandlineOptions);
            ApplySecretProvider(applicationSettings, commandlineOptions, CancellationToken.None).Await();
            DefaultSecretProvider = SecretProviderHelper.GetDefaultSecretProvider(commandlineOptions, CancellationToken.None)
                .Await();

            var parameterFileOption = PARAMETERS_FILE_OPTION_EXTRAS.Prepend(PARAMETERS_FILE_OPTION)
                .FirstOrDefault(x => commandlineOptions.ContainsKey(x));

            if (parameterFileOption != null && !string.IsNullOrEmpty(commandlineOptions[parameterFileOption]))
            {
                string filename = commandlineOptions[parameterFileOption];
                commandlineOptions.Remove(parameterFileOption);
                ReadOptionsFromFile(filename, ref filter, args, commandlineOptions);
            }

            var logHandler = new LogWriteHandler();
            using var logScope = ConfigureLogging(logHandler, commandlineOptions);

            // Validate after logging is configured
            CommandLineArgumentValidator.ValidateArguments(SupportedCommands, commandlineOptions, KnownDuplicateOptions, ValidationIgnoredOptions);

            // Unload any modules that are not allowed
            Library.DynamicLoader.BackendLoader.OnlyAllowBackends(commandlineOptions.GetValueOrDefault(ALLOWED_BACKEND_MODULES));
            Library.DynamicLoader.EncryptionLoader.OnlyAllowModules(commandlineOptions.GetValueOrDefault(ALLOWED_ENCRYPTION_MODULES));
            Library.DynamicLoader.CompressionLoader.OnlyAllowModules(commandlineOptions.GetValueOrDefault(ALLOWED_COMPRESSION_MODULES));


            var crashed = false;
            var terminated = false;
            IQueueRunnerService queueRunner = null;
            UpdatePollThread updatePollThread = null;
            EventPollNotify eventPollNotify = null;
            ISchedulerService scheduler = null;
            try
            {
                var connection = GetDatabaseConnection(applicationSettings, commandlineOptions, silentConsole, true);

                if (!connection.ApplicationSettings.FixedInvalidBackupId)
                    connection.FixInvalidBackupId();

                connection.ApplicationSettings.UpgradePasswordToKBDF();

                // Handle --configure-https option before starting the webserver
                if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, WebServerLoader.OPTION_CONFIGURE_HTTPS))
                    HandleConfigureHttps(connection, commandlineOptions);

                // Check for certificate renewal on startup (only for autogenerated certs)
                var renewalResult = CheckAndRenewCertificate(connection);
                if (renewalResult.RenewalFailedReason != null)
                    RegisterRenewalFailedNotification(connection, renewalResult.RenewalFailedReason);

                CreateApplicationInstance(applicationSettings.DataFolder, writeToConsoleOnException);

                applicationSettings.StartOrStopUsageReporter = () => StartOrStopUsageReporter(connection);
                applicationSettings.StartOrStopUsageReporter?.Invoke();

                AdjustApplicationSettings(connection, commandlineOptions);

                UpdaterManager.OnError += obj =>
                {
                    connection.LogError(null, "Error in updater", obj);
                };

                DuplicatiWebserver = StartWebServer(commandlineOptions, connection, logHandler, applicationSettings).Await();

                connection.SetServiceProvider(DuplicatiWebserver.Provider);
                queueRunner = DuplicatiWebserver.Provider.GetRequiredService<IQueueRunnerService>();
                updatePollThread = DuplicatiWebserver.Provider.GetRequiredService<UpdatePollThread>();
                eventPollNotify = DuplicatiWebserver.Provider.GetRequiredService<EventPollNotify>();
                scheduler = DuplicatiWebserver.Provider.GetRequiredService<ISchedulerService>();

                updatePollThread.Init(Library.Utility.Utility.ParseBoolOption(commandlineOptions, DISABLE_UPDATE_CHECK_OPTION));

                SetPurgeTempFilesTimer(connection, commandlineOptions);
                SetCertificateRenewalTimer(connection);

                LiveControl.StateChanged = (e) => { LiveControl_StateChanged(queueRunner, connection, eventPollNotify, scheduler, e); };

                // Invoke it once to set initial state
                LiveControl_StateChanged(queueRunner, connection, eventPollNotify, scheduler, new LiveControls.LiveControlEvent
                {
                    State = LiveControl.State,
                    TransfersPaused = LiveControl.TransfersPaused,
                    WaitTimeExpiration = LiveControl.EstimatedPauseEnd
                });

                if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, PING_PONG_KEEPALIVE_OPTION))
                {
                    PingPongThread = new Thread(() => PingPongMethod(applicationSettings)) { IsBackground = true };
                    PingPongThread.Start();
                }

                connection.ReWriteAllFieldsIfEncryptionChanged();
                connection.SetPreloadSettingsIfChanged(preloadDbSettings);
                EmitWarningsForConfigurationIssues(connection, applicationSettings, commandlineOptions);

                Log.WriteInformationMessage(LOGTAG, "ServerStarted", Strings.Program.ServerStarted(DuplicatiWebserver.Interface, DuplicatiWebserver.Port));
                logMessageToConsole(Strings.Program.ServerStarted(DuplicatiWebserver.Interface, DuplicatiWebserver.Port));

                var remoteControlUrl = commandlineOptions.GetValueOrDefault(REGISTER_REMOTE_CONTROL_OPTION);
                if (!string.IsNullOrWhiteSpace(remoteControlUrl))
                    RegisterForRemoteControl(DuplicatiWebserver.Provider.GetRequiredService<IRemoteControllerRegistration>(), DuplicatiWebserver.Provider.GetRequiredService<IRemoteController>(), remoteControlUrl, Library.Utility.Utility.ParseBoolOption(commandlineOptions, REGISTER_REMOTE_CONTROL_REREGISTER_OPTION), logMessageToConsole);

                if (!string.IsNullOrWhiteSpace(connection.ApplicationSettings.ClientLicenseKey))
                    Proprietary.LicenseChecker.LicenseHelper.SetRemoteClientLicenseKey(connection.ApplicationSettings.ClientLicenseKey);

                if (applicationSettings.Origin == "Server" && connection.ApplicationSettings.AutogeneratedPassphrase)
                {
                    var signinToken = DuplicatiWebserver.Provider.GetRequiredService<IJWTTokenProvider>().CreateSigninToken("server-cli");
                    var hostname = (connection.ApplicationSettings.AllowedHostnames ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(x => x != "*") ?? "localhost";
                    var scheme = connection.ApplicationSettings.UseHTTPS ? "https" : "http";

                    var url = $"{scheme}://{hostname}:{DuplicatiWebserver.Port}/signin.html?token={signinToken}";
                    Log.WriteWarningMessage(LOGTAG, "ServerStartedSignin", null, Strings.Program.ServerStartedSignin(url));
                    logMessageToConsole(Strings.Program.ServerStartedSignin(url));
                }

                DuplicatiWebserver.TerminationTask.ContinueWith((t) =>
                {
                    if (t.Exception != null)
                    {
                        Log.WriteWarningMessage(LOGTAG, "ServerCrashed", t.Exception, Strings.Program.ServerCrashed(t.Exception.Message));
                        logMessageToConsole(Strings.Program.ServerStartedSignin(Strings.Program.ServerCrashed(t.Exception.ToString())));
                    }

                    terminated = true;
                    applicationSettings.SignalApplicationExit();
                });

                var stopCounter = 0;
                Console.CancelKeyPress += (sender, e) =>
                {
                    if (Interlocked.Increment(ref stopCounter) <= 1)
                    {
                        Log.WriteInformationMessage(LOGTAG, "CancelKeyPressed", "Cancel key pressed, stopping server");
                        Task.Run(() => DuplicatiWebserver?.Stop());
                    }
                    else
                    {
                        Log.WriteWarningMessage(LOGTAG, "CancelKeyPressed", null, "Cancel key pressed twice, terminating now");
                        if (OperatingSystem.IsWindows())
                            Environment.FailFast("Cancel key pressed twice, terminating now");
                        else
                            Environment.Exit(0);
                    }
                };

                ServerStartedEvent.Set();
                applicationSettings.ApplicationExit.WaitHandle.WaitOne();
            }
            catch (SingleInstance.MultipleInstanceException mex)
            {
                crashed = true;
                Log.WriteErrorMessage(LOGTAG, "MultipleInstanceError", mex, Strings.Program.ServerCrashed(mex.Message));
                System.Diagnostics.Trace.WriteLine(Strings.Program.SeriousError(mex.ToString()));
                if (!writeToConsoleOnException) throw;

                Console.WriteLine(Strings.Program.SeriousError(mex.ToString()));
                return 100;
            }
            catch (Exception ex)
            {
                crashed = true;
                Log.WriteErrorMessage(LOGTAG, "ServerCrashed", ex, Strings.Program.ServerCrashed(ex.Message));
                System.Diagnostics.Trace.WriteLine(Strings.Program.SeriousError(ex.ToString()));
                if (writeToConsoleOnException)
                {
                    Console.WriteLine(Strings.Program.SeriousError(ex.ToString()));
                    return 100;
                }
                else
                    throw new Exception(Strings.Program.SeriousError(ex.ToString()), ex);
            }
            finally
            {
                Log.WriteInformationMessage(LOGTAG, "ServerStopping", Strings.Program.ServerStopping);

                var steps = new Action[] {
                    () => eventPollNotify?.SignalNewEvent(),
                    () => ShutdownModernWebserver(),
                    () => updatePollThread?.Terminate(),
                    () => scheduler?.Terminate(true),
                    () => queueRunner?.Terminate(true),
                    () => ApplicationInstance?.Dispose(),
                    () => PurgeTempFilesTimer?.Dispose(),
                    () => CertificateRenewalTimer?.Dispose(),
                    () => Library.UsageReporter.Reporter.ShutDown(),
                    () => PingPongThread?.Interrupt(),
                    () =>
                    {
                        Log.WriteInformationMessage(LOGTAG, "ServerStopped", Strings.Program.ServerStopped);
                        logHandler?.Dispose();
                    }
                };

                foreach (var teardownStep in steps)
                {
                    try
                    {
                        teardownStep();
                    }
                    catch (Exception ex)
                    {
                        // If the server is already crashed, that is the main error
                        // If the server crashes during teardown, we log that as an error
                        if (!(crashed || terminated))
                        {
                            System.Diagnostics.Trace.WriteLine(Strings.Program.TearDownError(ex.ToString()));
                            logMessageToConsole(Strings.Program.TearDownError(ex.ToString()));
                        }
                    }
                }
            }

            return 0;
        }

        /// <summary>
        /// Starts the Duplicati web server
        /// </summary>
        /// <param name="options">The commandline options</param>
        /// <param name="connection">The connection to use</param>
        /// <param name="logWriteHandler">The log write handler</param>
        /// <param name="applicationSettings">The application settings</param>
        /// <returns></returns>
        private static async Task<DuplicatiWebserver> StartWebServer(IReadOnlyDictionary<string, string> options, Connection connection, ILogWriteHandler logWriteHandler, IApplicationSettings applicationSettings)
        {
            var server = await WebServerLoader.TryRunServer(options, connection, async parsedOptions =>
            {
                var mappedSettings = new DuplicatiWebserver.InitSettings(
                    parsedOptions.WebRoot,
                    parsedOptions.Port,
                    parsedOptions.Interface,
                    parsedOptions.AllowedHostnames,
                    parsedOptions.DisableStaticFiles,
                    parsedOptions.TokenLifetimeInMinutes,
                    parsedOptions.SPAPaths,
                    parsedOptions.CorsOrigins,
                    parsedOptions.PreAuthTokens
                );

                var server = DuplicatiWebserver.CreateWebServer(mappedSettings, connection, logWriteHandler, applicationSettings);

                // Start the server, but catch any configuration issues
                var task = server.Start();
                await Task.WhenAny(task, Task.Delay(500));
                if (task.IsCompleted)
                    await task;

                return server;
            }).ConfigureAwait(false);

            connection.ApplicationSettings.ServerPortChanged |= server.Port != connection.ApplicationSettings.LastWebserverPort;
            connection.ApplicationSettings.LastWebserverPort = server.Port;

            return server;
        }

        /// <summary>
        /// Sets up the timer for purging temp files and log data
        /// </summary>
        /// <param name="connection">The connection to use</param>
        /// <param name="commandlineOptions">The commandline options</param>
        private static void SetPurgeTempFilesTimer(Connection connection, Dictionary<string, string> commandlineOptions)
        {
            var lastPurge = new DateTime(0);

            TimerCallback purgeTempFilesCallback = (x) =>
            {
                try
                {
                    if (Math.Abs((DateTime.Now - lastPurge).TotalHours) < (DEBUG_MODE ? 1 : 23))
                    {
                        return;
                    }

                    lastPurge = DateTime.Now;

                    foreach (var e in connection.GetTempFiles().Where((f) => f.Expires < DateTime.Now))
                    {
                        try
                        {
                            if (System.IO.File.Exists(e.Path))
                                System.IO.File.Delete(e.Path);
                        }
                        catch (Exception ex)
                        {
                            connection.LogError(null, $"Failed to delete temp file: {e.Path}", ex);
                        }

                        connection.DeleteTempFile(e.ID);
                    }


                    Library.Utility.TempFile.RemoveOldApplicationTempFiles((path, ex) =>
                    {
                        connection.LogError(null, $"Failed to delete temp file: {path}", ex);
                    });

                    if (!commandlineOptions.TryGetValue("log-retention", out string pts))
                    {
                        pts = DEFAULT_LOG_RETENTION;
                    }

                    connection.PurgeLogData(Library.Utility.Timeparser.ParseTimeInterval(pts, DateTime.Now, true));
                }
                catch (Exception ex)
                {
                    connection.LogError(null, "Failed during temp file cleanup", ex);
                }
            };

            PurgeTempFilesTimer =
                new System.Threading.Timer(purgeTempFilesCallback, null,
                    DEBUG_MODE ? TimeSpan.FromSeconds(10) : TimeSpan.FromHours(1),
                    DEBUG_MODE ? TimeSpan.FromHours(1) : TimeSpan.FromDays(1));
        }

        /// <summary>
        /// Sets up a timer to periodically check for certificate renewal.
        /// This ensures certificates are renewed even if the server runs continuously
        /// without restarts.
        /// </summary>
        /// <param name="connection">The database connection.</param>
        private static void SetCertificateRenewalTimer(Connection connection)
        {
            TimerCallback certificateRenewalCallback = (x) =>
            {
                try
                {
                    // Only check for autogenerated certificates
                    if (!connection.ApplicationSettings.ServerSSLCertificateAutogenerated)
                        return;

                    var renewalResult = CheckAndRenewCertificate(connection);

                    if (renewalResult.Renewed)
                    {
                        Log.WriteInformationMessage(LOGTAG, "CertificateRenewedAutomatically",
                            "SSL certificate was automatically renewed during periodic check.");
                    }
                    else if (renewalResult.RenewalFailedReason != null)
                    {
                        Log.WriteWarningMessage(LOGTAG, "CertificateRenewalFailed",
                            null,
                            $"Automatic certificate renewal failed: {renewalResult.RenewalFailedReason}");
                    }
                }
                catch (Exception ex)
                {
                    Log.WriteErrorMessage(LOGTAG, "CertificateRenewalCheckFailed", ex,
                        "Failed to check/renew SSL certificate during periodic check.");
                }
            };

            CertificateRenewalTimer =
                new System.Threading.Timer(certificateRenewalCallback, null,
                    TimeSpan.FromHours(1),
                    TimeSpan.FromDays(1));
        }

        /// <summary>
        /// Registers the server for remote control
        /// </summary>
        /// <param name="remoteControllerRegistration">The remote controller registration</param>
        /// <param name="remoteController">The remote controller</param>
        /// <param name="remoteControlUrl">The remote control URL</param>
        /// <param name="reRegister">Whether to re-register</param>
        /// <param name="logMessageToConsole">Callback to log messages to console</param>
        private static void RegisterForRemoteControl(IRemoteControllerRegistration remoteControllerRegistration, IRemoteController remoteController, string remoteControlUrl, bool reRegister, Action<string> logMessageToConsole)
        {
            if (remoteController.CanEnable)
            {
                if (!reRegister)
                {
                    Log.WriteInformationMessage(LOGTAG, "RemoteControlAlreadyRegistered", Strings.Program.RemoteControlAlreadyRegistered);
                    return;
                }

                remoteController.DeleteRegistration();
            }

            Task.Run(async () =>
            {
                try
                {
                    var regTask = remoteControllerRegistration.RegisterMachine(remoteControlUrl);
                    while (!regTask.IsCompleted)
                    {
                        // Interface does not have events, so poll it every second
                        await Task.WhenAny(Task.Delay(1000), regTask).ConfigureAwait(false);
                        if (!string.IsNullOrWhiteSpace(remoteControllerRegistration.RegistrationUrl))
                        {
                            Log.WriteWarningMessage(LOGTAG, "RemoteControlRegistrationUrl", null, Strings.Program.RemoteControlRegistrationUrl(remoteControllerRegistration.RegistrationUrl));
                            logMessageToConsole(Strings.Program.RemoteControlRegistrationUrl(remoteControllerRegistration.RegistrationUrl));
                            break;
                        }
                    }

                    // Await the registration task and ensure any errors are logged
                    await regTask.ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    Log.WriteErrorMessage(LOGTAG, "RemoteControlRegistrationFailed", ex, Strings.Program.RemoteControlRegistrationFailed(ex.Message));
                }
            });
        }

        /// <summary>
        /// Adjusts the application settings based on commandline options
        /// </summary>
        /// <param name="connection">The connection to use</param>
        /// <param name="commandlineOptions">The commandline options</param>
        private static void AdjustApplicationSettings(Connection connection, Dictionary<string, string> commandlineOptions)
        {
            // This clears the JWT config, and a new will be generated, invalidating all existing tokens
            if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, WebServerLoader.OPTION_WEBSERVICE_RESET_JWT_CONFIG))
            {
                connection.ApplicationSettings.JWTConfig = null;
                // Clean up stored tokens as they are now invalid
                connection.ExecuteWithCommand((con) => con.ExecuteNonQuery("DELETE FROM TokenFamily"));
            }

            if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, WebServerLoader.OPTION_WEBSERVICE_ENABLE_FOREVER_TOKEN))
                connection.ApplicationSettings.EnableForeverTokens();

            if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_DISABLE_SIGNIN_TOKENS))
                connection.ApplicationSettings.DisableSigninTokens = Library.Utility.Utility.ParseBoolOption(commandlineOptions, WebServerLoader.OPTION_WEBSERVICE_DISABLE_SIGNIN_TOKENS);

            if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_DISABLEAPIEXTENSIONS))
                connection.ApplicationSettings.DisabledAPIExtensions = commandlineOptions.GetValueOrDefault(WebServerLoader.OPTION_WEBSERVICE_DISABLEAPIEXTENSIONS)?
                    .Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                    .ToHashSet(StringComparer.OrdinalIgnoreCase) ?? [];

            if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_ENABLE_FOLDER_STATUS_SERVICE))
                connection.ApplicationSettings.EnableFolderStatusService = Library.Utility.Utility.ParseBoolOption(commandlineOptions, WebServerLoader.OPTION_WEBSERVICE_ENABLE_FOLDER_STATUS_SERVICE);

            if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_PASSWORD))
                connection.ApplicationSettings.SetWebserverPassword(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_PASSWORD]);

            if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES))
                connection.ApplicationSettings.SetAllowedHostnames(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES]);
            else if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES_ALT))
                connection.ApplicationSettings.SetAllowedHostnames(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES_ALT]);

            if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_TIMEZONE) && !string.IsNullOrEmpty(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_TIMEZONE]))
                try
                {
                    connection.ApplicationSettings.Timezone = TimeZoneHelper.FindTimeZone(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_TIMEZONE]);
                }
                catch (Exception ex)
                {
                    throw new UserInformationException(Strings.Program.InvalidTimezone(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_TIMEZONE]), "InvalidTimeZone", ex);
                }

            // The database has recorded a new version
            if (connection.ApplicationSettings.UpdatedVersion != null)
            {
                // Check if the running version is newer than the recorded version
                if (UpdaterManager.TryParseVersion(connection.ApplicationSettings.UpdatedVersion.Version) <= UpdaterManager.TryParseVersion(UpdaterManager.SelfVersion.Version))
                {
                    // Clean up lingering update notifications
                    var updateNotifications = connection.GetNotifications().Where(x => x.Action == "update:new").ToList();
                    foreach (var n in updateNotifications)
                        connection.DismissNotification(n.ID);

                    // Clear up the recorded version
                    connection.ApplicationSettings.UpdatedVersion = null;
                }
            }
        }

        /// <summary>
        /// Emits warnings for configuration issues
        /// </summary>
        /// <param name="connection">The connection</param>
        /// <param name="applicationSettings">The application settings</param>
        /// <param name="commandlineOptions">The commandline options</param>
        private static void EmitWarningsForConfigurationIssues(Connection connection, IApplicationSettings applicationSettings, Dictionary<string, string> commandlineOptions)
        {
            // First, remove any pending notifications, if the issue is resolved
            if (connection.IsEncryptingFields)
            {
                // Remove any existing unencrypted database notification
                var updateNotifications = connection.GetNotifications().Where(x => x.Action == "config:issue:unencrypted-database").ToList();
                foreach (var n in updateNotifications)
                    connection.DismissNotification(n.ID);

                // Remove any existing HTTPS related notifications
                var caKeyNotifications = connection.GetNotifications().Where(x => x.Action == NOTIFICATION_CA_KEY_UNENCRYPTED_DATABASE || x.Action == NOTIFICATION_CERTIFICATE_RENEWAL_FAILED).ToList();
                foreach (var n in caKeyNotifications)
                    connection.DismissNotification(n.ID);
            }

            if (OperatingSystem.IsWindows() && !applicationSettings.DataFolder.StartsWith(Util.AppendDirSeparator(Environment.GetFolderPath(Environment.SpecialFolder.Windows)), StringComparison.OrdinalIgnoreCase))
            {
                // Remove any existing windows folder used notification
                var updateNotifications = connection.GetNotifications().Where(x => x.Action == "config:issue:windows-folder-used").ToList();
                foreach (var n in updateNotifications)
                    connection.DismissNotification(n.ID);
            }

            // Check tempdir free space against volume sizes
            CheckTempDirFreeSpace(connection, commandlineOptions);

            // Emit warnings if the application has been updated
            if (connection.ApplicationSettings.LastConfigIssueCheckVersion != UpdaterManager.SelfVersion.Version)
            {
                var updateNotifications = connection.GetNotifications().Where(x => x.Action.StartsWith("config:issue:")).ToList();
                foreach (var n in updateNotifications)
                    connection.DismissNotification(n.ID);

                if (!connection.IsEncryptingFields && !Library.Utility.Utility.ParseBoolOption(commandlineOptions, DISABLE_DB_ENCRYPTION_OPTION))
                {
                    connection.RegisterNotification(
                        Serialization.NotificationType.Warning,
                        "Unencrypted database",
                        "The database is not encrypted. This is a security risk and should be fixed as soon as possible.",
                        null,
                        null,
                        "config:issue:unencrypted-database",
                        null,
                        "UnencryptedDatabase",
                        null,
                        (self, all) =>
                        {
                            return all.FirstOrDefault(x => x.Action == "config:issue:unencrypted-database") ?? self;
                        }
                    );
                }

                if (OperatingSystem.IsWindows() && applicationSettings.DataFolder.StartsWith(Util.AppendDirSeparator(Environment.GetFolderPath(Environment.SpecialFolder.Windows)), StringComparison.OrdinalIgnoreCase))
                {
                    connection.RegisterNotification(
                        Serialization.NotificationType.Warning,
                        "Incorrect storage folder",
                        "The server configuration is stored inside the Windows folder. Please move the configuration to a different location, or it may be deleted on Windows version upgrades.",
                        null,
                        null,
                        "config:issue:windows-folder-used",
                        null,
                        "UnencryptedDatabase",
                        null,
                        (self, all) =>
                        {
                            return all.FirstOrDefault(x => x.Action == "config:issue:windows-folder-used") ?? self;
                        }
                    );
                }

                connection.ApplicationSettings.LastConfigIssueCheckVersion = UpdaterManager.SelfVersion.Version;
            }
        }

        /// <summary>
        /// Checks if the tempdir has enough free space relative to the volume sizes of configured backups.
        /// Emits a warning if free space is less than 4 times the volume size.
        /// </summary>
        /// <param name="connection">The database connection</param>
        /// <param name="commandlineOptions">The commandline options</param>
        private static void CheckTempDirFreeSpace(Connection connection, Dictionary<string, string> commandlineOptions)
        {
            const string TEMPDIR_WARNING_ACTION = "config:issue:tempdir-low-space";

            // Get the tempdir path - check Connection settings first, then fall back to system default
            var tempDir = TempFolder.SystemTempPath;
            var serverSettings = connection.GetSettings(Connection.ANY_BACKUP_ID);
            var tempDirSetting = serverSettings?.FirstOrDefault(s =>
                string.Equals(s.Name, "tempdir", StringComparison.OrdinalIgnoreCase) || string.Equals(s.Name, "--tempdir", StringComparison.OrdinalIgnoreCase));
            if (tempDirSetting != null && !string.IsNullOrEmpty(tempDirSetting.Value))
                tempDir = tempDirSetting.Value;

            // Get free space in tempdir using the Utility method
            var spaceInfo = Library.Utility.Utility.GetFreeSpaceForPath(tempDir);
            // Could not determine free space, skip the check
            if (spaceInfo == null)
                return;

            // Get the maximum volume size from all configured backups
            var maxVolumeSize = GetMaxVolumeSize(connection, commandlineOptions);

            // Check if free space is less than 4 times the volume size
            if (spaceInfo.Value.FreeSpace < maxVolumeSize * Library.Utility.Utility.VOLUME_SIZE_FREE_SPACE_MULTIPLIER)
            {
                // Emit warning
                connection.RegisterNotification(
                    Serialization.NotificationType.Warning,
                    "Low temporary folder space",
                    $"The temporary folder '{tempDir}' has limited free space ({Library.Utility.Utility.FormatSizeString(spaceInfo.Value.FreeSpace)}). " +
                    $"It is recommended to have at least {Library.Utility.Utility.VOLUME_SIZE_FREE_SPACE_MULTIPLIER} times the volume size ({Library.Utility.Utility.FormatSizeString(maxVolumeSize * Library.Utility.Utility.VOLUME_SIZE_FREE_SPACE_MULTIPLIER)}) available for optimal operation.",
                    null,
                    null,
                    TEMPDIR_WARNING_ACTION,
                    null,
                    "TempDirLowSpace",
                    null,
                    (self, all) =>
                    {
                        return all.FirstOrDefault(x => x.Action == TEMPDIR_WARNING_ACTION) ?? self;
                    }
                );
            }
            else
            {
                // Clear any existing warning
                var existingNotifications = connection.GetNotifications().Where(x => x.Action == TEMPDIR_WARNING_ACTION).ToList();
                foreach (var n in existingNotifications)
                    connection.DismissNotification(n.ID);
            }
        }

        /// <summary>
        /// Gets the maximum volume size from all configured backups.
        /// </summary>
        /// <param name="connection">The database connection</param>
        /// <param name="commandlineOptions">The commandline options</param>
        /// <returns>The maximum volume size in bytes</returns>
        private static long GetMaxVolumeSize(Connection connection, Dictionary<string, string> commandlineOptions)
        {
            const string DEFAULT_VOLUME_SIZE = "50mb";
            long maxVolumeSize = 0;

            // Check if there's a global dblock-size in commandline options
            if (commandlineOptions.TryGetValue("dblock-size", out var globalDblockSize) && !string.IsNullOrEmpty(globalDblockSize))
            {
                try
                {
                    maxVolumeSize = Sizeparser.ParseSize(globalDblockSize, "mb");
                }
                catch
                {
                    // Ignore parsing errors
                }
            }

            // Check if there's a global dblock-size in the database settings
            var serverSettings = connection.GetSettings(Connection.ANY_BACKUP_ID);
            var defaultDblockSize = serverSettings?.FirstOrDefault(s => string.Equals(s.Name, "dblock-size", StringComparison.OrdinalIgnoreCase) || string.Equals(s.Name, "--dblock-size", StringComparison.OrdinalIgnoreCase));
            if (!string.IsNullOrEmpty(defaultDblockSize?.Value))
                try
                {
                    maxVolumeSize = Math.Max(Sizeparser.ParseSize(defaultDblockSize.Value, "mb"), maxVolumeSize);
                }
                catch
                {
                    // Ignore parsing errors
                }

            // If no global setting, use default
            maxVolumeSize = Math.Max(maxVolumeSize, Sizeparser.ParseSize(DEFAULT_VOLUME_SIZE, "mb"));

            // Check all configured backups for their volume sizes
            foreach (var backup in connection.Backups)
            {
                try
                {
                    // Get settings for this backup
                    var backupId = long.Parse(backup.ID);
                    var settings = connection.GetSettings(backupId);

                    // Look for dblock-size setting
                    var dblockSizeSetting = settings?.FirstOrDefault(s =>
                        string.Equals(s.Name, "dblock-size", StringComparison.OrdinalIgnoreCase) || string.Equals(s.Name, "--dblock-size", StringComparison.OrdinalIgnoreCase));

                    if (dblockSizeSetting != null && !string.IsNullOrEmpty(dblockSizeSetting.Value))
                    {
                        var backupVolumeSize = Sizeparser.ParseSize(dblockSizeSetting.Value, "mb");
                        if (backupVolumeSize > maxVolumeSize)
                            maxVolumeSize = backupVolumeSize;
                    }
                }
                catch
                {
                    // Ignore errors for individual backups
                }
            }

            return maxVolumeSize;
        }

        /// <summary>
        /// Creates the application instance to ensure a single instance is running for the current user
        /// </summary>
        /// <param name="dataFolder">The data folder to use for single instance locking</param>
        /// <param name="writeToConsoleOnException">Write to console on exception</param>
        private static void CreateApplicationInstance(string dataFolder, bool writeToConsoleOnException)
        {
            try
            {
                //This will also create DATAFOLDER if it does not exist
                ApplicationInstance = new SingleInstance(dataFolder);
            }
            catch (Exception ex)
            {
                if (writeToConsoleOnException)
                {
                    Console.WriteLine(Strings.Program.StartupFailure(ex));
                    Environment.Exit(200);
                }

                throw new Exception(Strings.Program.StartupFailure(ex));
            }

            if (!ApplicationInstance.IsFirstInstance)
            {
                if (writeToConsoleOnException)
                {
                    Console.WriteLine(Strings.Program.AnotherInstanceDetected);
                    Environment.Exit(200);
                }

                throw new SingleInstance.MultipleInstanceException(Strings.Program.AnotherInstanceDetected);
            }
        }

        /// <summary>
        /// Applies environment variables by converting them to commandline options
        /// </summary>
        /// <param name="commandlineOptions">The commandline options</param>
        private static void ApplyEnvironmentVariables(Dictionary<string, string> commandlineOptions)
        {
            foreach (var key in SupportedCommands.SelectMany(x => (x.Aliases ?? []).Prepend(x.Name)).Distinct())
            {
                // Commandline options take precedence
                if (commandlineOptions.ContainsKey(key))
                    continue;

                var envkey = $"{ENV_NAME_PREFIX}__{key.Replace('-', '_').ToUpperInvariant()}";
                var envval = Environment.GetEnvironmentVariable(envkey);
                if (!string.IsNullOrWhiteSpace(envval))
                    commandlineOptions[key] = envval;
            }

            // Set the encryption key from the environment variable
            if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME)) && string.IsNullOrWhiteSpace(commandlineOptions.GetValueOrDefault(SETTINGS_ENCRYPTION_KEY_OPTION)))
                commandlineOptions[SETTINGS_ENCRYPTION_KEY_OPTION] = Environment.GetEnvironmentVariable(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME);
        }

        /// <summary>
        /// Applies the secret provider to the commandline options and application settings
        /// </summary>
        /// <param name="applicationSettings">The application settings</param>
        /// <param name="commandlineOptions">The commandline options</param>
        /// <param name="cancellationToken">The cancellation token</param>
        /// <returns>A task that represents the asynchronous operation</returns>
        private static async Task ApplySecretProvider(IApplicationSettings applicationSettings, Dictionary<string, string> commandlineOptions, CancellationToken cancellationToken)
            => applicationSettings.SecretProvider = await SecretProviderHelper.ApplySecretProviderAsync([], [], commandlineOptions, TempFolder.SystemTempPath, applicationSettings.SecretProvider, cancellationToken).ConfigureAwait(false);

        /// <summary>
        /// A log destination that writes to the console
        /// </summary>
        /// <param name="level">The minimum log level</param>
        private class ConsoleLogDestination(LogMessageType level) : ILogDestination
        {
            /// <inheritdoc />
            public void WriteMessage(LogEntry entry)
            {
                if (entry.Level >= level)
                    Console.WriteLine(entry.AsString(true));
            }
        }

        /// <summary>
        /// Configures logging based on commandline options
        /// </summary>
        /// <param name="logWriteHandler">The log write handler</param>
        /// <param name="commandlineOptions">The commandline options</param>
        /// <returns>A disposable log scope</returns>
        private static IDisposable ConfigureLogging(ILogWriteHandler logWriteHandler, Dictionary<string, string> commandlineOptions)
        {
            IDisposable logScope;
            //Log various information in the logfile
            if (DEBUG_MODE && !commandlineOptions.ContainsKey(LOG_FILE_OPTION))
            {
                var prefix = System.Reflection.Assembly.GetEntryAssembly().GetName().Name.StartsWith("Duplicati.Server") ? "server" : "trayicon";
                commandlineOptions[LOG_FILE_OPTION] = System.IO.Path.Combine(StartupPath, $"Duplicati-{prefix}.debug.log");
                commandlineOptions[LOG_LEVEL_OPTION] = Duplicati.Library.Logging.LogMessageType.Profiling.ToString();
                if (System.IO.File.Exists(commandlineOptions[LOG_FILE_OPTION]))
                    System.IO.File.Delete(commandlineOptions[LOG_FILE_OPTION]);
            }

            logScope = Log.StartScope(logWriteHandler, null);

            if (commandlineOptions.ContainsKey(LOG_FILE_OPTION))
            {
                var loglevel = Library.Logging.LogMessageType.Warning;
                if (commandlineOptions.ContainsKey(LOG_LEVEL_OPTION))
                    Enum.TryParse(commandlineOptions[LOG_LEVEL_OPTION], true, out loglevel);

                logWriteHandler.SetServerFile(commandlineOptions[LOG_FILE_OPTION], loglevel);
            }

            if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, LOG_CONSOLE_OPTION))
            {
                var loglevel = Library.Logging.LogMessageType.Information;
                if (commandlineOptions.ContainsKey(LOG_LEVEL_OPTION))
                    Enum.TryParse(commandlineOptions[LOG_LEVEL_OPTION], true, out loglevel);

                logWriteHandler.AppendLogDestination(new ConsoleLogDestination(loglevel), loglevel);
            }

            if (commandlineOptions.TryGetValue(WINDOWS_EVENTLOG_OPTION, out var source) && !string.IsNullOrEmpty(source))
            {
                if (!OperatingSystem.IsWindows())
                {
                    Log.WriteWarningMessage(LOGTAG, "WindowsLogNotSupported", null, Strings.Program.WindowsEventLogNotSupported);
                }
                else
                {
                    if (!WindowsEventLogSource.SourceExists(source))
                    {
                        Log.WriteInformationMessage(LOGTAG, "WindowsLogMissingCreating", null, Strings.Program.WindowsEventLogSourceNotFound(source));
                        try
                        {
                            WindowsEventLogSource.CreateEventSource(source);
                        }
                        catch (Exception ex)
                        {
                            Log.WriteWarningMessage(LOGTAG, "WindowsLogFailedCreate", ex, Strings.Program.WindowsEventLogSourceNotCreated(source));
                        }
                    }

                    if (WindowsEventLogSource.SourceExists(source))
                    {
                        var loglevel = LogMessageType.Information;
                        if (commandlineOptions.ContainsKey(WINDOWS_EVENTLOG_LEVEL_OPTION))
                            Enum.TryParse(commandlineOptions[WINDOWS_EVENTLOG_LEVEL_OPTION], true, out loglevel);

                        logWriteHandler.AppendLogDestination(new WindowsEventLogSource(source), loglevel);
                    }
                }
            }

            CrashlogHelper.OnUnobservedTaskException += (ex) => logWriteHandler.WriteMessage(new LogEntry(ex.Message, null, Library.Logging.LogMessageType.Error, LOGTAG, "UnobservedTaskException", ex));
            return logScope;
        }

        /// <summary>
        /// Shows the help information
        /// </summary>
        /// <param name="writeToConsoleOnException">Whether to write to console on exception</param>
        /// <returns>An integer indicating the result</returns>
        private static int ShowHelp(bool writeToConsoleOnException)
        {
            if (writeToConsoleOnException)
            {
                Console.WriteLine(Strings.Program.HelpDisplayDialog);

                foreach (var arg in SupportedCommands)
                    Console.WriteLine(Strings.Program.HelpDisplayFormat(arg.Name, arg.LongDescription));

                return 0;
            }

            throw new Exception("Server invoked with --help");
        }

        /// <summary>
        /// Gets the database connection, upgrading the database if needed
        /// </summary>
        /// <param name="applicationSettings">The application settings</param>
        /// <param name="commandlineOptions">The commandline options</param>
        /// <param name="silentConsole">Whether to suppress console output</param>
        /// <param name="changeDbEncryption">Whether to change the database encryption state if the arguments require it</param>
        /// <returns>The database connection</returns>
        public static Connection GetDatabaseConnection(IApplicationSettings applicationSettings, Dictionary<string, string> commandlineOptions, bool silentConsole, bool changeDbEncryption)
        {
            // Emit a warning if the database is stored in the Windows folder
            if (Util.IsPathUnderWindowsFolder(applicationSettings.DataFolder))
                Log.WriteWarningMessage(LOGTAG, "DatabaseInWindowsFolder", null, "The database is stored in the Windows folder, this is not recommended as it will be deleted on Windows upgrades.");

            CrashlogHelper.DefaultLogDir = applicationSettings.DataFolder;

            var sqliteVersion = new Version(Duplicati.Library.SQLiteHelper.SQLiteLoader.SQLiteVersion);
            if (sqliteVersion < new Version(3, 6, 3))
            {
                //The official Mono SQLite provider is also broken with less than 3.6.3
                throw new Exception(Strings.Program.WrongSQLiteVersion(sqliteVersion, "3.6.3"));
            }

            //Create the connection instance
            var con = Library.SQLiteHelper.SQLiteLoader.LoadConnection();

            try
            {
                var databasePath = System.IO.Path.Combine(applicationSettings.DataFolder, DataFolderManager.SERVER_DATABASE_FILENAME);

                if (!System.IO.Directory.Exists(System.IO.Path.GetDirectoryName(databasePath)))
                    System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(databasePath));

                // Attempt to open the database, removing any encryption present
                Library.SQLiteHelper.SQLiteLoader.OpenDatabaseAsync(con, databasePath, Library.SQLiteHelper.SQLiteRC4Decrypter.GetEncryptionPassword(commandlineOptions)).Await();

                Library.SQLiteHelper.DatabaseUpgrader.UpgradeDatabase(con, databasePath, typeof(Library.RestAPI.Database.DatabaseSchemaMarker));
            }
            catch (Exception ex)
            {
                //Unwrap the reflection exceptions
                if (ex is System.Reflection.TargetInvocationException && ex.InnerException != null)
                    ex = ex.InnerException;

                throw new Exception(Strings.Program.DatabaseOpenError(ex.Message), ex);
            }

            var disableDbEncryption = Library.Utility.Utility.ParseBoolOption(commandlineOptions, DISABLE_DB_ENCRYPTION_OPTION);
            var requireDbEncryptionKey = Library.Utility.Utility.ParseBoolOption(commandlineOptions, REQUIRE_DB_ENCRYPTION_KEY_OPTION);
            var encKey = EncryptedFieldHelper.KeyInstance.CreateKeyIfValid(commandlineOptions.GetValueOrDefault(SETTINGS_ENCRYPTION_KEY_OPTION));
            var usingBlacklistedKey = encKey?.IsBlacklisted ?? false;
            var hasValidEncryptionKey = encKey != null;

            // Don't encrypt the database in debug mode, unless explicitly requested
            if (DEBUG_MODE)
            {
                if (string.IsNullOrWhiteSpace(commandlineOptions.GetValueOrDefault(DISABLE_DB_ENCRYPTION_OPTION)))
                    disableDbEncryption = true;
            }

            // If we are supposed to have an encryption key, but do not, try to get it from the (default) secret provider
            if (!hasValidEncryptionKey && DefaultSecretProvider != null)
            {
                string encryptionKey = null;
                try
                {
                    encryptionKey = DefaultSecretProvider.ResolveSecretAsync(DATABASE_SECRET_VALUE_NAME, CancellationToken.None).Await();
                }
                catch
                {
                    Log.WriteInformationMessage(LOGTAG, "SecretProviderFailedToGetEncryptionKey", null, Strings.Program.SecretProviderFailedToGetEncryptionKey);
                }

                // If there is no encryption key, and the secret provider supports setting secrets, try to set the encryption key
                if (!disableDbEncryption && string.IsNullOrWhiteSpace(encryptionKey) && DefaultSecretProvider.IsSetSupported)
                {
                    var tmpkey = Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(32));
                    try
                    {
                        DefaultSecretProvider.SetSecretAsync(
                            DATABASE_SECRET_VALUE_NAME, tmpkey, false,
                            CancellationToken.None).Await();

                        // For some cloud providers, this can sometimes fail due to eventual consistency
                        // For now, the solution is that settings will be encrypted in the next run
                        var tmp = DefaultSecretProvider.ResolveSecretAsync(DATABASE_SECRET_VALUE_NAME, CancellationToken.None).Await();

                        if (tmp != tmpkey)
                            throw new Exception("Secret provider did not return the expected value");

                        encryptionKey = tmpkey;
                    }
                    catch
                    {
                        Log.WriteVerboseMessage(LOGTAG, "SecretProviderFailedToSetEncryptionKey", null, Strings.Program.SecretProviderFailedToSetEncryptionKey);
                    }
                }

                // If we got an encryption key (or created one), apply it
                if (!string.IsNullOrWhiteSpace(encryptionKey))
                {
                    encKey = EncryptedFieldHelper.KeyInstance.CreateKeyIfValid(encryptionKey);
                    hasValidEncryptionKey = encKey != null;
                }
            }


            if (requireDbEncryptionKey && !(hasValidEncryptionKey || disableDbEncryption))
                throw new UserInformationException(Strings.Program.DatabaseEncryptionKeyRequired(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION), "RequireDbEncryptionKey");

            applicationSettings.SettingsEncryptionKeyProvidedExternally = hasValidEncryptionKey;

            var hasEncryptedFields = false;
            try
            {
                using (var cmd = con.CreateCommand(@$"SELECT ""Value"" FROM ""Option"" WHERE ""Name"" = @Name AND ""BackupID"" = @BackupId"))
                    hasEncryptedFields = Library.Utility.Utility.ParseBool(cmd
                        .SetParameterValue("@Name", Database.ServerSettings.CONST.ENCRYPTED_FIELDS)
                        .SetParameterValue("@BackupId", Connection.SERVER_SETTINGS_ID).ExecuteScalar()?.ToString(), false);

                if (hasEncryptedFields && !hasValidEncryptionKey)
                {
                    Log.WriteWarningMessage(LOGTAG, "EncryptionKeyMissing", null, Strings.Program.EncryptionKeyMissing(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME));
                    if (!silentConsole)
                        Console.WriteLine(Strings.Program.EncryptionKeyMissing(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME));
                }
            }
            catch
            {
                // Ignore errors here, as we are just checking for a potential issue
                // Only negative effect is that we do not show a potentially helpful warning
            }

            if (!hasValidEncryptionKey && !disableDbEncryption)
            {
                disableDbEncryption = true;
                Log.WriteWarningMessage(LOGTAG, "MissingEncryptionKey", null, Strings.Program.NoEncryptionKeySpecified(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
                if (!silentConsole)
                    Console.WriteLine(Strings.Program.NoEncryptionKeySpecified(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
            }

            if (usingBlacklistedKey && !disableDbEncryption)
            {
                disableDbEncryption = true;
                Log.WriteErrorMessage(LOGTAG, "BlacklistedEncryptionKey", null, Strings.Program.BlacklistedEncryptionKey(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
                if (!silentConsole)
                    Console.WriteLine(Strings.Program.BlacklistedEncryptionKey(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
            }

            // If the database is not encrypted, and we are not changing the encryption
            // don't pass the key, as that would cause the database to be encrypted
            if (!hasEncryptedFields && !changeDbEncryption && hasValidEncryptionKey)
                encKey = null;

            return new Connection(con, disableDbEncryption, encKey, applicationSettings.DataFolder, applicationSettings.StartOrStopUsageReporter);
        }

        /// <summary>
        /// Starts or stops the usage reporter based on application settings
        /// </summary>
        /// <param name="connection">The connection</param>
        private static void StartOrStopUsageReporter(Connection connection)
        {
            var disableUsageReporter =
                string.Equals(connection.ApplicationSettings.UsageReporterLevel, "none", StringComparison.OrdinalIgnoreCase)
                ||
                string.Equals(connection.ApplicationSettings.UsageReporterLevel, "disabled", StringComparison.OrdinalIgnoreCase);

            if (!Enum.TryParse<Library.UsageReporter.ReportType>(connection.ApplicationSettings.UsageReporterLevel, true, out var reportLevel))
                Library.UsageReporter.Reporter.SetReportLevel(null, disableUsageReporter);
            else
                Library.UsageReporter.Reporter.SetReportLevel(reportLevel, disableUsageReporter);
        }

        /// <summary>
        /// This event handler updates the trayicon menu with the current state of the runner.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException"></exception>
        private static void LiveControl_StateChanged(IQueueRunnerService queueRunnerService, Connection connection, EventPollNotify eventPollNotify, ISchedulerService schedulerService, LiveControls.LiveControlEvent e)
        {
            var appSettings = connection.ApplicationSettings;
            switch (e.State)
            {
                case LiveControls.LiveControlState.Paused:
                    {
                        queueRunnerService.Pause();
                        queueRunnerService.GetCurrentTask()?.Pause(e.TransfersPaused);
                        appSettings.PausedUntil = e.WaitTimeExpiration;
                        break;
                    }
                case LiveControls.LiveControlState.Running:
                    {
                        queueRunnerService.Resume();
                        queueRunnerService.GetCurrentTask()?.Resume();
                        schedulerService?.Reschedule();
                        appSettings.PausedUntil = null;
                        break;
                    }
                default:
                    Log.WriteWarningMessage(LOGTAG, "InvalidPauseResumeState", null, Strings.Program.InvalidPauseResumeState(LiveControl.State));
                    break;
            }

            eventPollNotify.SignalNewEvent();

        }

        /// <summary>
        /// Simple method for tracking if the server has crashed
        /// </summary>
        /// <param name="applicationSettings">The application settings</param>
        private static void PingPongMethod(IApplicationSettings applicationSettings)
        {
            var rd = new System.IO.StreamReader(Console.OpenStandardInput());
            var wr = new System.IO.StreamWriter(Console.OpenStandardOutput());
            string line;
            while ((line = rd.ReadLine()) != null)
            {
                if (string.Equals("shutdown", line, StringComparison.OrdinalIgnoreCase))
                {
                    // TODO: All calls to ApplicationExitEvent and TrayIcon->Quit
                    // should check if we are running something
                    applicationSettings.SignalApplicationExit();
                }
                else
                {
                    wr.WriteLine("pong");
                    wr.Flush();
                }
            }
        }

        /// <summary>
        /// Handles the --configure-https option by generating and installing HTTPS certificates.
        /// </summary>
        /// <param name="connection">The database connection.</param>
        /// <param name="commandlineOptions">The commandline options.</param>
        private static void HandleConfigureHttps(Connection connection, Dictionary<string, string> commandlineOptions)
        {
            var hostnames = commandlineOptions.GetValueOrDefault(WebServerLoader.OPTION_CONFIGURE_HTTPS_HOSTNAMES);

            // Read existing certificate data from database
            var isAutogenerated = connection.ApplicationSettings.ServerSSLCertificateAutogenerated;
            var serverCert = connection.ApplicationSettings.ServerSSLCertificate;
            var caCert = connection.ApplicationSettings.ServerCACertificate;
            var caKey = connection.ApplicationSettings.ServerCACertificateKey;
            var caPassword = connection.ApplicationSettings.ServerCACertificatePassword;

            CACertificateData caData = null;
            if (!string.IsNullOrEmpty(caCert) && !string.IsNullOrEmpty(caKey) && !string.IsNullOrEmpty(caPassword))
            {
                caData = new CACertificateData
                {
                    CACertificate = caCert,
                    CAKey = caKey,
                    CAPassword = caPassword
                };
            }

            var result = CertificateRenewalChecker.GenerateAndInstallCertificates(
                isAutogenerated,
                serverCert,
                caData,
                hostnames,
                skipTrustInstallation: false);

            if (result.Success)
            {
                Log.WriteInformationMessage(LOGTAG, "ConfigureHttpsComplete", "HTTPS certificates configured successfully.");

                // Store generated/renewed certificates in database
                if (result.CACertificate != null && result.ServerCertificate != null)
                {
                    // New CA + server certificate generated
                    var settings = new Dictionary<string, string>
                    {
                        [Database.ServerSettings.CONST.SERVER_CA_CERTIFICATE] = result.CACertificate.CACertificate,
                        [Database.ServerSettings.CONST.SERVER_CA_CERTIFICATE_KEY] = result.CACertificate.CAKey,
                        [Database.ServerSettings.CONST.SERVER_CA_CERTIFICATE_PASSWORD] = result.CACertificate.CAPassword,
                        [Database.ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = result.ServerCertificate.ServerCertificate,
                        [Database.ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = result.ServerCertificate.Password,
                        [Database.ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "true"
                    };

                    connection.ApplicationSettings.UpdateSettings(settings, false);

                    // Check if database is encrypted and emit warning if not
                    if (!connection.IsEncryptingFields)
                        EmitUnencryptedDatabaseWarning(connection);
                }
                else if (result.ServerCertificate != null)
                {
                    // Server certificate renewed using existing CA
                    var settings = new Dictionary<string, string>
                    {
                        [Database.ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = result.ServerCertificate.ServerCertificate,
                        [Database.ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = result.ServerCertificate.Password,
                        [Database.ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "true"
                    };

                    connection.ApplicationSettings.UpdateSettings(settings, false);

                    // Check if database is encrypted and emit warning if not
                    if (!connection.IsEncryptingFields)
                        EmitUnencryptedDatabaseWarning(connection);
                }
            }
            else
            {
                Log.WriteErrorMessage(LOGTAG, "ConfigureHttpsFailed", null, "Failed to configure HTTPS certificates.");
            }
        }

        /// <summary>
        /// Checks if the server certificate needs renewal and performs renewal if necessary.
        /// Handles database I/O for the renewal process.
        /// </summary>
        /// <param name="connection">The database connection.</param>
        /// <returns>A result object containing renewal status and any failure reason.</returns>
        private static CertificateRenewalResult CheckAndRenewCertificate(Connection connection)
        {
            // Read certificate data from database
            var isAutogenerated = connection.ApplicationSettings.ServerSSLCertificateAutogenerated;
            var serverCert = connection.ApplicationSettings.ServerSSLCertificate;
            var caCert = connection.ApplicationSettings.ServerCACertificate;
            var caKey = connection.ApplicationSettings.ServerCACertificateKey;
            var caPassword = connection.ApplicationSettings.ServerCACertificatePassword;

            CACertificateData caData = null;
            if (!string.IsNullOrEmpty(caCert) && !string.IsNullOrEmpty(caKey) && !string.IsNullOrEmpty(caPassword))
            {
                caData = new CACertificateData
                {
                    CACertificate = caCert,
                    CAKey = caKey,
                    CAPassword = caPassword
                };
            }

            // Check and renew if needed
            var result = CertificateRenewalChecker.CheckAndRenewIfNeeded(isAutogenerated, serverCert, caData);

            // If renewed, store the new certificate in database
            if (result.Renewed && result.RenewedCertificate != null)
            {
                var settings = new Dictionary<string, string>
                {
                    [Database.ServerSettings.CONST.SERVER_SSL_CERTIFICATE] = result.RenewedCertificate.ServerCertificate,
                    [Database.ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD] = result.RenewedCertificate.Password,
                    [Database.ServerSettings.CONST.SERVER_SSL_CERTIFICATE_AUTOGENERATED] = "true"
                };

                connection.ApplicationSettings.UpdateSettings(settings, false);

                // Check if database is encrypted and emit warning if not
                if (!connection.IsEncryptingFields)
                    EmitUnencryptedDatabaseWarning(connection);
            }

            return result;
        }

        /// <summary>
        /// Emits a warning notification if the database is not encrypted.
        /// </summary>
        /// <param name="connection">The database connection.</param>
        private static void EmitUnencryptedDatabaseWarning(Connection connection)
        {
            connection.RegisterNotification(
                Serialization.NotificationType.Warning,
                "Unencrypted database with CA key",
                "The database is not encrypted and contains the CA private key. This is a major security risk and could allow MitM attacks on all HTTPS connections. Consider enabling database encryption.",
                null,
                null,
                NOTIFICATION_CA_KEY_UNENCRYPTED_DATABASE,
                null,
                "UnencryptedDatabaseWithCAKey",
                null,
                (self, all) =>
                {
                    return all.FirstOrDefault(x => x.Action == NOTIFICATION_CA_KEY_UNENCRYPTED_DATABASE) ?? self;
                }
            );
        }

        /// <summary>
        /// Registers a notification for certificate renewal failure.
        /// </summary>
        /// <param name="connection">The database connection.</param>
        /// <param name="reason">The reason for the failure.</param>
        private static void RegisterRenewalFailedNotification(Connection connection, string reason)
        {
            connection.RegisterNotification(
                Serialization.NotificationType.Error,
                "Certificate renewal failed",
                $"The automatic renewal of the HTTPS certificate failed: {reason}. Please run '{PackageHelper.GetExecutableName(PackageHelper.NamedExecutable.ConfigureTool)} https regenerate-ca' to generate new certificates.",
                null,
                null,
                NOTIFICATION_CERTIFICATE_RENEWAL_FAILED,
                null,
                "CertificateRenewalFailed",
                null,
                (self, all) =>
                {
                    return all.FirstOrDefault(x => x.Action == NOTIFICATION_CERTIFICATE_RENEWAL_FAILED) ?? self;
                }
            );
        }

        /// <summary>
        /// The default log retention
        /// </summary>
        private static readonly string DEFAULT_LOG_RETENTION = "30D";

        /// <summary>
        /// The options related to the secret provider
        /// </summary>
        private static readonly IReadOnlyList<ICommandLineArgument> SECRET_PROVIDER_OPTIONS = new Options(new Dictionary<string, string>()).SupportedCommands.Where(x => x.Name.StartsWith("secret-provider")).ToList();

        /// <summary>
        /// Gets additional commandline arguments support on Windows
        /// </summary>
        private static readonly ICommandLineArgument[] WindowsOptions = OperatingSystem.IsWindows()
            ? [
                new CommandLineArgument(WINDOWS_EVENTLOG_OPTION, CommandLineArgument.ArgumentType.String, Strings.Program.LogwindowseventlogShort, Strings.Program.LogwindowseventlogLong),
                new CommandLineArgument(WINDOWS_EVENTLOG_LEVEL_OPTION, CommandLineArgument.ArgumentType.Enumeration, Strings.Program.LogwindowseventloglevelShort, Strings.Program.LogwindowseventloglevelLong, Library.Logging.LogMessageType.Information.ToString(), null, Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType)))
            ]
            : [];

        /// <summary>
        /// Gets a list of all supported commandline options
        /// </summary>
        public static ICommandLineArgument[] SupportedCommands =>
        [
            .. WindowsOptions,
            new CommandLineArgument(TEMPDIR_OPTION, CommandLineArgument.ArgumentType.Path, Strings.Program.TempdirShort, Strings.Program.TempdirLong, System.IO.Path.GetTempPath()),
            new CommandLineArgument(HELP_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.HelpCommandDescription, Strings.Program.HelpCommandDescription),
            new CommandLineArgument(PARAMETERS_FILE_OPTION, CommandLineArgument.ArgumentType.Path, Strings.Program.ParametersFileOptionShort, Strings.Program.ParametersFileOptionLong, "", PARAMETERS_FILE_OPTION_EXTRAS),
            new CommandLineArgument(DataFolderManager.PORTABLE_MODE_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.PortablemodeCommandDescription, Strings.Program.PortablemodeCommandDescription, DataFolderManager.PORTABLE_MODE.ToString().ToLowerInvariant()),
            new CommandLineArgument(LOG_FILE_OPTION, CommandLineArgument.ArgumentType.Path, Strings.Program.LogfileCommandDescription, Strings.Program.LogfileCommandDescription),
            new CommandLineArgument(LOG_LEVEL_OPTION, CommandLineArgument.ArgumentType.Enumeration, Strings.Program.LoglevelCommandDescription, Strings.Program.LoglevelCommandDescription, Library.Logging.LogMessageType.Warning.ToString(), null, Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType))),
            new CommandLineArgument(LOG_CONSOLE_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.LogConsoleDescription, Strings.Program.LogConsoleDescription, false.ToString()),
            new CommandLineArgument(WebServerLoader.OPTION_WEBROOT, CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverWebrootDescription, Strings.Program.WebserverWebrootDescription, WebServerLoader.DEFAULT_OPTION_WEBROOT),
            new CommandLineArgument(WebServerLoader.OPTION_PORT, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverPortDescription, Strings.Program.WebserverPortDescription, WebServerLoader.DEFAULT_OPTION_PORT.ToString()),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_DISABLEHTTPS, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverDisableHTTPSDescription, Strings.Program.WebserverDisableHTTPSDescription),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_REMOVESSLCERTIFICATE, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverRemoveCertificateDescription, Strings.Program.WebserverRemoveCertificateDescription),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_SSLCERTIFICATEFILE, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverCertificateFileDescription, Strings.Program.WebserverCertificateFileDescription),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_SSLCERTIFICATEFILEPASSWORD, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverCertificatePasswordDescription, Strings.Program.WebserverCertificatePasswordDescription),
            new CommandLineArgument(WebServerLoader.OPTION_INTERFACE, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverInterfaceDescription, Strings.Program.WebserverInterfaceDescription, WebServerLoader.DEFAULT_OPTION_INTERFACE),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_PASSWORD, CommandLineArgument.ArgumentType.Password, Strings.Program.WebserverPasswordDescription, Strings.Program.WebserverPasswordDescription),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverAllowedhostnamesDescription, Strings.Program.WebserverAllowedhostnamesDescription, null, [WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES_ALT]),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_RESET_JWT_CONFIG, CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverResetJwtConfigDescription, Strings.Program.WebserverResetJwtConfigDescription),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_ENABLE_FOREVER_TOKEN, CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverEnableForeverTokenDescription, Strings.Program.WebserverEnableForeverTokenDescription),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_DISABLEAPIEXTENSIONS, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverDisableApiExtensionsDescription, Strings.Program.WebserverDisableApiExtensionsDescription),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_API_ONLY, CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverApiOnlyDescription, Strings.Program.WebserverApiOnlyDescription),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_DISABLE_SIGNIN_TOKENS, CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverDisableSigninTokensDescription, Strings.Program.WebserverDisableSigninTokensDescription),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_SPAPATHS, CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverSpaPathsDescription, Strings.Program.WebserverSpaPathsDescription, WebServerLoader.DEFAULT_OPTION_SPAPATHS),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_TIMEZONE, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverTimezoneDescription, Strings.Program.WebserverTimezoneDescription, TimeZoneHelper.GetLocalTimeZone(), null, TimeZoneHelper.GetTimeZones().Select(x => x.Id).ToArray()),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_CORS_ORIGINS, CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverCorsOriginsDescription, Strings.Program.WebserverCorsOriginsDescription, WebServerLoader.DEFAULT_OPTION_SPAPATHS),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_PRE_AUTH_TOKENS, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverPreAuthTokensDescription, Strings.Program.WebserverPreAuthTokensDescription),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_TOKENDURATION, CommandLineArgument.ArgumentType.Timespan, Strings.Program.WebserverTokenDurationDescription, Strings.Program.WebserverTokenDurationDescription, WebServerLoader.DEFAULT_OPTION_TOKENDURATION),
            new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_ENABLE_FOLDER_STATUS_SERVICE, CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverEnableFolderStatusServiceDescription, Strings.Program.WebserverEnableFolderStatusServiceDescription),
            new CommandLineArgument(WebServerLoader.OPTION_CONFIGURE_HTTPS, CommandLineArgument.ArgumentType.Boolean, Strings.Program.ConfigureHttpsShort, Strings.Program.ConfigureHttpsLong),
            new CommandLineArgument(WebServerLoader.OPTION_CONFIGURE_HTTPS_HOSTNAMES, CommandLineArgument.ArgumentType.String, Strings.Program.ConfigureHttpsHostnamesShort, Strings.Program.ConfigureHttpsHostnamesLong),
            new CommandLineArgument(PING_PONG_KEEPALIVE_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.PingpongkeepaliveShort, Strings.Program.PingpongkeepaliveLong),
            new CommandLineArgument(DISABLE_UPDATE_CHECK_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.DisableupdatecheckShort, Strings.Program.DisableupdatecheckLong),
            new CommandLineArgument(LOG_RETENTION_OPTION, CommandLineArgument.ArgumentType.Timespan, Strings.Program.LogretentionShort, Strings.Program.LogretentionLong, DEFAULT_LOG_RETENTION),
            new CommandLineArgument(DataFolderManager.SERVER_DATAFOLDER_OPTION, CommandLineArgument.ArgumentType.Path, Strings.Program.ServerdatafolderShort, Strings.Program.ServerdatafolderLong(DataFolderManager.DATAFOLDER_ENV_NAME), DataFolderManager.GetDataFolder(DataFolderManager.AccessMode.ProbeOnly)),
            new CommandLineArgument(DISABLE_DB_ENCRYPTION_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.DisabledbencryptionShort, Strings.Program.DisabledbencryptionLong),
            new CommandLineArgument(REQUIRE_DB_ENCRYPTION_KEY_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.RequiredbencryptionShort, Strings.Program.RequiredbencryptionLong),
            new CommandLineArgument(SETTINGS_ENCRYPTION_KEY_OPTION, CommandLineArgument.ArgumentType.Password, Strings.Program.SettingsencryptionkeyShort, Strings.Program.SettingsencryptionkeyLong(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME)),
            new CommandLineArgument(REGISTER_REMOTE_CONTROL_OPTION, CommandLineArgument.ArgumentType.String, Strings.Program.RegisterRemoteControlShort, Strings.Program.RegisterRemoteControlLong),
            new CommandLineArgument(REGISTER_REMOTE_CONTROL_REREGISTER_OPTION, CommandLineArgument.ArgumentType.String, Strings.Program.RegisterRemoteControlReregisterShort, Strings.Program.RegisterRemoteControlReregisterLong),
            new CommandLineArgument(ALLOWED_BACKEND_MODULES, CommandLineArgument.ArgumentType.String, Strings.Program.AllowedBackendModulesShort, Strings.Program.AllowedBackendModulesLong),
            new CommandLineArgument(ALLOWED_ENCRYPTION_MODULES, CommandLineArgument.ArgumentType.String, Strings.Program.AllowedEncryptionModulesShort, Strings.Program.AllowedEncryptionModulesLong),
            new CommandLineArgument(ALLOWED_COMPRESSION_MODULES, CommandLineArgument.ArgumentType.String, Strings.Program.AllowedCompressionModulesShort, Strings.Program.AllowedCompressionModulesLong),
            .. SECRET_PROVIDER_OPTIONS
        ];

        /// <summary>
        /// List of known duplicate option names
        /// </summary>
        public static readonly HashSet<string> KnownDuplicateOptions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        /// <summary>
        /// Reads options from a file and applies them to the current commandline arguments and options
        /// </summary>
        /// <param name="filename">The filename</param>
        /// <param name="filter">The filter</param>
        /// <param name="cargs">The commandline arguments</param>
        /// <param name="options">The options</param>
        /// <returns>True if successful</returns>
        private static void ReadOptionsFromFile(string filename, ref IFilter filter, List<string> cargs, Dictionary<string, string> options)
        {
            try
            {
                var fargs = new List<string>(Library.Utility.Utility.ReadFileWithDefaultEncoding(Environment.ExpandEnvironmentVariables(filename)).Replace("\r\n", "\n").Replace("\r", "\n").Split(new String[] { "\n" }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()));
                var newsource = new List<string>();
                string newtarget = null;
                string prependfilter = null;
                string appendfilter = null;
                string replacefilter = null;

                var tmpparsed = FilterCollector.ExtractOptions(fargs, (key, value) =>
                {
                    if (key.Equals("source", StringComparison.OrdinalIgnoreCase))
                    {
                        newsource.Add(value);
                        return false;
                    }
                    else if (key.Equals("target", StringComparison.OrdinalIgnoreCase))
                    {
                        newtarget = value;
                        return false;
                    }
                    else if (key.Equals("append-filter", StringComparison.OrdinalIgnoreCase))
                    {
                        appendfilter = value;
                        return false;
                    }
                    else if (key.Equals("prepend-filter", StringComparison.OrdinalIgnoreCase))
                    {
                        prependfilter = value;
                        return false;
                    }
                    else if (key.Equals("replace-filter", StringComparison.OrdinalIgnoreCase))
                    {
                        replacefilter = value;
                        return false;
                    }

                    return true;
                });

                var opt = tmpparsed.Item1;
                var newfilter = tmpparsed.Item2;

                // If the user specifies parameters-file, all filters must be in the file.
                // Allowing to specify some filters on the command line could result in wrong filter ordering
                if (!filter.Empty && !newfilter.Empty)
                    throw new UserInformationException(Strings.Program.FiltersCannotBeUsedWithFileError2, "FiltersCannotBeUsedOnCommandLineAndInParameterFile");

                if (!newfilter.Empty)
                    filter = newfilter;

                if (!string.IsNullOrWhiteSpace(prependfilter))
                    filter = FilterExpression.Combine(FilterExpression.Deserialize(prependfilter.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries)), filter);

                if (!string.IsNullOrWhiteSpace(appendfilter))
                    filter = FilterExpression.Combine(filter, FilterExpression.Deserialize(appendfilter.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries)));

                if (!string.IsNullOrWhiteSpace(replacefilter))
                    filter = FilterExpression.Deserialize(replacefilter.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries));

                foreach (var keyvalue in opt)
                    options[keyvalue.Key] = keyvalue.Value;

                if (!string.IsNullOrEmpty(newtarget))
                {
                    if (cargs.Count <= 1)
                        cargs.Add(newtarget);
                    else
                        cargs[1] = newtarget;
                }

                if (cargs.Count >= 1 && cargs[0].Equals("backup", StringComparison.OrdinalIgnoreCase))
                    cargs.AddRange(newsource);
                else if (newsource.Count > 0)
                    Log.WriteVerboseMessage(LOGTAG, "NotUsingBackupSources", Strings.Program.SkippingSourceArgumentsOnNonBackupOperation);
            }
            catch (Exception e)
            {
                throw new Exception(Strings.Program.FailedToParseParametersFileError(filename, e.Message));
            }
        }
    }
}
