#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Stores;
using BTCPayServer.Storage.Services;
using BTCPayServer.Storage.Services.Providers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MimeKit;
using NBitcoin;
using NBitcoin.DataEncoders;
using Renci.SshNet;
using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes;

namespace BTCPayServer.Controllers
{
    [Authorize(Policy = Client.Policies.CanModifyServerSettings,
               AuthenticationSchemes = AuthenticationSchemes.Cookie)]
    public partial class UIServerController : Controller
    {
        private readonly UserManager<ApplicationUser> _UserManager;
        private readonly UserService _userService;
        readonly SettingsRepository _SettingsRepository;
        readonly PoliciesSettings _policiesSettings;
        private readonly NBXplorerDashboard _dashBoard;
        private readonly StoreRepository _StoreRepository;
        readonly LightningConfigurationProvider _LnConfigProvider;
        private readonly TorServices _torServices;
        private readonly BTCPayServerOptions _Options;
        private readonly AppService _AppService;
        private readonly CheckConfigurationHostedService _sshState;
        private readonly EventAggregator _eventAggregator;
        private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
        private readonly Logs Logs;
        private readonly StoredFileRepository _StoredFileRepository;
        private readonly IFileService _fileService;
        private readonly IEnumerable<IStorageProviderService> _StorageProviderServices;
        private readonly CallbackGenerator _callbackGenerator;
        private readonly UriResolver _uriResolver;
        private readonly EmailSenderFactory _emailSenderFactory;
        private readonly TransactionLinkProviders _transactionLinkProviders;
        private readonly LocalizerService _localizer;
        public IStringLocalizer StringLocalizer { get; }

        public UIServerController(
            UserManager<ApplicationUser> userManager,
            UserService userService,
            StoredFileRepository storedFileRepository,
            IFileService fileService,
            IEnumerable<IStorageProviderService> storageProviderServices,
            BTCPayServerOptions options,
            SettingsRepository settingsRepository,
            PoliciesSettings policiesSettings,
            NBXplorerDashboard dashBoard,
            IHttpClientFactory httpClientFactory,
            LightningConfigurationProvider lnConfigProvider,
            TorServices torServices,
            StoreRepository storeRepository,
            AppService appService,
            CheckConfigurationHostedService sshState,
            EventAggregator eventAggregator,
            IOptions<ExternalServicesOptions> externalServiceOptions,
            Logs logs,
            CallbackGenerator callbackGenerator,
            UriResolver uriResolver,
            EmailSenderFactory emailSenderFactory,
            IHostApplicationLifetime applicationLifetime,
            IHtmlHelper html,
            TransactionLinkProviders transactionLinkProviders,
            LocalizerService localizer,
            IStringLocalizer stringLocalizer,
            BTCPayServerEnvironment environment
        )
        {
            _policiesSettings = policiesSettings;
            _Options = options;
            _StoredFileRepository = storedFileRepository;
            _fileService = fileService;
            _StorageProviderServices = storageProviderServices;
            _UserManager = userManager;
            _userService = userService;
            _SettingsRepository = settingsRepository;
            _dashBoard = dashBoard;
            HttpClientFactory = httpClientFactory;
            _StoreRepository = storeRepository;
            _LnConfigProvider = lnConfigProvider;
            _torServices = torServices;
            _AppService = appService;
            _sshState = sshState;
            _eventAggregator = eventAggregator;
            _externalServiceOptions = externalServiceOptions;
            Logs = logs;
            _callbackGenerator = callbackGenerator;
            _uriResolver = uriResolver;
            _emailSenderFactory = emailSenderFactory;
            ApplicationLifetime = applicationLifetime;
            Html = html;
            _transactionLinkProviders = transactionLinkProviders;
            _localizer = localizer;
            Environment = environment;
            StringLocalizer = stringLocalizer;
        }

        [HttpGet("server/stores")]
        public async Task<IActionResult> ListStores()
        {
            var stores = await _StoreRepository.GetStores();
            var vm = new ListStoresViewModel
            {
                Stores = stores
                    .Select(s => new ListStoresViewModel.StoreViewModel
                    {
                        StoreId = s.Id,
                        StoreName = s.StoreName,
                        Archived = s.Archived,
                        Users = s.UserStores
                    })
                    .OrderBy(s => !s.Archived)
                    .ToList()
            };
            return View(vm);
        }

        [HttpGet("server/maintenance")]
        public IActionResult Maintenance()
        {
            var vm = new MaintenanceViewModel
            {
                CanUseSSH = _sshState.CanUseSSH,
                DNSDomain = Request.Host.Host
            };

            if (!vm.CanUseSSH)
                TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Maintenance feature requires access to SSH properly configured in BTCPay Server configuration."].Value;
            if (IPAddress.TryParse(vm.DNSDomain, out var unused))
                vm.DNSDomain = null;

            return View(vm);
        }

