// Copyright (C) 2025, 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.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using CoCoL;
using Duplicati.CommandLine;
using Duplicati.Library.AutoUpdater;
using Duplicati.Library.Interface;
using Duplicati.Library.Utility;
using Duplicati.Server;
using Duplicati.Server.Database;
using Duplicati.WebserverCore.Services;
using Microsoft.Extensions.DependencyInjection;
using Uri = System.Uri;

namespace Duplicati.GUI.TrayIcon
{
    public static class Program
    {
        /// <summary>
        /// The source of the password
        /// </summary>
        public enum PasswordSource
        {
            /// <summary>
            /// No password, using token information from the database
            /// </summary>
            Database,
            /// <summary>
            /// No password supplied, using the hosted server
            /// </summary>
            HostedServer,
            /// <summary>
            /// The password was supplied on the commandline
            /// </summary>
            SuppliedPassword
        }

        /// <summary>
        /// The connection to the server
        /// </summary>
        public static HttpServerConnection Connection;

        /// <summary>
        /// The default secret provider
        /// </summary>
        public static PasswordStorageHelper PasswordStorage;

        private const string HOSTURL_OPTION = "hosturl";
        private const string NOHOSTEDSERVER_OPTION = "no-hosted-server";
        private const string READCONFIGFROMDB_OPTION = "read-config-from-db";
        private const string ACCEPTED_SSL_CERTIFICATE = "host-cert-hash";

        private const string DETACHED_PROCESS = "detached-process";
        private const string BROWSER_COMMAND_OPTION = "browser-command";
        private static readonly IReadOnlySet<string> DETATCHED_WEBERVER_OPTIONS = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            WebServerLoader.OPTION_WEBSERVICE_PASSWORD,
            WebServerLoader.OPTION_PORT,
            DataFolderManager.SERVER_DATAFOLDER_OPTION,
            DataFolderManager.PORTABLE_MODE_OPTION
        };


        private static string DEFAULT_HOSTURL => $"http://{Utility.IpVersionCompatibleLoopback}:8200";

        private static string _browser_command = null;
        private static bool disableTrayIconLogin = false;
        private static bool openui = false;
        public static string BrowserCommand { get { return _browser_command; } }
        public static Server.Database.Connection databaseConnection = null;

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        public static int Main(string[] _args)
        {
            PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, PackageHelper.NamedExecutable.TrayIcon);
            var args = new List<string>(_args);
            var options = CommandLineParser.ExtractOptions(args);

            if (OperatingSystem.IsWindows() && !Utility.ParseBoolOption(options, DETACHED_PROCESS))
                Win32.AttachConsole(Win32.ATTACH_PARENT_PROCESS);

            if (HelpOptionExtensions.IsArgumentAnyHelpString(_args))
            {
                Console.WriteLine("Supported commandline arguments:");
                Console.WriteLine();

                foreach (ICommandLineArgument arg in AllSupportedCommands)
                    Console.WriteLine("--{0}: {1}", arg.Name, arg.LongDescription);

                Console.WriteLine("Additionally, these server options are also supported:");
                Console.WriteLine();

                foreach (ICommandLineArgument arg in WebserverSupportedCommands)
                    Console.WriteLine("--{0}: {1}", arg.Name, arg.LongDescription);

                return 0;
            }

            // Apply secret provider, if any, so password etc can be retrieved
            Library.Main.SecretProviderHelper.ApplySecretProviderAsync([], [], options, TempFolder.SystemTempPath, null, CancellationToken.None)
                .Await();

            options.TryGetValue(BROWSER_COMMAND_OPTION, out _browser_command);

            HostedInstanceKeeper hosted = null;

            string password = null;
            var passwordSource = PasswordSource.SuppliedPassword;
            var acceptedHostCertificate = options.GetValueOrDefault(ACCEPTED_SSL_CERTIFICATE, null);
            var detached = Utility.ParseBoolOption(options, NOHOSTEDSERVER_OPTION);

            var supportedCommands = BasicSupportedCommands.AsEnumerable();
            if (detached)
            {
                supportedCommands = DetachSupportedCommands
                    .Concat(supportedCommands);
            }
            else
            {
                supportedCommands = supportedCommands.Concat(WebserverSupportedCommands);
            }

            // Validate options, and log to console
            using (var logger = new ConsoleOutput(Console.Out, options))
                CommandLineArgumentValidator.ValidateArguments(supportedCommands, options, Server.Program.KnownDuplicateOptions, new HashSet<string>());

            Uri serverURL = new Uri(DEFAULT_HOSTURL);