        [HttpPost("server/maintenance")]
        public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
        {
            vm.CanUseSSH = _sshState.CanUseSSH;
            if (command != "soft-restart" && !vm.CanUseSSH)
            {
                TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Maintenance feature requires access to SSH properly configured in BTCPay Server configuration."].Value;
                return View(vm);
            }
            if (!ModelState.IsValid)
                return View(vm);
            
            if (command == "changedomain")
            {
                if (string.IsNullOrWhiteSpace(vm.DNSDomain))
                {
                    ModelState.AddModelError(nameof(vm.DNSDomain), $"Required field");
                    return View(vm);
                }
                vm.DNSDomain = vm.DNSDomain.Trim().ToLowerInvariant();
                if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.OrdinalIgnoreCase))
                    return View(vm);
                if (IPAddress.TryParse(vm.DNSDomain, out var unused))
                {
                    ModelState.AddModelError(nameof(vm.DNSDomain), $"This should be a domain name");
                    return View(vm);
                }
                if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.InvariantCultureIgnoreCase))
                {
                    ModelState.AddModelError(nameof(vm.DNSDomain), $"The server is already set to use this domain");
                    return View(vm);
                }
                var builder = new UriBuilder();
                try
                {
                    builder.Scheme = this.Request.Scheme;
                    builder.Host = vm.DNSDomain;
                    var addresses1 = GetAddressAsync(this.Request.Host.Host);
                    var addresses2 = GetAddressAsync(vm.DNSDomain);
                    await Task.WhenAll(addresses1, addresses2);

                    var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
                    var hasCommonAddress = addresses2.GetAwaiter().GetResult().Select(c => c.ToString()).Any(s => addressesSet.Contains(s));
                    if (!hasCommonAddress)
                    {
                        ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
                        return View(vm);
                    }
                }
                catch (Exception ex)
                {
                    var messages = new List<object>();
                    messages.Add(ex.Message);
                    if (ex.InnerException != null)
                        messages.Add(ex.InnerException.Message);
                    ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid domain ({string.Join(", ", messages.ToArray())})");
                    return View(vm);
                }

                var error = await RunSSH(vm, $"changedomain.sh {vm.DNSDomain}");
                if (error != null)
                    return error;

                builder.Path = null;
                builder.Query = null;
                TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Domain name changing... the server will restart, please use \"{0}\" (this page won't reload automatically)", builder.Uri.AbsoluteUri].Value;
            }
            else if (command == "update")
            {
                var error = await RunSSH(vm, $"btcpay-update.sh");
                if (error != null)
                    return error;
                TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The server might restart soon if an update is available... (this page won't reload automatically)"].Value;
            }
            else if (command == "clean")
            {
                var error = await RunSSH(vm, $"btcpay-clean.sh");
                if (error != null)
                    return error;
                TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The old docker images will be cleaned soon..."].Value;
            }
            else if (command == "restart")
            {
                var error = await RunSSH(vm, $"btcpay-restart.sh");
                if (error != null)
                    return error;
                Logs.PayServer.LogInformation("A hard restart has been requested");
                TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["BTCPay will restart momentarily."].Value;
            }
            else if (command == "soft-restart")
            {
                TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["BTCPay will restart momentarily."].Value;
                Logs.PayServer.LogInformation("A soft restart has been requested");
                _ = Task.Delay(3000).ContinueWith((t) => ApplicationLifetime.StopApplication());
            }
            else
            {
                return NotFound();
            }
            return RedirectToAction(nameof(Maintenance));
        }

        private Task<IPAddress[]> GetAddressAsync(string domainOrIP)
        {
            if (IPAddress.TryParse(domainOrIP, out var ip))
                return Task.FromResult(new[] { ip });
            return Dns.GetHostAddressesAsync(domainOrIP);
        }

        public static string RunId = Encoders.Hex.EncodeData(NBitcoin.RandomUtils.GetBytes(32));
        [HttpGet]
        [Route("runid")]
        [AllowAnonymous]
        public IActionResult SeeRunId(string? expected = null)
        {
            if (expected == RunId)
                return Ok();
            return BadRequest();
        }

        private async Task<IActionResult?> RunSSH(MaintenanceViewModel vm, string command)
        {
            SshClient? sshClient = null;

            try
            {
                sshClient = await _Options.SSHSettings.ConnectAsync();
            }
            catch (Exception ex)
            {
                var message = ex.Message;
                if (ex is AggregateException aggrEx && aggrEx.InnerException?.Message != null)
                {
                    message = aggrEx.InnerException.Message;
                }
                ModelState.AddModelError(string.Empty, $"Connection problem ({message})");
                return View(vm);
            }
            _ = RunSSHCore(sshClient, $". /etc/profile.d/btcpay-env.sh && nohup {command} > /dev/null 2>&1 & disown");
            return null;
        }

        private async Task RunSSHCore(SshClient sshClient, string ssh)
        {
            try
            {
                Logs.PayServer.LogInformation("Running SSH command: " + ssh);
                var result = await sshClient.RunBash(ssh, TimeSpan.FromMinutes(1.0));
                Logs.PayServer.LogInformation($"SSH command executed with exit status {result.ExitStatus}. Output: {result.Output}");
            }
            catch (Exception ex)
            {
                Logs.PayServer.LogWarning("Error while executing SSH command: " + ex.Message);
            }
            finally
            {
                sshClient.Dispose();
            }
        }

        public IHttpClientFactory HttpClientFactory { get; }
        public IHostApplicationLifetime ApplicationLifetime { get; }
        public IHtmlHelper Html { get; }
        public BTCPayServerEnvironment Environment { get; }

        [Route("server/policies")]
        public async Task<IActionResult> Policies()
        {
            await UpdateViewBag();
            return View(_policiesSettings);
        }

        private async Task UpdateViewBag()
        {
            ViewBag.UpdateUrlPresent = _Options.UpdateUrl != null;
            ViewBag.AppsList = await GetAppSelectList();
            ViewBag.LangDictionaries = await GetLangDictionariesSelectList();
        }

        [HttpPost("server/policies")]
        public async Task<IActionResult> Policies([FromServices] BTCPayNetworkProvider btcPayNetworkProvider, PoliciesSettings settings, string command = "")
        {
            await UpdateViewBag();

            if (command == "add-domain")
            {
                ModelState.Clear();
                settings.DomainToAppMapping.Add(new PoliciesSettings.DomainToAppMappingItem());
                return View(settings);
            }
            if (command.StartsWith("remove-domain", StringComparison.InvariantCultureIgnoreCase))
            {
                ModelState.Clear();
                var index = int.Parse(command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), CultureInfo.InvariantCulture);
                settings.DomainToAppMapping.RemoveAt(index);
                return View(settings);
            }
            settings.BlockExplorerLinks = settings.BlockExplorerLinks
                                            .Where(tuple => _transactionLinkProviders.GetDefaultBlockExplorerLink(tuple.PaymentMethodId) != tuple.Link)
                                            .Where(tuple => tuple.Link is not null)
                                            .ToList();

            if (!ModelState.IsValid)
            {
                return View(settings);
            }
            var appIdsToFetch = settings.DomainToAppMapping.Select(item => item.AppId).ToList();
            if (!string.IsNullOrEmpty(settings.RootAppId))
            {
                appIdsToFetch.Add(settings.RootAppId);
            }
            else
            {
                settings.RootAppType = null;
            }

            if (appIdsToFetch.Any())
            {
                var apps = (await _AppService.GetApps(appIdsToFetch.ToArray()))
                    .ToDictionary(data => data.Id, data => data.AppType);
                ;
                if (!string.IsNullOrEmpty(settings.RootAppId))
                {
                    settings.RootAppType = apps[settings.RootAppId];
                }

                foreach (var domainToAppMappingItem in settings.DomainToAppMapping)
                {
                    domainToAppMappingItem.AppType = apps[domainToAppMappingItem.AppId];
                }
            }
            

            await _SettingsRepository.UpdateSetting(settings);
            _ = _transactionLinkProviders.RefreshTransactionLinkTemplates();
            if (_policiesSettings.LangDictionary != settings.LangDictionary)
                await _localizer.Load();
            TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Policies updated successfully"].Value;
            return RedirectToAction(nameof(Policies));
        }

        [Route("server/services")]
        public IActionResult Services()
        {
            var result = new ServicesViewModel { ExternalServices = _externalServiceOptions.Value.ExternalServices.ToList() };

            // other services
            foreach (var externalService in _externalServiceOptions.Value.OtherExternalServices)
            {
                result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
                {
                    Name = externalService.Key,
                    Link = Request.GetAbsoluteUriNoPathBase(externalService.Value).AbsoluteUri
                });
            }
            if (CanShowSSHService())
            {
                result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
                {
                    Name = "SSH",
                    Link = Url.Action(nameof(SSHService))
                });
            }
            result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
            {
                Name = "Dynamic DNS",
                Link = Url.Action(nameof(DynamicDnsServices))
            });
            foreach (var torService in _torServices.Services)
            {
                if (torService.VirtualPort == 80)
                {
                    result.TorHttpServices.Add(new ServicesViewModel.OtherExternalService()
                    {
                        Name = torService.Name,
                        Link = $"http://{torService.OnionHost}"
                    });
                }
                else if (TryParseAsExternalService(torService, out var externalService))
                {
                    result.ExternalServices.Add(externalService);
                }
                else
                {
                    result.TorOtherServices.Add(new ServicesViewModel.OtherExternalService()
                    {
                        Name = torService.Name,
                        Link = $"{torService.OnionHost}:{torService.VirtualPort}"
                    });
                }
            }

            return View(result);
        }

        private async Task<List<SelectListItem>> GetAppSelectList()
        {
            var types = _AppService.GetAvailableAppTypes();
            var apps = (await _AppService.GetAllApps(null, true))
                .Select(a =>
                    new SelectListItem($"{types[a.AppType]} - {a.AppName} - {a.StoreName}", a.Id)).ToList();
            apps.Insert(0, new SelectListItem("(None)", null));
            return apps;
        }

        private async Task<List<SelectListItem>> GetLangDictionariesSelectList()
        {
            var dictionaries = await this._localizer.GetDictionaries();
            return dictionaries.Select(d => new SelectListItem(d.DictionaryName, d.DictionaryName)).OrderBy(d => d.Value).ToList();
        }

        private static bool TryParseAsExternalService(TorService torService, [MaybeNullWhen(false)] out ExternalService externalService)
        {
            externalService = null;
            if (torService.ServiceType == TorServiceType.P2P)
            {
                externalService = new ExternalService()
                {
                    CryptoCode = torService.Network.CryptoCode,
                    DisplayName = "Full node P2P",
                    Type = ExternalServiceTypes.P2P,
                    ConnectionString = new ExternalConnectionString(new Uri($"bitcoin-p2p://{torService.OnionHost}:{torService.VirtualPort}", UriKind.Absolute)),
                    ServiceName = torService.Name,
                };
            }
            if (torService.ServiceType == TorServiceType.RPC)
            {
                externalService = new ExternalService()
                {
                    CryptoCode = torService.Network.CryptoCode,
                    DisplayName = "Full node RPC",
                    Type = ExternalServiceTypes.RPC,
                    ConnectionString = new ExternalConnectionString(new Uri($"btcrpc://btcrpc:btcpayserver4ever@{torService.OnionHost}:{torService.VirtualPort}?label=BTCPayNode", UriKind.Absolute)),
                    ServiceName = torService.Name
                };
            }
            return externalService != null;
        }

        private ExternalService? GetService(string serviceName, string cryptoCode)
        {
            var result = _externalServiceOptions.Value.ExternalServices.GetService(serviceName, cryptoCode);
            if (result != null)
                return result;
            foreach (var torService in _torServices.Services)
            {
                if (TryParseAsExternalService(torService, out var torExternalService) &&
                    torExternalService.ServiceName == serviceName)
                    return torExternalService;
            }
            return null;
        }

        [Route("server/services/{serviceName}/{cryptoCode?}")]
        public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, ulong? nonce = null)
        {
            var service = GetService(serviceName, cryptoCode);
            if (service == null)
                return NotFound();
            if (!string.IsNullOrEmpty(cryptoCode) && !_dashBoard.IsFullySynched(cryptoCode, out _) && service.Type != ExternalServiceTypes.RPC)
            {
                TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["{0} is not fully synched", cryptoCode].Value;
                return RedirectToAction(nameof(Services));
            }
            try
            {

                if (service.Type == ExternalServiceTypes.P2P)
                {
                    return View("P2PService", new LightningWalletServices()
                    {
                        ShowQR = showQR,
                        WalletName = service.ServiceName,
                        ServiceLink = service.ConnectionString.Server.AbsoluteUri.WithoutEndingSlash()
                    });
                }
                if (service.Type == ExternalServiceTypes.LNDSeedBackup)
                {
                    var model = LndSeedBackupViewModel.Parse(service.ConnectionString.CookieFilePath);
                    if (!model.IsWalletUnlockPresent)
                    {
                        TempData.SetStatusMessageModel(new StatusMessageModel()
                        {
                            Severity = StatusMessageModel.StatusSeverity.Warning,
                            Html = "Your LND does not seem to allow seed backup.<br />" +
                            "It's recommended, but not required, that you migrate as instructed by <a href=\"https://blog.btcpayserver.org/btcpay-lnd-migration\">our migration blog post</a>.<br />" +
                            "You will need to close all of your channels, and migrate your funds as <a href=\"https://blog.btcpayserver.org/btcpay-lnd-migration\">we documented</a>."
                        });
                    }
                    return View("LndSeedBackup", model);
                }
                if (service.Type == ExternalServiceTypes.RPC)
                {
                    return View("RPCService", new LightningWalletServices()
                    {
                        ShowQR = showQR,
                        WalletName = service.ServiceName,
                        ServiceLink = service.ConnectionString.Server.AbsoluteUri.WithoutEndingSlash()
                    });
                }
                var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type, _Options.NetworkType);
                switch (service.Type)
                {
                    case ExternalServiceTypes.Charge:
                        return LightningChargeServices(service, connectionString, showQR);
                    case ExternalServiceTypes.RTL:
                    case ExternalServiceTypes.ThunderHub:
                    case ExternalServiceTypes.Spark:
                    case ExternalServiceTypes.Torq:
                        if (connectionString.AccessKey == null)
                        {
                            TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["The access key of the service is not set"].Value;
                            return RedirectToAction(nameof(Services));
                        }
                        LightningWalletServices vm = new LightningWalletServices();
                        vm.ShowQR = showQR;
                        vm.WalletName = service.DisplayName;
                        string tokenParam = "access-key";
                        if (service.Type == ExternalServiceTypes.ThunderHub)
                            tokenParam = "token";
                        vm.ServiceLink = $"{connectionString.Server}?{tokenParam}={connectionString.AccessKey}";
                        return View("LightningWalletServices", vm);
                    case ExternalServiceTypes.CLightningRest:
                        return LndServices(service, connectionString, nonce, "CLightningRestServices");
                    case ExternalServiceTypes.LNDGRPC:
                    case ExternalServiceTypes.LNDRest:
                        return LndServices(service, connectionString, nonce);
                    case ExternalServiceTypes.Configurator:
                        return View("ConfiguratorService",
                            new LightningWalletServices()
                            {
                                ShowQR = showQR,
                                WalletName = service.ServiceName,
                                ServiceLink = $"{connectionString.Server}?password={connectionString.AccessKey}"
                            });
                    default:
                        throw new NotSupportedException(service.Type.ToString());
                }
            }
            catch (Exception ex)
            {
                TempData[WellKnownTempData.ErrorMessage] = ex.Message;
                return RedirectToAction(nameof(Services));
            }
        }

        [HttpGet("server/services/{serviceName}/{cryptoCode}/removelndseed")]
        public IActionResult RemoveLndSeed(string serviceName, string cryptoCode)
        {
            return View("Confirm", new ConfirmModel(StringLocalizer["Delete LND seed"], StringLocalizer["This action will permanently delete your LND seed and password. You will not be able to recover them if you don't have a backup. Are you sure?"], StringLocalizer["Delete"]));
        }

        [HttpPost("server/services/{serviceName}/{cryptoCode}/removelndseed")]
        public async Task<IActionResult> RemoveLndSeedPost(string serviceName, string cryptoCode)
        {
            var service = GetService(serviceName, cryptoCode);
            if (service == null)
                return NotFound();

            var model = LndSeedBackupViewModel.Parse(service.ConnectionString.CookieFilePath);
            if (!model.IsWalletUnlockPresent)
            {
                TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["File with wallet password and seed info not present"].Value;
                return RedirectToAction(nameof(Services));
            }

            if (string.IsNullOrEmpty(model.Seed))
            {
                TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Seed information was already removed"].Value;
                return RedirectToAction(nameof(Services));
            }

            if (await model.RemoveSeedAndWrite(service.ConnectionString.CookieFilePath))
            {
                TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Seed successfully removed"].Value;
                return RedirectToAction(nameof(Service), new { serviceName, cryptoCode });
            }
            else
            {
                TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Seed removal failed"].Value;
                return RedirectToAction(nameof(Services));
            }
        }

        private IActionResult LightningChargeServices(ExternalService service, ExternalConnectionString connectionString, bool showQR = false)
        {
            ChargeServiceViewModel vm = new ChargeServiceViewModel();
            vm.Uri = connectionString.Server.AbsoluteUri;
            vm.APIToken = connectionString.APIToken;
            var builder = new UriBuilder(connectionString.Server);
            builder.UserName = "api-token";
            builder.Password = vm.APIToken;
            vm.AuthenticatedUri = builder.ToString();
            return View(nameof(LightningChargeServices), vm);
        }

        private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, ulong? nonce, string view = nameof(LndServices))
        {
            var model = new LndServicesViewModel();
            if (service.Type == ExternalServiceTypes.LNDGRPC)
            {
                model.Host = $"{connectionString.Server.DnsSafeHost}:{connectionString.Server.Port}";
                model.SSL = connectionString.Server.Scheme == "https";
                model.ConnectionType = "GRPC";
                model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
            }
            else if (service.Type == ExternalServiceTypes.LNDRest || service.Type == ExternalServiceTypes.CLightningRest)
            {
                model.Uri = connectionString.Server.AbsoluteUri;
                model.ConnectionType = "REST";
            }

            if (connectionString.CertificateThumbprint != null)
            {
                model.CertificateThumbprint = connectionString.CertificateThumbprint;
            }
            if (connectionString.Macaroon != null)
            {
                model.Macaroon = Encoders.Hex.EncodeData(connectionString.Macaroon);
            }
            model.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
            model.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
            model.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;

            if (nonce != null)
            {
                var configKey = GetConfigKey("lnd", service.ServiceName, service.CryptoCode, nonce.Value);
                var lnConfig = _LnConfigProvider.GetConfig(configKey);
                if (lnConfig != null)
                {
                    model.QRCodeLink = Request.GetAbsoluteUri(Url.Action(nameof(GetLNDConfig), new { configKey = configKey }));
                    model.QRCode = $"config={model.QRCodeLink}";
                }
            }

            return View(view, model);
        }

        private static ulong GetConfigKey(string type, string serviceName, string cryptoCode, ulong nonce)
        {
            return ((ulong)(uint)HashCode.Combine(type, serviceName, cryptoCode, nonce) | (nonce & 0xffffffff00000000UL));
        }

        [Route("lnd-config/{configKey}/lnd.config")]
        [AllowAnonymous]
        [EnableCors(CorsPolicies.All)]
        [IgnoreAntiforgeryToken]
        public IActionResult GetLNDConfig(ulong configKey)
        {
            var conf = _LnConfigProvider.GetConfig(configKey);
            if (conf == null)
                return NotFound();
            return Json(conf);
        }

        [Route("server/services/{serviceName}/{cryptoCode}")]
        [HttpPost]
        public async Task<IActionResult> ServicePost(string serviceName, string cryptoCode)
        {
            if (!_dashBoard.IsFullySynched(cryptoCode, out _))
            {
                TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["{0} is not fully synched", cryptoCode].Value;
                return RedirectToAction(nameof(Services));
            }
            var service = GetService(serviceName, cryptoCode);
            if (service == null)
                return NotFound();

            ExternalConnectionString? connectionString = null;
            try
            {
                connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type, _Options.NetworkType);
            }
            catch (Exception ex)
            {
                TempData[WellKnownTempData.ErrorMessage] = ex.Message;
                return RedirectToAction(nameof(Services));
            }

            LightningConfigurations confs = new LightningConfigurations();
            if (service.Type == ExternalServiceTypes.LNDGRPC)
            {
                LightningConfiguration grpcConf = new LightningConfiguration();
                grpcConf.Type = "grpc";
                grpcConf.Host = connectionString.Server.DnsSafeHost;
                grpcConf.Port = connectionString.Server.Port;
                grpcConf.SSL = connectionString.Server.Scheme == "https";
                confs.Configurations.Add(grpcConf);
            }
            else if (service.Type == ExternalServiceTypes.LNDRest || service.Type == ExternalServiceTypes.CLightningRest)
            {
                var restconf = new LNDRestConfiguration();
                restconf.Type = service.Type == ExternalServiceTypes.LNDRest ? "lnd-rest" : "clightning-rest";
                restconf.Uri = connectionString.Server.AbsoluteUri;
                confs.Configurations.Add(restconf);
            }
            else
                throw new NotSupportedException(service.Type.ToString());
            var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1];
            commonConf.ChainType = _Options.NetworkType.ToString();
            commonConf.CryptoCode = cryptoCode;
            commonConf.Macaroon = connectionString.Macaroon == null ? null : Encoders.Hex.EncodeData(connectionString.Macaroon);
            commonConf.CertificateThumbprint = connectionString.CertificateThumbprint == null ? null : connectionString.CertificateThumbprint;
            commonConf.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
            commonConf.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
            commonConf.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;

            var nonce = RandomUtils.GetUInt64();
            var configKey = GetConfigKey("lnd", serviceName, cryptoCode, nonce);
            _LnConfigProvider.KeepConfig(configKey, confs);
            return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
        }

        [Route("server/services/dynamic-dns")]
        public async Task<IActionResult> DynamicDnsServices()
        {
            var settings = (await _SettingsRepository.GetSettingAsync<DynamicDnsSettings>()) ?? new DynamicDnsSettings();
            return View(settings.Services.Select(s => new DynamicDnsViewModel()
            {
                Settings = s
            }).ToArray());
        }
        [Route("server/services/dynamic-dns/{hostname}")]
        public async Task<IActionResult> DynamicDnsServices(string hostname)
        {
            var settings = (await _SettingsRepository.GetSettingAsync<DynamicDnsSettings>()) ?? new DynamicDnsSettings();
            var service = settings.Services.FirstOrDefault(s => s.Hostname.Equals(hostname, StringComparison.OrdinalIgnoreCase));
            if (service == null)
                return NotFound();
            var vm = new DynamicDnsViewModel();
            vm.Modify = true;
            vm.Settings = service;
            return View(nameof(DynamicDnsService), vm);
        }
        [Route("server/services/dynamic-dns")]
        [HttpPost]
        public async Task<IActionResult> DynamicDnsService(DynamicDnsViewModel viewModel, string? command = null)
        {
            if (!ModelState.IsValid)
            {
                return View(viewModel);
            }
            if (command == "Save")
            {
                var settings = (await _SettingsRepository.GetSettingAsync<DynamicDnsSettings>()) ?? new DynamicDnsSettings();
                var i = settings.Services.FindIndex(d => d.Hostname.Equals(viewModel.Settings.Hostname, StringComparison.OrdinalIgnoreCase));
                if (i != -1)
                {
                    ModelState.AddModelError(nameof(viewModel.Settings.Hostname), "This hostname already exists");
                    return View(viewModel);
                }
                if (viewModel.Settings.Hostname != null)
                    viewModel.Settings.Hostname = viewModel.Settings.Hostname.Trim().ToLowerInvariant();
                string errorMessage = await viewModel.Settings.SendUpdateRequest(HttpClientFactory.CreateClient());
                if (errorMessage == null)
                {
                    TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The Dynamic DNS has been successfully queried, your configuration is saved"].Value;
                    viewModel.Settings.LastUpdated = DateTimeOffset.UtcNow;
                    settings.Services.Add(viewModel.Settings);
                    await _SettingsRepository.UpdateSetting(settings);
                    return RedirectToAction(nameof(DynamicDnsServices));
                }
                else
                {
                    ModelState.AddModelError(string.Empty, errorMessage);
                    return View(viewModel);
                }
            }
            else
            {
                return View(new DynamicDnsViewModel() { Settings = new DynamicDnsService() });
            }
        }
        [Route("server/services/dynamic-dns/{hostname}")]
        [HttpPost]
        public async Task<IActionResult> DynamicDnsService(DynamicDnsViewModel viewModel, string hostname, string? command = null)
        {
            if (!ModelState.IsValid)
            {
                return View(viewModel);
            }
            var settings = (await _SettingsRepository.GetSettingAsync<DynamicDnsSettings>()) ?? new DynamicDnsSettings();

            var i = settings.Services.FindIndex(d => d.Hostname.Equals(hostname, StringComparison.OrdinalIgnoreCase));
            if (i == -1)
                return NotFound();
            if (viewModel.Settings.Password == null)
                viewModel.Settings.Password = settings.Services[i].Password;
            if (viewModel.Settings.Hostname != null)
                viewModel.Settings.Hostname = viewModel.Settings.Hostname.Trim().ToLowerInvariant();
            if (!viewModel.Settings.Enabled)
            {
                TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The Dynamic DNS service has been disabled"].Value;
                viewModel.Settings.LastUpdated = null;
            }
            else
            {
                string errorMessage = await viewModel.Settings.SendUpdateRequest(HttpClientFactory.CreateClient());
                if (errorMessage == null)
                {
                    TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The Dynamic DNS has been successfully queried, your configuration is saved"].Value;
                    viewModel.Settings.LastUpdated = DateTimeOffset.UtcNow;
                }
                else
                {
                    ModelState.AddModelError(string.Empty, errorMessage);
                    return View(viewModel);
                }
            }
            settings.Services[i] = viewModel.Settings;
            await _SettingsRepository.UpdateSetting(settings);
            this.RouteData.Values.Remove(nameof(hostname));
            return RedirectToAction(nameof(DynamicDnsServices));
        }

        [HttpGet("server/services/dynamic-dns/{hostname}/delete")]
        public async Task<IActionResult> DeleteDynamicDnsService(string hostname)
        {
            var settings = await _SettingsRepository.GetSettingAsync<DynamicDnsSettings>() ?? new DynamicDnsSettings();
            var i = settings.Services.FindIndex(d => d.Hostname.Equals(hostname, StringComparison.OrdinalIgnoreCase));
            if (i == -1)
                return NotFound();
            return View("Confirm",
                new ConfirmModel("Delete dynamic DNS service",
                    $"Deleting the dynamic DNS service for <strong>{Html.Encode(hostname)}</strong> means your BTCPay Server will stop updating the associated DNS record periodically.", "Delete"));
        }

        [HttpPost("server/services/dynamic-dns/{hostname}/delete")]
        public async Task<IActionResult> DeleteDynamicDnsServicePost(string hostname)
        {
            var settings = (await _SettingsRepository.GetSettingAsync<DynamicDnsSettings>()) ?? new DynamicDnsSettings();
            var i = settings.Services.FindIndex(d => d.Hostname.Equals(hostname, StringComparison.OrdinalIgnoreCase));
            if (i == -1)
                return NotFound();
            settings.Services.RemoveAt(i);
            await _SettingsRepository.UpdateSetting(settings);
            TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Dynamic DNS service successfully removed"].Value;
            RouteData.Values.Remove(nameof(hostname));
            return RedirectToAction(nameof(DynamicDnsServices));
        }

        [HttpGet("server/services/ssh")]
        public async Task<IActionResult> SSHService()
        {
            if (!CanShowSSHService())
                return NotFound();

            var settings = _Options.SSHSettings;
            var server = Extensions.IsLocalNetwork(settings.Server) ? this.Request.Host.Host : settings.Server;
            SSHServiceViewModel vm = new SSHServiceViewModel();
            string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
            vm.CommandLine = $"ssh {settings.Username}@{server}{port}";
            vm.Password = settings.Password;
            vm.KeyFilePassword = settings.KeyFilePassword;
            vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);

            //  Let's try to just read the authorized key file
            if (CanAccessAuthorizedKeyFile())
            {
                try
                {
                    vm.SSHKeyFileContent = await System.IO.File.ReadAllTextAsync(settings.AuthorizedKeysFile);
                }
                catch { }
            }

            // If that fail, just fallback to ssh
            if (vm.SSHKeyFileContent == null && _sshState.CanUseSSH)
            {
                try
                {
                    using var sshClient = await _Options.SSHSettings.ConnectAsync();
                    var result = await sshClient.RunBash("cat ~/.ssh/authorized_keys", TimeSpan.FromSeconds(10));
                    vm.SSHKeyFileContent = result.Output;
                }
                catch { }
            }
            return View(vm);
        }

        bool CanShowSSHService()
        {
            return !_policiesSettings.DisableSSHService &&
                   _Options.SSHSettings != null && (_sshState.CanUseSSH || CanAccessAuthorizedKeyFile());
        }

        private bool CanAccessAuthorizedKeyFile()
        {
            return _Options.SSHSettings?.AuthorizedKeysFile != null && System.IO.File.Exists(_Options.SSHSettings.AuthorizedKeysFile);
        }

        [HttpPost("server/services/ssh")]
        public async Task<IActionResult> SSHService(SSHServiceViewModel viewModel, string? command = null)
        {
            if (!CanShowSSHService())
                return NotFound();

            if (command is "Save")
            {
                string newContent = viewModel?.SSHKeyFileContent ?? string.Empty;
                newContent = newContent.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase);

                bool updated = false;
                Exception? exception = null;
                // Let's try to just write the file
                if (CanAccessAuthorizedKeyFile())
                {
                    try
                    {
                        await System.IO.File.WriteAllTextAsync(_Options.SSHSettings.AuthorizedKeysFile, newContent);
                        TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["authorized_keys has been updated"].Value;
                        updated = true;
                    }
                    catch (Exception ex)
                    {
                        exception = ex;
                    }
                }

                // If that fail, fallback to ssh
                if (!updated && _sshState.CanUseSSH)
                {
                    try
                    {
                        using (var sshClient = await _Options.SSHSettings.ConnectAsync())
                        {
                            await sshClient.RunBash($"mkdir -p ~/.ssh && echo '{newContent.EscapeSingleQuotes()}' > ~/.ssh/authorized_keys", TimeSpan.FromSeconds(10));
                        }
                        updated = true;
                        exception = null;
                    }
                    catch (Exception ex)
                    {
                        exception = ex;
                    }
                }

                if (exception is null)
                {
                    TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["authorized_keys has been updated"].Value;
                }
                else
                {
                    TempData[WellKnownTempData.ErrorMessage] = exception.Message;
                }
                return RedirectToAction(nameof(SSHService));
            }

            if (command is "disable")
            {
                return RedirectToAction(nameof(SSHServiceDisable));
            }

            return NotFound();
        }

        [HttpGet("server/services/ssh/disable")]
        public IActionResult SSHServiceDisable()
        {
            return View("Confirm", new ConfirmModel("Disable modification of SSH settings", "This action is permanent and will remove the ability to change the SSH settings via the BTCPay Server user interface.", "Disable"));
        }

        [HttpPost("server/services/ssh/disable")]
        public async Task<IActionResult> SSHServiceDisablePost()
        {
            var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
            policies.DisableSSHService = true;
            await _SettingsRepository.UpdateSetting(policies);
            TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Changes to the SSH settings are now permanently disabled in the BTCPay Server user interface"].Value;
            return RedirectToAction(nameof(Services));
        }

        [HttpGet("server/branding")]
        public async Task<IActionResult> Branding()
        {
            var server = await _SettingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
            var theme = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
            
            var vm = new BrandingViewModel
            {
                ServerName = server.ServerName,
                ContactUrl = server.ContactUrl,
                CustomTheme = theme.CustomTheme,
                CustomThemeExtension = theme.CustomThemeExtension,
                CustomThemeCssUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), theme.CustomThemeCssUrl),
                LogoUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), theme.LogoUrl)
            };
            return View(vm);
        }

        [HttpPost("server/branding")]
        public async Task<IActionResult> Branding(
            BrandingViewModel vm,
            [FromForm] bool RemoveLogoFile,
            [FromForm] bool RemoveCustomThemeFile)
        {
            var settingsChanged = false;
            var server = await _SettingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
            var theme = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();

            var userId = GetUserId();
            if (userId is null)
                return NotFound();

            vm.LogoUrl = await _uriResolver.Resolve(this.Request.GetAbsoluteRootUri(), theme.LogoUrl);
            vm.CustomThemeCssUrl = await _uriResolver.Resolve(this.Request.GetAbsoluteRootUri(), theme.CustomThemeCssUrl);
            
            if (server.ServerName != vm.ServerName)
            {
                server.ServerName = vm.ServerName;
                settingsChanged = true;
            }
            
            if (server.ContactUrl != vm.ContactUrl)
            {
                server.ContactUrl = !string.IsNullOrWhiteSpace(vm.ContactUrl)
                    ? vm.ContactUrl.IsValidEmail() ? $"mailto:{vm.ContactUrl}" : vm.ContactUrl
                    : null;
                settingsChanged = true;
            }
            
            if (settingsChanged)
            {
                await _SettingsRepository.UpdateSetting(server);
            }

            if (vm.CustomThemeFile != null)
            {
                if (vm.CustomThemeFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
                {
                    // add new file
                    try
                    {
                        var storedFile = await _fileService.AddFile(vm.CustomThemeFile, userId);
                        theme.CustomThemeCssUrl = new UnresolvedUri.FileIdUri(storedFile.Id);
                        vm.CustomThemeCssUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), theme.CustomThemeCssUrl);
                        settingsChanged = true;
                    }
                    catch (Exception e)
                    {
                        ModelState.AddModelError(nameof(vm.CustomThemeFile), StringLocalizer["Could not save CSS file: {0}", e.Message]);
                    }
                }
                else
                {
                    ModelState.AddModelError(nameof(vm.CustomThemeFile), StringLocalizer["The uploaded file needs to be a CSS file"]);
                }
            }
            else if (RemoveCustomThemeFile && theme.CustomThemeCssUrl is not null)
            {
                vm.CustomThemeCssUrl = null;
                theme.CustomThemeCssUrl = null;
                theme.CustomTheme = false;
                theme.CustomThemeExtension = ThemeExtension.Custom;
                settingsChanged = true;
            }

            if (vm.LogoFile != null)
            {
                if (vm.LogoFile.Length > 1_000_000)
                {
                    ModelState.AddModelError(nameof(vm.LogoFile), StringLocalizer["The uploaded file should be less than {0}", "1MB"]);
                }
                else if (!vm.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
                {
                    ModelState.AddModelError(nameof(vm.LogoFile), StringLocalizer["The uploaded file needs to be an image"]);
                }
                else
                {
                    var formFile = await vm.LogoFile.Bufferize();
                    if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
                    {
                        ModelState.AddModelError(nameof(vm.LogoFile), StringLocalizer["The uploaded file needs to be an image"]);
                    }
                    else
                    {
                        vm.LogoFile = formFile;
                        // add new file
                        try
                        {
                            var storedFile = await _fileService.AddFile(vm.LogoFile, userId);
                            theme.LogoUrl = new UnresolvedUri.FileIdUri(storedFile.Id);
                            vm.LogoUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), theme.LogoUrl);
                            settingsChanged = true;
                        }
                        catch (Exception e)
                        {
                            ModelState.AddModelError(nameof(vm.LogoFile), StringLocalizer["Could not save logo: {0}", e.Message]);
                        }
                    }
                }
            }
            else if (RemoveLogoFile && theme.LogoUrl is not null)
            {
                vm.LogoUrl = null;
                theme.LogoUrl = null;
                settingsChanged = true;
            }

            if (vm.CustomTheme && theme.CustomThemeExtension != vm.CustomThemeExtension)
            {
                // Require a custom theme to be defined in that case
                if (string.IsNullOrEmpty(vm.CustomThemeCssUrl) && theme.CustomThemeCssUrl is null)
                {
                    ModelState.AddModelError(nameof(vm.CustomThemeCssUrl), "Please provide a custom theme");
                }
                else
                {
                    theme.CustomThemeExtension = vm.CustomThemeExtension;
                    settingsChanged = true;
                }
            }

            if (theme.CustomTheme != vm.CustomTheme && !RemoveCustomThemeFile)
            {
                theme.CustomTheme = vm.CustomTheme;
                settingsChanged = true;
            }

            if (settingsChanged)
            {
                await _SettingsRepository.UpdateSetting(theme);
                TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Settings updated successfully"].Value;
                return RedirectToAction(nameof(Branding));
            }

            return View(vm);
        }

        [HttpGet("server/emails")]
        public async Task<IActionResult> Emails()
        {
            var email = await _emailSenderFactory.GetSettings() ?? new EmailSettings();
            var vm = new ServerEmailsViewModel(email)
            {
                EnableStoresToUseServerEmailSettings = !_policiesSettings.DisableStoresToUseServerEmailSettings
            };
            return View(vm);
        }

        [HttpPost("server/emails")]
        public async Task<IActionResult> Emails(ServerEmailsViewModel model, string command)
        {
            if (command == "Test")
            {
                try
                {
                    if (model.PasswordSet)
                    {
                        var settings = await _emailSenderFactory.GetSettings() ?? new EmailSettings();
                        model.Settings.Password = settings.Password;
                    }
                    model.Settings.Validate("Settings.", ModelState);
                    if (string.IsNullOrEmpty(model.TestEmail))
                        ModelState.AddModelError(nameof(model.TestEmail), new RequiredAttribute().FormatErrorMessage(nameof(model.TestEmail)));
                    if (!ModelState.IsValid)
                        return View(model);
                    var serverSettings = await _SettingsRepository.GetSettingAsync<ServerSettings>();
                    var serverName = string.IsNullOrEmpty(serverSettings?.ServerName) ? "BTCPay Server" : serverSettings.ServerName;
                    using (var client = await model.Settings.CreateSmtpClient())
                    using (var message = model.Settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), $"{serverName}: Email test", "You received it, the BTCPay Server SMTP settings work.", false))
                    {
                        await client.SendAsync(message);
                        await client.DisconnectAsync(true);
                    }
                    TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email sent to {0}. Please verify you received it.", model.TestEmail].Value;
                }
                catch (Exception ex)
                {
                    TempData[WellKnownTempData.ErrorMessage] = ex.Message;
                }
                return View(model);
            }

            if (_policiesSettings.DisableStoresToUseServerEmailSettings == model.EnableStoresToUseServerEmailSettings)
            {
                _policiesSettings.DisableStoresToUseServerEmailSettings = !model.EnableStoresToUseServerEmailSettings;
                await _SettingsRepository.UpdateSetting(_policiesSettings);
            }

            if (command == "ResetPassword")
            {
                var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
                settings.Password = null;
                await _SettingsRepository.UpdateSetting(settings);
                TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email server password reset"].Value;
                return RedirectToAction(nameof(Emails));
            }
            
            // save
            if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
            {
                ModelState.AddModelError("Settings.From", StringLocalizer["Invalid email"]);
                return View(model);
            }
            var oldSettings = await _emailSenderFactory.GetSettings() ?? new EmailSettings();
            if (new ServerEmailsViewModel(oldSettings).PasswordSet)
            {
                model.Settings.Password = oldSettings.Password;
            }
            
            await _SettingsRepository.UpdateSetting(model.Settings);
            TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email settings saved"].Value;
            return RedirectToAction(nameof(Emails));
        }

        [Route("server/logs/{file?}")]
        public async Task<IActionResult> LogsView(string? file = null, int offset = 0, bool download = false)
        {
            if (offset < 0)
            {
                offset = 0;
            }

            var vm = new LogsViewModel();

            if (string.IsNullOrEmpty(_Options.LogFile))
            {
                TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["File Logging Option not specified. You need to set debuglog and optionally debugloglevel in the configuration or through runtime arguments"].Value;
            }
            else
            {
                var di = Directory.GetParent(_Options.LogFile);
                if (di is null)
                {
                    TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Could not load log files"].Value;
                    return View("Logs", vm);
                }

                var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(_Options.LogFile);
                var fileExtension = Path.GetExtension(_Options.LogFile) ?? string.Empty;
                // We are checking if "di" is null above yet accessing GetFiles on it, this could lead to an exception?
                var logFiles = di.GetFiles($"{fileNameWithoutExtension}*{fileExtension}");
                vm.LogFileCount = logFiles.Length;
                vm.LogFiles = logFiles
                    .OrderBy(info => info.LastWriteTime)
                    .Skip(offset)
                    .Take(5)
                    .ToList();
                vm.LogFileOffset = offset;

                if (string.IsNullOrEmpty(file) || !file.EndsWith(fileExtension, StringComparison.Ordinal))
                    return View("Logs", vm);
                vm.Log = "";
                var fi = vm.LogFiles.FirstOrDefault(o => o.Name == file);
                if (fi == null)
                    return NotFound();
                try
                {
                    var fileStream = new FileStream(
                        fi.FullName,
                        FileMode.Open,
                        FileAccess.Read,
                        FileShare.ReadWrite);
                    if (download)
                    {
                        return new FileStreamResult(fileStream, "text/plain")
                        {
                            FileDownloadName = file
                        };
                    }
                    await using (fileStream)
                    {
                        using var reader = new StreamReader(fileStream);
                        vm.Log = await reader.ReadToEndAsync();
                    }
                }
                catch
                {
                    return NotFound();
                }
            }

            return View("Logs", vm);
        }
    }
}