            if (!detached)
            {
                try
                {
                    // Tell the hosted server it was started by the TrayIcon
                    var applicationSettings = new ApplicationSettings
                    {
                        Origin = "Tray icon"
                    };
                    passwordSource = PasswordSource.HostedServer;
                    // Ignore TrayIcon specific settings
                    foreach (var c in BasicSupportedCommands.Select(x => x.Name))
                        Server.Program.ValidationIgnoredOptions.Add(c);
                    hosted = new HostedInstanceKeeper(applicationSettings, _args);
                }
                catch (Exception ex)
                {
                    if (ex.InnerException != null && ex.InnerException is Server.SingleInstance.MultipleInstanceException)
                    {
                        Console.WriteLine(Server.Strings.Program.AnotherInstanceDetected);
                        return 1;
                    }
                    else
                        throw;
                }

                // We have a hosted server, if this is the first run,
                // we should open the main page
                var connection = Server.Program.DuplicatiWebserver.Provider.GetRequiredService<Connection>();
                openui = connection.ApplicationSettings.IsFirstRun || connection.ApplicationSettings.ServerPortChanged;

                var scheme = connection.ApplicationSettings.UseHTTPS ? "https" : "http";
                serverURL = new UriBuilder(serverURL)
                {
                    Port = Server.Program.DuplicatiWebserver.Port,
                    Scheme = scheme
                }.Uri;

                if (connection.ApplicationSettings.UseHTTPS && string.IsNullOrWhiteSpace(acceptedHostCertificate))
                    acceptedHostCertificate = connection.ApplicationSettings.ServerSSLCertificate?.FirstOrDefault(x => x.HasPrivateKey)?.GetCertHashString();

            }
            else if (Utility.ParseBoolOption(options, READCONFIGFROMDB_OPTION))
            {
                if (File.Exists(Path.Combine(DataFolderManager.GetDataFolder(DataFolderManager.AccessMode.ReadWritePermissionSet), DataFolderManager.SERVER_DATABASE_FILENAME)))
                {
                    passwordSource = PasswordSource.Database;
                    databaseConnection = Server.Program.GetDatabaseConnection(new ApplicationSettings(), options, true, false);

                    if (databaseConnection != null)
                    {
                        disableTrayIconLogin = databaseConnection.ApplicationSettings.DisableTrayIconLogin;
                        if (databaseConnection.ApplicationSettings.UseHTTPS && string.IsNullOrWhiteSpace(acceptedHostCertificate))
                            acceptedHostCertificate = databaseConnection.ApplicationSettings.ServerSSLCertificate?.FirstOrDefault(x => x.HasPrivateKey)?.GetCertHashString();

                        var scheme = databaseConnection.ApplicationSettings.UseHTTPS ? "https" : "http";
                        serverURL = new UriBuilder(serverURL)
                        {
                            Port = databaseConnection.ApplicationSettings.LastWebserverPort == -1 ? serverURL.Port : databaseConnection.ApplicationSettings.LastWebserverPort,
                            Scheme = scheme
                        }.Uri;
                    }
                }
            }

            // Legacy undocumented way to provide the password
            if (options.TryGetValue("webserver-password", out var pwd))
                password = pwd;
            if (options.TryGetValue(WebServerLoader.OPTION_WEBSERVICE_PASSWORD, out pwd))
                password = pwd;

            // Let the user specify the port, if they are not providing a hosturl
            var customUrl = false;
            if (!options.ContainsKey(HOSTURL_OPTION) && options.TryGetValue(WebServerLoader.OPTION_PORT, out var portString) && int.TryParse(portString, out var port))
            {
                serverURL = new UriBuilder(serverURL) { Port = port }.Uri;
                customUrl = true;
            }

            if (options.TryGetValue(HOSTURL_OPTION, out var url))
            {
                serverURL = new Uri(url);
                customUrl = true;
            }

            StartTray(_args, options, hosted, passwordSource, password, serverURL, customUrl, acceptedHostCertificate);

            return 0;
        }

        private static void StartTray(string[] _args, Dictionary<string, string> options, HostedInstanceKeeper hosted, PasswordSource passwordSource, string password, Uri serverURL, bool customUrl, string acceptedHostCertificate)
        {
            PasswordStorage = PasswordStorageHelper.CreateAsync(serverURL.ToString(), customUrl, password, passwordSource, options)
                .Await();

            using (hosted)
            {
                var reSpawn = 0;

                do
                {
                    if (reSpawn > 0)
                        Thread.Sleep(1000);

                    try
                    {
                        using (Connection = new HttpServerConnection(hosted?.applicationSettings, passwordSource, disableTrayIconLogin, acceptedHostCertificate, options, PasswordStorage))
                        {
                            // Make sure we have the latest status, but don't care if it fails
                            Connection.UpdateStatus().FireAndForget();
                            using (var tk = RunTrayIcon())
                            {
                                if (Server.Program.ApplicationInstance != null)
                                    Server.Program.ApplicationInstance.SecondInstanceDetected +=
                                        x => tk.ShowStatusWindow();

                                // TODO: If we change to hosted browser this should be a callback
                                if (openui)
                                {
                                    Connection.GetStatusWindowURLAsync().ContinueWith(t =>
                                    {
                                        if (t.IsFaulted)
                                        {
                                            Console.WriteLine("Failed to get status window URL: " + t.Exception.Message);
                                            tk.NotifyUser("Failed to get status window URL", t.Exception.Message, NotificationType.Error);
                                            return;
                                        }

                                        tk.ShowUrlInWindow(t.Result);
                                        var connection = Server.Program.DuplicatiWebserver.Provider.GetRequiredService<Connection>();
                                        connection.ApplicationSettings.IsFirstRun = false;
                                        connection.ApplicationSettings.ServerPortChanged = false;

                                    });
                                }

                                // If the server shuts down, shut down the tray-icon as well
                                Action shutdownEvent = () =>
                                {
                                    // Make sure we do not start again after
                                    // a controlled exit
                                    reSpawn = 100;
                                    tk.InvokeExit();
                                };

                                Connection.ConnectionClosed = shutdownEvent;
                                if (hosted != null)
                                    hosted.InstanceShutdown = shutdownEvent;

                                tk.Init(_args);

                                // If the tray-icon quits, stop the server
                                reSpawn = 100;

                                // Make sure that the server shutdown does not access the tray-icon,
                                // as it would be disposed by now
                                if (hosted != null)
                                    hosted.InstanceShutdown = null;
                                Connection.ConnectionClosed = null;

                            }
                        }

                    }
                    catch (HttpRequestException ex)
                    {
                        System.Diagnostics.Trace.WriteLine("Request error: " + ex);
                        Console.WriteLine("Request error: " + ex);

                        reSpawn++;
                    }
                    catch (Exception ex)
                    {
                        System.Diagnostics.Trace.WriteLine("Unexpected error: " + ex);
                        Console.WriteLine("Unexpected error: " + ex);
                        return;
                    }
                } while (reSpawn < 3);
            }
        }

        private static TrayIconBase RunTrayIcon()
            => new AvaloniaRunner();

        public static ICommandLineArgument[] DetachSupportedCommands =>
        [
            new CommandLineArgument(NOHOSTEDSERVER_OPTION, CommandLineArgument.ArgumentType.String, "Disables local server", "Set this option to not spawn a local service, use if the TrayIcon should connect to a running service"),
            new CommandLineArgument(READCONFIGFROMDB_OPTION, CommandLineArgument.ArgumentType.String, "Read server connection info from DB", $"Set this option to read server connection info for running service from its database (only together with {NOHOSTEDSERVER_OPTION})"),
            .. WebserverSupportedCommands.Where(x => DETATCHED_WEBERVER_OPTIONS.Contains(x.Name))
        ];

        public static ICommandLineArgument[] BasicSupportedCommands =>
        [
            new CommandLineArgument(HOSTURL_OPTION, CommandLineArgument.ArgumentType.String, "Selects the url to connect to", "Supply the url that the TrayIcon will connect to and show status for", DEFAULT_HOSTURL),
            new CommandLineArgument(BROWSER_COMMAND_OPTION, CommandLineArgument.ArgumentType.String, "Sets the browser command", "Set this option to override the default browser detection"),
            new CommandLineArgument(ACCEPTED_SSL_CERTIFICATE, CommandLineArgument.ArgumentType.String, "Accepts a specific SSL certificate", "Set this option to accept a specific SSL certificate, the value should be the hash of the certificate in hexadecimal format. Use * to accept any certificate (dangerous)"),
            .. WindowsSupportedCommands
        ];

        private static ICommandLineArgument[] WindowsSupportedCommands =>
            OperatingSystem.IsWindows()
                ? [new CommandLineArgument(DETACHED_PROCESS, CommandLineArgument.ArgumentType.String, "Runs the tray-icon detached", "This option runs the tray-icon in detached mode, meaning that the process will exit immediately and not send output to the console of the caller")]
                : [];

        public static ICommandLineArgument[] WebserverSupportedCommands
            => Server.Program.SupportedCommands;

        public static ICommandLineArgument[] AllSupportedCommands =>
        [
            .. DetachSupportedCommands,
            .. BasicSupportedCommands
        ];
    }
}
