using System.Collections.ObjectModel;
using System.Globalization;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using ServerMonitorManager.Core;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
using Windows.Storage;
using Windows.Storage.Pickers;

namespace ServerMonitorManager_Desktop;

public sealed partial class MainPage : Page
{
    private readonly ServerStorage _storage = new();
    private readonly SshMonitorService _ssh = new();
    private readonly MetricsHistoryStorage _historyStorage = new();
    private readonly ControlClientService _control = new();
    private readonly UpdateService _update = new(new DefaultHttpTransport(), new ProcessSignatureVerifier(new DefaultFileStorage(), new DefaultHttpTransport()), new DefaultFileStorage());
    private UpdateInfo? _pendingUpdate;
    private readonly List<MetricSampleData> _history = [];
    private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromSeconds(30) };
    private readonly SemaphoreSlim _refreshLock = new(1, 1);
    private readonly CancellationTokenSource _controlCancellation = new();
    private ServersPage? _serversPage;
    private LinksPage? _linksPage;
    private SessionsPage? _sessionsPage;
    private SettingsPage? _settingsPage;
    private bool _loaded;
    private bool _controlListening;
    private bool _meshFirewallUnavailable;

    public MainPage()
    {
        InitializeComponent();
        Loaded += MainPage_Loaded;
        Unloaded += (_, _) => _controlCancellation.Cancel();
        _refreshTimer.Tick += async (_, _) => await RefreshAllAsync();
        MainNavigation.SelectedItem = MainNavigation.MenuItems[0];
    }

    public ObservableCollection<ServerViewModel> Servers { get; } = [];
    public ObservableCollection<MeshNodeViewModel> MeshNodes { get; } = [];
    public ObservableCollection<MeshLinkViewModel> MeshLinks { get; } = [];

    internal bool IsControlConfigured => _control.IsConfigured;

    internal void AddServerFromPage() => AddServerButton_Click(this, new RoutedEventArgs());

    internal void EditServerFromPage(ServerViewModel? server)
    {
        ServerList.SelectedItem = server;
        EditServerButton_Click(this, new RoutedEventArgs());
    }

    internal void DeleteServerFromPage(ServerViewModel? server)
    {
        ServerList.SelectedItem = server;
        DeleteServerButton_Click(this, new RoutedEventArgs());
    }

    internal async Task ConfirmHostKeyFromPageAsync(ServerViewModel? server)
    {
        if (server is null)
        {
            ShowInfo("Сервер не выбран", "Выберите профиль для подтверждения host key.", InfoBarSeverity.Warning);
            return;
        }

        var fingerprint = await ConfirmHostKeyAsync(server.Profile);
        if (fingerprint is null)
        {
            return;
        }

        var index = Servers.IndexOf(server);
        if (index < 0)
        {
            return;
        }
        var confirmed = new ServerViewModel(server.Profile with { HostKeyFingerprint = fingerprint });
        Servers[index] = confirmed;
        ServerList.SelectedItem = confirmed;
        await SaveProfilesAsync();
        await RefreshServerAsync(confirmed);
        ShowInfo("SSH host key подтверждён", confirmed.Name, InfoBarSeverity.Success);
    }

    internal void OpenTerminalFromPage(ServerViewModel? server)
    {
        ServerList.SelectedItem = server;
        TerminalButton_Click(this, new RoutedEventArgs());
    }

    internal async Task RefreshServersFromPageAsync()
        => await RefreshAllAsync();

    internal async Task RefreshLinksFromPageAsync()
        => await RefreshMeshAsync();

    internal async Task ChangeLinkFromPageAsync(
        MeshNodeViewModel? source,
        MeshNodeViewModel? target,
        MeshLinkViewModel? selectedLink,
        string protocol,
        int port,
        int ttlMinutes,
        bool enable)
    {
        SourceNodeBox.SelectedItem = source;
        TargetNodeBox.SelectedItem = target;
        LinkProtocolBox.SelectedIndex = string.Equals(protocol, "udp", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
        LinkPortBox.Value = port;
        LinkTtlBox.Value = ttlMinutes;
        MeshLinksList.SelectedItem = selectedLink;
        await ChangeLinkAsync(enable);
    }

    internal void ReenrollNodeFromPage(MeshNodeViewModel? source)
    {
        SourceNodeBox.SelectedItem = source;
        ReenrollNodeButton_Click(this, new RoutedEventArgs());
    }

    internal void ShowSshKeyFromPage() => SshKeyButton_Click(this, new RoutedEventArgs());

    internal void ConnectControlHubFromPage() => ControlHubButton_Click(this, new RoutedEventArgs());

    internal void ExportDiagnosticsFromPage() => ExportDiagnosticsButton_Click(this, new RoutedEventArgs());

    private async void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        if (_loaded)
        {
            return;
        }

        _loaded = true;
        
        _ = Task.Run(async () =>
        {
            try
            {
                var update = await _update.CheckForUpdatesAsync(usePreRelease: true);
                if (update is not null)
                {
                    DispatcherQueue.TryEnqueue(() =>
                    {
                        _pendingUpdate = update;
                        UpdateInfoBar.Message = $"Доступна новая версия: {update.Version}";
                        UpdateInfoBar.IsOpen = true;
                    });
                }
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine($"Update check failed: {ex.Message}");
            }
        });
        
        _refreshTimer.Start();
        _history.AddRange(await _historyStorage.LoadAsync());
        foreach (var profile in await _storage.LoadAsync())
        {
            Servers.Add(new ServerViewModel(profile));
        }
        UpdateEmptyState();
        if (Servers.Count > 0)
        {
            ServerList.SelectedIndex = 0;
            RenderHistory();
        }

        var pendingNames = Servers
            .Where(server => server.HostKeyPendingConfirmation)
            .Select(server => server.Name)
            .ToArray();
        if (pendingNames.Length > 0)
        {
            ShowInfo(
                "Требуется подтверждение SSH host key",
                $"Нажмите «Подтвердить host key» в карточке: {string.Join(", ", pendingNames)}.",
                InfoBarSeverity.Warning);
        }

        if (Servers.Count > 0 || _control.IsConfigured)
        {
            await RefreshAllAsync();
        }
        StartControlEvents();
    }

    private async void SshKeyButton_Click(object sender, RoutedEventArgs e)
    {
        try
        {
            var publicKey = await _ssh.EnsureKeyPairAsync();
            var keyBox = new TextBox
            {
                Text = publicKey,
                IsReadOnly = true,
                AcceptsReturn = true,
                TextWrapping = TextWrapping.Wrap,
                MinWidth = 520
            };
            AutomationProperties.SetName(keyBox, "Публичный SSH-ключ мониторинга");
            var dialog = new ContentDialog
            {
                XamlRoot = XamlRoot,
                Title = "SSH-ключ мониторинга",
                Content = new StackPanel
                {
                    Spacing = 12,
                    Children =
                    {
                        new TextBlock
                        {
                            Text = "Скопируйте этот публичный ключ и вставьте его в установочный скрипт на каждом сервере. Приватный ключ остаётся только на этом ПК.",
                            TextWrapping = TextWrapping.Wrap
                        },
                        keyBox
                    }
                },
                PrimaryButtonText = "Копировать",
                CloseButtonText = "Закрыть",
                DefaultButton = ContentDialogButton.Primary
            };

            if (await dialog.ShowAsync() == ContentDialogResult.Primary)
            {
                var package = new DataPackage();
                package.SetText(publicKey);
                Clipboard.SetContent(package);
                ShowInfo("SSH-ключ скопирован", "Вставьте его в ochenstarik-server-monitor-manager.sh на каждом сервере.", InfoBarSeverity.Success);
            }
        }
        catch (Exception exception)
        {
            ShowInfo("Не удалось создать SSH-ключ", exception.Message, InfoBarSeverity.Error);
        }
    }

    private async void ExportDiagnosticsButton_Click(object sender, RoutedEventArgs e)
    {
        try
        {
            var window = App.MainWindow
                ?? throw new InvalidOperationException("Главное окно приложения недоступно.");
            var picker = new FileSavePicker
            {
                SuggestedStartLocation = PickerLocationId.DocumentsLibrary,
                SuggestedFileName = $"server-monitor-manager-diagnostics-{DateTimeOffset.Now:yyyyMMdd-HHmmss}"
            };
            picker.FileTypeChoices.Add("JSON diagnostics", [".json"]);
            WinRT.Interop.InitializeWithWindow.Initialize(
                picker,
                WinRT.Interop.WindowNative.GetWindowHandle(window));
            var file = await picker.PickSaveFileAsync();
            if (file is null)
            {
                return;
            }

            var json = DiagnosticsExportService.CreateJson(
                Servers.Select(server => new DiagnosticServerInput(
                    $"{server.Profile.Host}:{server.Profile.Port}",
                    server.IsHub,
                    server.IsOnline,
                    server.HasWarning,
                    server.CpuPercent)),
                MeshNodes.Select(node => new DiagnosticNodeInput(
                    $"{node.Name}:{node.Address}",
                    node.State,
                    node.HandshakeAgeSeconds)),
                MeshLinks.Select(link => new DiagnosticLinkInput(
                    link.Source,
                    link.Target,
                    link.Protocol,
                    link.Port,
                    link.State,
                    link.Version,
                    link.ExpiresUnix)),
                _history.Select(sample => new DiagnosticMetricInput(
                    sample.ServerId,
                    sample.Timestamp,
                    sample.CpuPercent,
                    sample.MemoryPercent,
                    sample.DiskPercent)),
                _control.IsConfigured);
            await FileIO.WriteTextAsync(file, json);
            ShowInfo(
                "Диагностика экспортирована",
                "Файл не содержит адресов серверов, пользователей, ключей, сертификатов или токенов.",
                InfoBarSeverity.Success);
        }
        catch (Exception exception)
        {
            ShowInfo(
                "Не удалось экспортировать диагностику",
                CompactError(exception),
                InfoBarSeverity.Error);
        }
    }

    private async void TerminalButton_Click(object sender, RoutedEventArgs e)
    {
        if (ServerList.SelectedItem is not ServerViewModel selected)
        {
            ShowInfo("Сервер не выбран", "Выберите сервер, к которому нужно открыть SSH-терминал.", InfoBarSeverity.Warning);
            return;
        }

        var userBox = new TextBox
        {
            Header = "Unix-пользователь",
            PlaceholderText = "starik",
            MinWidth = 360
        };
        AutomationProperties.SetName(userBox, "Пользователь интерактивного SSH-терминала");
        var dialog = new ContentDialog
        {
            XamlRoot = XamlRoot,
            Title = $"SSH-терминал: {selected.Name}",
            Content = new StackPanel
            {
                Spacing = 12,
                Children =
                {
                    new TextBlock
                    {
                        Text = "Терминал использует системный OpenSSH и ваши обычные SSH-ключи. Ключ мониторинга с ограниченной командой здесь не применяется.",
                        TextWrapping = TextWrapping.Wrap
                    },
                    userBox
                }
            },
            PrimaryButtonText = "Открыть",
            CloseButtonText = "Отмена",
            DefaultButton = ContentDialogButton.Primary
        };

        if (await dialog.ShowAsync() != ContentDialogResult.Primary)
        {
            return;
        }

        try
        {
            _ssh.OpenInteractiveTerminal(selected.Profile, userBox.Text.Trim());
            ShowInfo("SSH-терминал открыт", $"Подключение к {selected.Profile.Host} запущено от имени {userBox.Text.Trim()}.", InfoBarSeverity.Success);
        }
        catch (Exception exception)
        {
            ShowInfo("Не удалось открыть SSH-терминал", exception.Message, InfoBarSeverity.Error);
        }
    }

    private async void ControlHubButton_Click(object sender, RoutedEventArgs e)
    {
        var codeBox = new TextBox
        {
            Header = "Код устройства SMMDEV1",
            PlaceholderText = "SMMDEV1-...",
            AcceptsReturn = true,
            TextWrapping = TextWrapping.Wrap,
            MinWidth = 360
        };
        AutomationProperties.SetName(codeBox, "Одноразовый код Control Hub для Windows-устройства");
        var codeDialog = new ContentDialog
        {
            XamlRoot = XamlRoot,
            Title = "Подключение Control Hub",
            Content = new StackPanel
            {
                Spacing = 12,
                Children =
                {
                    new TextBlock
                    {
                        Text = "На Hub выполните: sudo ochenstarik-server-monitor-manager.sh control-device-code windows-pc. Вставьте полученный одноразовый код.",
                        TextWrapping = TextWrapping.Wrap
                    },
                    codeBox
                }
            },
            PrimaryButtonText = "Проверить",
            CloseButtonText = "Отмена",
            DefaultButton = ContentDialogButton.Primary
        };
        if (await codeDialog.ShowAsync() != ContentDialogResult.Primary)
        {
            return;
        }

        try
        {
            var preview = _control.ParseEnrollmentCode(codeBox.Text);
            var fingerprintBox = new TextBox
            {
                Header = "SHA-256 fingerprint Control CA",
                Text = preview.Fingerprint,
                IsReadOnly = true,
                TextWrapping = TextWrapping.Wrap
            };
            AutomationProperties.SetName(fingerprintBox, "SHA-256 fingerprint Control CA");
            var confirmDialog = new ContentDialog
            {
                XamlRoot = XamlRoot,
                Title = "Подтвердите Control Hub",
                Content = new StackPanel
                {
                    Spacing = 12,
                    Children =
                    {
                        new TextBlock
                        {
                            Text = $"Устройство: {preview.DeviceId}\nHub: {preview.ControlUrl}",
                            TextWrapping = TextWrapping.Wrap
                        },
                        fingerprintBox,
                        new TextBlock
                        {
                            Text = "Сравните fingerprint с показанным на Hub. После подтверждения приложение локально создаст отдельный operator key и защитит его через Windows DPAPI.",
                            TextWrapping = TextWrapping.Wrap
                        }
                    }
                },
                PrimaryButtonText = "Fingerprint совпадает",
                CloseButtonText = "Отмена",
                DefaultButton = ContentDialogButton.Close
            };
            if (await confirmDialog.ShowAsync() != ContentDialogResult.Primary)
            {
                return;
            }

            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
            await _control.EnrollAsync(preview, timeout.Token);
            ShowInfo(
                "Control Hub подключён",
                $"Operator identity {preview.DeviceId} сохранена через DPAPI. Поток событий запускается.",
                InfoBarSeverity.Success);
            StartControlEvents();
        }
        catch (Exception exception)
        {
            ShowInfo("Не удалось подключить Control Hub", CompactError(exception), InfoBarSeverity.Error);
        }
    }

    private void StartControlEvents()
    {
        if (_controlListening || _controlCancellation.IsCancellationRequested)
        {
            return;
        }
        _controlListening = true;
        _ = Task.Run(async () =>
        {
            try
            {
                await _control.ListenAsync(HandleControlEventAsync, _controlCancellation.Token);
            }
            finally
            {
                _controlListening = false;
            }
        });
    }

    private async void InstallUpdateButton_Click(object sender, RoutedEventArgs e)
    {
        if (_pendingUpdate is null) return;
        
        InstallUpdateButton.IsEnabled = false;
        InstallUpdateButton.Content = "Скачивание...";
        UpdateInfoBar.IsClosable = false;
        
        try
        {
            await _update.DownloadAndVerifyUpdateAsync(_pendingUpdate);
            _update.InstallUpdate();
        }
        catch (Exception ex)
        {
            ShowInfo("Ошибка обновления", ex.Message, InfoBarSeverity.Error);
            UpdateInfoBar.IsOpen = false;
        }
    }

    private Task HandleControlEventAsync(ControlEvent controlEvent)
    {
        DispatcherQueue.TryEnqueue(() =>
        {
            if (controlEvent.Type == MeshLinkViewModel.FirewallUnavailableErrorCode)
            {
                SetMeshFirewallUnavailable(true);
                _ = RefreshMeshAsync(showSuccess: false);
                return;
            }
            if (controlEvent.Type == "mesh.firewall-available")
            {
                SetMeshFirewallUnavailable(false);
                _ = RefreshMeshAsync(showSuccess: false);
                return;
            }
            if (controlEvent.Type.StartsWith("link.", StringComparison.Ordinal))
            {
                ShowInfo(
                    "Событие Control Hub",
                    $"{controlEvent.Subject}: {controlEvent.Type}",
                    controlEvent.Type is "link.failed" or "link.partial"
                        ? InfoBarSeverity.Warning
                        : InfoBarSeverity.Informational);
                _ = RefreshMeshAsync(showSuccess: false);
            }
        });
        return Task.CompletedTask;
    }

    private async Task<string?> ConfirmHostKeyAsync(ServerProfileData profile)
    {
        try
        {
            SshHostKeyCandidate candidate;
            using (var scanTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(15)))
            {
                candidate = await _ssh.ScanHostKeyAsync(profile, scanTimeout.Token);
            }
            var fingerprintBox = new TextBox
            {
                Text = candidate.Fingerprint,
                IsReadOnly = true,
                TextWrapping = TextWrapping.Wrap
            };
            AutomationProperties.SetName(fingerprintBox, "Fingerprint SSH host key");
            var dialog = new ContentDialog
            {
                XamlRoot = XamlRoot,
                Title = $"Подтвердите SSH host key: {profile.Name}",
                Content = new StackPanel
                {
                    Spacing = 12,
                    MinWidth = 480,
                    Children =
                    {
                        new TextBlock
                        {
                            Text = $"Алгоритм: {candidate.KeyType}\nСверьте fingerprint через доверенную консоль сервера или панель провайдера. Не подтверждайте его только по данным текущего подключения.",
                            TextWrapping = TextWrapping.Wrap
                        },
                        fingerprintBox
                    }
                },
                PrimaryButtonText = "Fingerprint совпадает",
                CloseButtonText = "Отмена",
                DefaultButton = ContentDialogButton.Close
            };
            if (await dialog.ShowAsync() != ContentDialogResult.Primary)
            {
                return null;
            }

            using var persistTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            await _ssh.TrustHostKeyAsync(candidate, persistTimeout.Token);
            return candidate.Fingerprint;
        }
        catch (Exception exception)
        {
            ShowInfo(
                "Не удалось проверить SSH host key",
                exception.Message,
                InfoBarSeverity.Error);
            return null;
        }
    }

    private async void AddServerButton_Click(object sender, RoutedEventArgs e)
    {
        try
        {
            await _ssh.EnsureKeyPairAsync();
        }
        catch (Exception exception)
        {
            ShowInfo("Не удалось подготовить SSH-ключ", exception.Message, InfoBarSeverity.Error);
            return;
        }

        var nameBox = new TextBox { Header = "Название", PlaceholderText = "Home Lab" };
        var hostBox = new TextBox { Header = "IP или домен", PlaceholderText = "192.0.2.10" };
        var portBox = new NumberBox
        {
            Header = "SSH-порт",
            Value = 22,
            Minimum = 1,
            Maximum = 65535,
            SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Compact
        };
        var userBox = new TextBox { Header = "Пользователь", Text = "ochenstarik-monitor" };
        var hubBox = new CheckBox
        {
            Content = "Это главный Mesh Hub"
        };
        AutomationProperties.SetName(hubBox, "Использовать сервер как главный Mesh Hub");
        var validationText = new TextBlock { TextWrapping = TextWrapping.Wrap };
        var content = new StackPanel
        {
            Spacing = 12,
            MinWidth = 420,
            Children = { nameBox, hostBox, portBox, userBox, hubBox, validationText }
        };
        var dialog = new ContentDialog
        {
            XamlRoot = XamlRoot,
            Title = "Добавить сервер",
            Content = content,
            PrimaryButtonText = "Добавить и проверить",
            CloseButtonText = "Отмена",
            DefaultButton = ContentDialogButton.Primary
        };
        dialog.PrimaryButtonClick += (_, args) =>
        {
            if (string.IsNullOrWhiteSpace(nameBox.Text)
                || string.IsNullOrWhiteSpace(hostBox.Text)
                || string.IsNullOrWhiteSpace(userBox.Text)
                || double.IsNaN(portBox.Value))
            {
                validationText.Text = "Заполните название, адрес, порт и пользователя.";
                args.Cancel = true;
            }
        };

        if (await dialog.ShowAsync() != ContentDialogResult.Primary)
        {
            return;
        }
        if (hubBox.IsChecked == true && Servers.Any(server => server.IsHub))
        {
            ShowInfo("Hub уже выбран", "Измените существующий Hub или снимите эту отметку.", InfoBarSeverity.Warning);
            return;
        }

        var profile = new ServerProfileData(
            Guid.NewGuid().ToString("N"),
            nameBox.Text.Trim(),
            hostBox.Text.Trim(),
            checked((int)portBox.Value),
            userBox.Text.Trim(),
            hubBox.IsChecked == true);
        var hostKeyFingerprint = await ConfirmHostKeyAsync(profile);
        if (hostKeyFingerprint is null)
        {
            return;
        }
        profile = profile with { HostKeyFingerprint = hostKeyFingerprint };
        var server = new ServerViewModel(profile);
        Servers.Add(server);
        await SaveProfilesAsync();
        UpdateEmptyState();
        await RefreshServerAsync(server);
    }

    private async void EditServerButton_Click(object sender, RoutedEventArgs e)
    {
        if (ServerList.SelectedItem is not ServerViewModel selected)
        {
            ShowInfo("Сервер не выбран", "Выберите сервер в списке для изменения.", InfoBarSeverity.Warning);
            return;
        }

        var nameBox = new TextBox { Header = "Название", Text = selected.Profile.Name };
        var hostBox = new TextBox { Header = "IP или домен", Text = selected.Profile.Host };
        var portBox = new NumberBox
        {
            Header = "SSH-порт",
            Value = selected.Profile.Port,
            Minimum = 1,
            Maximum = 65535,
            SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Compact
        };
        var userBox = new TextBox { Header = "Пользователь", Text = selected.Profile.User };
        var hubBox = new CheckBox { Content = "Это главный Mesh Hub", IsChecked = selected.IsHub };
        AutomationProperties.SetName(hubBox, "Использовать сервер как главный Mesh Hub");
        var dialog = new ContentDialog
        {
            XamlRoot = XamlRoot,
            Title = "Изменить сервер",
            Content = new StackPanel
            {
                Spacing = 12,
                MinWidth = 420,
                Children = { nameBox, hostBox, portBox, userBox, hubBox }
            },
            PrimaryButtonText = "Сохранить",
            CloseButtonText = "Отмена",
            DefaultButton = ContentDialogButton.Primary
        };
        if (await dialog.ShowAsync() != ContentDialogResult.Primary)
        {
            return;
        }
        if (string.IsNullOrWhiteSpace(nameBox.Text)
            || string.IsNullOrWhiteSpace(hostBox.Text)
            || string.IsNullOrWhiteSpace(userBox.Text)
            || double.IsNaN(portBox.Value))
        {
            ShowInfo("Данные не сохранены", "Название, адрес, порт и пользователь обязательны.", InfoBarSeverity.Warning);
            return;
        }
        if (hubBox.IsChecked == true && Servers.Any(server => server.IsHub && server != selected))
        {
            ShowInfo("Hub уже выбран", "В конфигурации может быть только один главный Mesh Hub.", InfoBarSeverity.Warning);
            return;
        }

        var index = Servers.IndexOf(selected);
        var updatedProfile = new ServerProfileData(
            selected.Profile.Id,
            nameBox.Text.Trim(),
            hostBox.Text.Trim(),
            checked((int)portBox.Value),
            userBox.Text.Trim(),
            hubBox.IsChecked == true);
        var knownHostsDirectory = Path.Combine(
            ApplicationData.Current.LocalFolder.Path,
            "ssh",
            "known-hosts");
        var hostKeyFingerprint = SshHostKeyTrust.CanReusePin(
            knownHostsDirectory,
            selected.Profile,
            updatedProfile)
            ? selected.Profile.HostKeyFingerprint
            : await ConfirmHostKeyAsync(updatedProfile);
        if (hostKeyFingerprint is null)
        {
            return;
        }
        var updated = new ServerViewModel(
            updatedProfile with { HostKeyFingerprint = hostKeyFingerprint });
        Servers[index] = updated;
        await SaveProfilesAsync();
        await RefreshServerAsync(updated);
        ShowInfo("Сервер изменён", updated.Name, InfoBarSeverity.Success);
    }

    private async void DeleteServerButton_Click(object sender, RoutedEventArgs e)
    {
        if (ServerList.SelectedItem is not ServerViewModel selected)
        {
            ShowInfo("Сервер не выбран", "Выберите сервер в списке для удаления.", InfoBarSeverity.Warning);
            return;
        }
        var dialog = new ContentDialog
        {
            XamlRoot = XamlRoot,
            Title = $"Удалить {selected.Name}?",
            Content = "Удаляется только локальный профиль. Серверная часть и WireGuard Node останутся установленными.",
            PrimaryButtonText = "Удалить профиль",
            CloseButtonText = "Отмена",
            DefaultButton = ContentDialogButton.Close
        };
        if (await dialog.ShowAsync() != ContentDialogResult.Primary)
        {
            return;
        }
        Servers.Remove(selected);
        _history.RemoveAll(sample => sample.ServerId == selected.Profile.Id);
        await SaveProfilesAsync();
        await _historyStorage.SaveAsync(_history);
        UpdateEmptyState();
        ShowInfo("Профиль удалён", selected.Name, InfoBarSeverity.Success);
    }

    private async void RefreshButton_Click(object sender, RoutedEventArgs e)
        => await RefreshAllAsync();

    private async Task RefreshAllAsync()
    {
        if (!await _refreshLock.WaitAsync(0))
        {
            return;
        }
        try
        {
            if (Servers.Count == 0)
            {
                if (_control.IsConfigured)
                {
                    await RefreshControlMeshAsync(showSuccess: false);
                    return;
                }
                ShowInfo("Серверы не добавлены", "Сначала создайте SSH-ключ и установите его на сервере.", InfoBarSeverity.Informational);
                return;
            }

            await Task.WhenAll(Servers.Select(RefreshServerAsync));
            await _historyStorage.SaveAsync(_history);
            var online = Servers.Count(server => server.IsOnline);
            var warnings = Servers.Count(server => !server.IsOnline || server.HasWarning);
            AvailabilityValueText.Text = $"{online} / {Servers.Count}";
            AvailabilityDetailText.Text = warnings == 0 ? "Все серверы доступны" : $"Недоступно: {warnings}";
            AverageLoadValueText.Text = online == 0
                ? "—"
                : $"{Servers.Where(server => server.IsOnline).Average(server => server.CpuPercent):F0}%";
            WarningValueText.Text = warnings.ToString(CultureInfo.InvariantCulture);
            WarningDetailText.Text = warnings == 0 ? "Нет предупреждений" : "Проверьте доступность и ресурсы";
            HeaderStatusText.Text = $"SSH monitoring · {Servers.Count} сервер(а) · обновлено {DateTime.Now:HH:mm:ss}";
            if (Servers.Any(server => server.IsHub && !server.HostKeyPendingConfirmation)
                || _control.IsConfigured)
            {
                await RefreshMeshAsync(showSuccess: false);
            }
        }
        finally
        {
            _refreshLock.Release();
        }
    }

    private async Task RefreshServerAsync(ServerViewModel server)
    {
        if (server.HostKeyPendingConfirmation)
        {
            server.Status = "Требуется подтверждение host key";
            server.IsOnline = false;
            server.HasWarning = true;
            return;
        }

        server.Status = "Подключение…";
        try
        {
            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
            var metrics = await _ssh.QueryAsync(server.Profile, timeout.Token);
            server.CpuPercent = metrics.CpuPercent;
            server.CpuText = $"{metrics.CpuPercent:F0}% load";
            server.MemoryText = $"{FormatSize(metrics.MemoryUsedKb)} / {FormatSize(metrics.MemoryTotalKb)} RAM";
            server.DiskText = $"{FormatSize(metrics.DiskUsedKb)} / {FormatSize(metrics.DiskTotalKb)} disk";
            var memoryPercent = Percent(metrics.MemoryUsedKb, metrics.MemoryTotalKb);
            var diskPercent = Percent(metrics.DiskUsedKb, metrics.DiskTotalKb);
            var inodePercent = Percent(metrics.InodesUsed, metrics.InodesTotal);
            server.HasWarning = memoryPercent >= 90 || diskPercent >= 90 || inodePercent >= 90 || metrics.SshState != "active";
            server.HealthText = $"swap {FormatSize(metrics.SwapUsedKb)}/{FormatSize(metrics.SwapTotalKb)} · inode {inodePercent:F0}% · net ↓{FormatBytes(metrics.NetworkRxBytes)} ↑{FormatBytes(metrics.NetworkTxBytes)} · SSH {metrics.SshState}";
            server.LatencyText = $"{metrics.Latency.TotalMilliseconds:F0} ms";
            server.Status = $"Онлайн · uptime {FormatUptime(metrics.Uptime)}";
            server.IsOnline = true;
            _history.Add(new MetricSampleData(
                server.Profile.Id,
                DateTimeOffset.Now,
                metrics.CpuPercent,
                memoryPercent,
                diskPercent));
            var overflow = _history
                .Where(sample => sample.ServerId == server.Profile.Id)
                .OrderByDescending(sample => sample.Timestamp)
                .Skip(240)
                .ToList();
            foreach (var sample in overflow)
            {
                _history.Remove(sample);
            }
            if (ServerList.SelectedItem == server)
            {
                RenderHistory();
            }
        }
        catch (Exception exception)
        {
            server.Status = CompactError(exception);
            server.CpuText = "—";
            server.CpuPercent = 0;
            server.MemoryText = "—";
            server.DiskText = "—";
            server.LatencyText = "offline";
            server.IsOnline = false;
            server.HasWarning = true;
            server.HealthText = "Нет данных о службах и сети";
        }
    }

    private async Task SaveProfilesAsync()
        => await _storage.SaveAsync(Servers.Select(server => server.Profile));

    private void UpdateEmptyState()
    {
        EmptyServersText.Visibility = Servers.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
        HeaderStatusText.Text = Servers.Count == 0
            ? "SSH monitoring · серверы ещё не добавлены"
            : $"SSH monitoring · {Servers.Count} сервер(а)";
        if (Servers.Count == 0)
        {
            AvailabilityValueText.Text = "0";
            AvailabilityDetailText.Text = "Добавьте серверы";
            AverageLoadValueText.Text = "—";
            WarningValueText.Text = "0";
            WarningDetailText.Text = "Нет данных";
        }
    }

    private static string FormatSize(long kilobytes)
        => kilobytes >= 1024 * 1024
            ? $"{kilobytes / 1024d / 1024d:F1} GB"
            : $"{kilobytes / 1024d:F0} MB";

    private static string FormatUptime(TimeSpan uptime)
        => uptime.TotalDays >= 1
            ? $"{uptime.TotalDays:F0} дн."
            : $"{uptime.TotalHours:F0} ч.";

    private static double Percent(long used, long total)
        => total <= 0 ? 0 : Math.Clamp(used * 100d / total, 0, 100);

    private static string FormatBytes(long bytes)
        => bytes >= 1024L * 1024 * 1024
            ? $"{bytes / 1024d / 1024d / 1024d:F1} GB"
            : $"{bytes / 1024d / 1024d:F1} MB";

    private void ServerList_SelectionChanged(object sender, SelectionChangedEventArgs e)
        => RenderHistory();

    private void HistoryChart_SizeChanged(object sender, SizeChangedEventArgs e)
        => RenderHistory();

    private void RenderHistory()
    {
        if (ServerList.SelectedItem is not ServerViewModel server)
        {
            HistoryCaptionText.Text = "Выберите сервер";
            CpuHistoryLine.Points = new PointCollection();
            MemoryHistoryLine.Points = new PointCollection();
            DiskHistoryLine.Points = new PointCollection();
            return;
        }

        var samples = _history
            .Where(sample => sample.ServerId == server.Profile.Id)
            .OrderBy(sample => sample.Timestamp)
            .ToList();
        HistoryCaptionText.Text = samples.Count == 0
            ? $"{server.Name} · данные появятся после обновления"
            : $"{server.Name} · {samples.Count} точек · {samples[0].Timestamp:dd.MM HH:mm} — {samples[^1].Timestamp:dd.MM HH:mm}";
        CpuHistoryLine.Points = BuildHistoryPoints(samples.Select(sample => sample.CpuPercent).ToList());
        MemoryHistoryLine.Points = BuildHistoryPoints(samples.Select(sample => sample.MemoryPercent).ToList());
        DiskHistoryLine.Points = BuildHistoryPoints(samples.Select(sample => sample.DiskPercent).ToList());
    }

    private PointCollection BuildHistoryPoints(IReadOnlyList<double> values)
    {
        var points = new PointCollection();
        if (values.Count == 0 || HistoryChart.ActualWidth <= 0 || HistoryChart.ActualHeight <= 0)
        {
            return points;
        }
        var step = values.Count == 1 ? 0 : HistoryChart.ActualWidth / (values.Count - 1);
        for (var index = 0; index < values.Count; index++)
        {
            var x = values.Count == 1 ? HistoryChart.ActualWidth : index * step;
            var y = HistoryChart.ActualHeight * (1 - Math.Clamp(values[index], 0, 100) / 100d);
            points.Add(new Point(x, y));
        }
        return points;
    }

    private static string CompactError(Exception exception)
    {
        var firstLine = exception.Message.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
        return string.IsNullOrWhiteSpace(firstLine) ? "Ошибка подключения" : firstLine;
    }

    private void ShowInfo(string title, string message, InfoBarSeverity severity)
    {
        LinkActionInfo.Title = title;
        LinkActionInfo.Message = message;
        LinkActionInfo.Severity = severity;
        LinkActionInfo.IsOpen = true;
    }

    private void SetMeshFirewallUnavailable(bool unavailable)
    {
        _meshFirewallUnavailable = unavailable;
        _linksPage?.SetFirewallUnavailable(unavailable);
    }

    private ServerViewModel? FindHub()
        => Servers.FirstOrDefault(server => server.IsHub);

    private async Task RefreshMeshAsync(bool showSuccess = true)
    {
        if (_control.IsConfigured)
        {
            await RefreshControlMeshAsync(showSuccess);
            return;
        }

        var hub = FindHub();
        if (hub is null)
        {
            ShowInfo("Mesh Hub не выбран", "Добавьте главный сервер или отметьте его как Mesh Hub.", InfoBarSeverity.Warning);
            return;
        }

        try
        {
            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
            var nodesOutput = await _ssh.RunRestrictedCommandAsync(hub.Profile, "mesh nodes", timeout.Token);
            var linksOutput = await _ssh.RunRestrictedCommandAsync(hub.Profile, "mesh links", timeout.Token);
            MeshNodes.Clear();
            foreach (var line in nodesOutput.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
            {
                if (!line.StartsWith("NODE=", StringComparison.Ordinal))
                {
                    continue;
                }
                var fields = line[5..].Split('|');
                if (fields.Length == 4 && int.TryParse(fields[3], out var age))
                {
                    MeshNodes.Add(new MeshNodeViewModel(fields[0], fields[1], fields[2], age));
                }
            }

            MeshLinks.Clear();
            foreach (var line in linksOutput.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
            {
                if (!line.StartsWith("LINK=", StringComparison.Ordinal))
                {
                    continue;
                }
                var fields = line[5..].Split('|');
                if (fields.Length >= 8
                    && int.TryParse(fields[4], CultureInfo.InvariantCulture, out var port)
                    && long.TryParse(fields[5], CultureInfo.InvariantCulture, out var expiresUnix)
                    && long.TryParse(fields[7], CultureInfo.InvariantCulture, out var version))
                {
                    MeshLinks.Add(new MeshLinkViewModel(
                        fields[0],
                        fields[1],
                        fields[2],
                        fields[3],
                        port,
                        expiresUnix,
                        fields[6],
                        version));
                }
            }

            ActiveLinksValueText.Text = MeshLinks.Count.ToString(CultureInfo.InvariantCulture);
            MeshStatusText.Text = $"{MeshNodes.Count} узлов · {MeshLinks.Count} активных связей";
            if (showSuccess)
            {
                ShowInfo("Mesh обновлён", MeshStatusText.Text, InfoBarSeverity.Success);
            }
        }
        catch (Exception exception)
        {
            MeshStatusText.Text = "Hub недоступен";
            ShowInfo("Не удалось получить Mesh-состояние", CompactError(exception), InfoBarSeverity.Error);
        }
    }

    private async Task RefreshControlMeshAsync(bool showSuccess)
    {
        try
        {
            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
            var agents = await _control.GetAgentsAsync(timeout.Token);
            var links = await _control.GetLinksAsync(timeout.Token);
            var now = DateTimeOffset.UtcNow;
            MeshNodes.Clear();
            foreach (var agent in agents)
            {
                var age = agent.LastSeenAt is null
                    ? int.MaxValue
                    : (int)Math.Clamp((now - agent.LastSeenAt.Value).TotalSeconds, 0, int.MaxValue);
                MeshNodes.Add(new MeshNodeViewModel(
                    agent.NodeId,
                    "Control",
                    agent.Status == "Revoked" ? "revoked" : age <= 90 ? "online" : "offline",
                    age));
            }

            MeshLinks.Clear();
            foreach (var link in links)
            {
                MeshLinks.Add(new MeshLinkViewModel(
                    link.SourceNodeId,
                    link.TargetNodeId,
                    link.TargetNodeId,
                    link.Protocol,
                    link.Port,
                    link.ExpiresAt?.ToUnixTimeSeconds() ?? 0,
                    link.ActualState,
                    link.Version,
                    link.Id,
                    link.DesiredState,
                    link.ActualState,
                    link.LastError));
            }

            SetMeshFirewallUnavailable(links
                .GroupBy(link => new { link.SourceNodeId, link.TargetNodeId, link.Protocol, link.Port })
                .Select(group => group.MaxBy(link => link.Version)!)
                .Any(link => link.LastError == MeshLinkViewModel.FirewallUnavailableErrorCode));
            _linksPage?.RefreshFilter();

            var activeLinks = MeshLinks.Count(link => link.ActualState == "Active");
            ActiveLinksValueText.Text = activeLinks.ToString(CultureInfo.InvariantCulture);
            MeshStatusText.Text = $"Control · {MeshNodes.Count} узлов · {activeLinks} активных · {MeshLinks.Count} политик";
            if (showSuccess)
            {
                ShowInfo("Control Hub обновлён", MeshStatusText.Text, InfoBarSeverity.Success);
            }
        }
        catch (Exception exception)
        {
            MeshStatusText.Text = "Control Hub недоступен";
            ShowInfo("Не удалось получить Control-состояние", CompactError(exception), InfoBarSeverity.Error);
        }
    }

    private async void RefreshMeshButton_Click(object sender, RoutedEventArgs e)
        => await RefreshMeshAsync();

    private async void ConnectLinkButton_Click(object sender, RoutedEventArgs e)
        => await ChangeLinkAsync(enable: true);

    private async void DisconnectLinkButton_Click(object sender, RoutedEventArgs e)
        => await ChangeLinkAsync(enable: false);

    private async void ReenrollNodeButton_Click(object sender, RoutedEventArgs e)
    {
        if (!_control.IsConfigured)
        {
            ShowInfo(
                "Control Hub не подключён",
                "Перерегистрация доступна после подключения через SMMDEV1.",
                InfoBarSeverity.Warning);
            return;
        }

        if (SourceNodeBox.SelectedItem is not MeshNodeViewModel node)
        {
            ShowInfo(
                "Выберите Node",
                "Выберите сервер в поле «Источник доступа».",
                InfoBarSeverity.Warning);
            return;
        }

        var confirmation = new ContentDialog
        {
            XamlRoot = XamlRoot,
            Title = $"Перерегистрировать {node.Name}?",
            Content = "Старый mTLS-сертификат будет немедленно отозван. Все связанные Links получат желаемое состояние Disabled. Новый token действует 10 минут.",
            PrimaryButtonText = "Отозвать и создать token",
            CloseButtonText = "Отмена",
            DefaultButton = ContentDialogButton.Close
        };
        if (await confirmation.ShowAsync() != ContentDialogResult.Primary)
        {
            return;
        }

        try
        {
            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(20));
            var ticket = await _control.ReenrollAgentAsync(
                node.Name,
                "Windows operator requested certificate rotation",
                timeout.Token);
            await RefreshMeshAsync(showSuccess: false);
            var tokenBox = new TextBox
            {
                Header = "Одноразовый enrollment token",
                Text = ticket.Token,
                IsReadOnly = true,
                TextWrapping = TextWrapping.Wrap
            };
            var result = await new ContentDialog
            {
                XamlRoot = XamlRoot,
                Title = $"{ticket.EntityId} ожидает перерегистрацию",
                Content = new StackPanel
                {
                    Spacing = 12,
                    Children =
                    {
                        new TextBlock
                        {
                            Text = $"Token действителен до {ticket.ExpiresAt.ToLocalTime():dd.MM.yyyy HH:mm}. Отключено Links: {ticket.DisabledLinks}. Передайте token только администратору этого Node.",
                            TextWrapping = TextWrapping.Wrap
                        },
                        tokenBox
                    }
                },
                PrimaryButtonText = "Копировать token",
                CloseButtonText = "Закрыть",
                DefaultButton = ContentDialogButton.Close
            }.ShowAsync();
            if (result == ContentDialogResult.Primary)
            {
                var package = new DataPackage();
                package.SetText(ticket.Token);
                Clipboard.SetContent(package);
                Clipboard.Flush();
                ShowInfo(
                    "Token скопирован",
                    "Он действует 10 минут и предназначен только для выбранного Node.",
                    InfoBarSeverity.Success);
            }
        }
        catch (Exception exception)
        {
            ShowInfo("Перерегистрация не выполнена", CompactError(exception), InfoBarSeverity.Error);
        }
    }

    private async Task ChangeLinkAsync(bool enable)
    {
        var useControl = _control.IsConfigured;
        var hub = FindHub();
        if (!useControl && hub is null)
        {
            ShowInfo("Mesh Hub не выбран", "Сначала добавьте главный сервер с отметкой Mesh Hub.", InfoBarSeverity.Warning);
            return;
        }
        MeshNodeViewModel? source = null;
        MeshNodeViewModel? target = null;
        string protocol;
        int port;
        int ttlMinutes;

        if (enable)
        {
            if (SourceNodeBox.SelectedItem is not MeshNodeViewModel selectedSource
                || TargetNodeBox.SelectedItem is not MeshNodeViewModel selectedTarget)
            {
                ShowInfo("Выберите серверы", "Укажите источник и сервер назначения.", InfoBarSeverity.Warning);
                return;
            }
            source = selectedSource;
            target = selectedTarget;
            protocol = (LinkProtocolBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "tcp";
            if (double.IsNaN(LinkPortBox.Value)
                || double.IsNaN(LinkTtlBox.Value)
                || LinkPortBox.Value is < 1 or > 65535
                || LinkTtlBox.Value is < 0 or > 525600)
            {
                ShowInfo("Некорректная политика", "Проверьте порт и TTL.", InfoBarSeverity.Warning);
                return;
            }
            port = checked((int)LinkPortBox.Value);
            ttlMinutes = checked((int)LinkTtlBox.Value);
        }
        else
        {
            if (MeshLinksList.SelectedItem is not MeshLinkViewModel selectedLink)
            {
                ShowInfo("Выберите связь", "Для отключения выберите правило в списке.", InfoBarSeverity.Warning);
                return;
            }
            source = MeshNodes.FirstOrDefault(node => node.Name == selectedLink.Source);
            target = MeshNodes.FirstOrDefault(node => node.Name == selectedLink.Target);
            if (source is null || target is null)
            {
                ShowInfo("Узел не найден", "Обновите список Mesh и повторите попытку.", InfoBarSeverity.Warning);
                return;
            }
            protocol = selectedLink.Protocol;
            port = selectedLink.Port;
            ttlMinutes = 0;
        }

        if (source.Name == target.Name)
        {
            ShowInfo("Некорректная связь", "Источник и назначение должны отличаться.", InfoBarSeverity.Warning);
            return;
        }

        try
        {
            if (useControl)
            {
                using var controlTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(20));
                LinkPolicy link;
                if (enable)
                {
                    link = await _control.CreateLinkAsync(
                        new LinkPolicyCreateRequest(
                            source.Name,
                            target.Name,
                            protocol,
                            port,
                            ttlMinutes,
                            "Windows client",
                            Guid.NewGuid().ToString()),
                        controlTimeout.Token);
                }
                else
                {
                    var selectedLink = (MeshLinkViewModel)MeshLinksList.SelectedItem;
                    if (string.IsNullOrWhiteSpace(selectedLink.Id))
                    {
                        throw new InvalidOperationException("У выбранного Link отсутствует Control id.");
                    }
                    link = await _control.DisableLinkAsync(selectedLink.Id, controlTimeout.Token);
                }
                await RefreshMeshAsync(showSuccess: false);
                ShowInfo(
                    enable ? "Control Link создан" : "Control Link отключён",
                    link.LastError == MeshLinkViewModel.NodeNotActivatedErrorCode
                        ? $"{source.Name} → {target.Name} · {protocol.ToUpperInvariant()}/{port} · ожидает активации Node в Mesh"
                        : $"{source.Name} → {target.Name} · {protocol.ToUpperInvariant()}/{port} · {link.ActualState} v{link.Version}",
                    link.LastError == MeshLinkViewModel.NodeNotActivatedErrorCode
                        ? InfoBarSeverity.Informational
                        : link.ActualState is "Failed" or "Partial"
                            ? InfoBarSeverity.Warning
                            : InfoBarSeverity.Success);
                return;
            }

            var action = enable ? "connect" : "disconnect";
            var policyArguments = enable
                ? $"{protocol} {port} {ttlMinutes}"
                : $"{protocol} {port}";
            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
            var commandOutput = await _ssh.RunRestrictedCommandAsync(
                hub!.Profile,
                $"mesh {action} {source.Name} {target.Name} {policyArguments}",
                timeout.Token);
            var confirmation = commandOutput
                .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
                .FirstOrDefault(line => line.StartsWith("LINK_STATE=", StringComparison.Ordinal));
            if (confirmation is null)
            {
                throw new InvalidOperationException("Hub не подтвердил фактическое состояние Link.");
            }
            await RefreshMeshAsync(showSuccess: false);
            ShowInfo(
                enable ? "Hub подтвердил Active" : "Hub подтвердил Disabled",
                $"{source.Name} → {target.Name} · {protocol.ToUpperInvariant()}/{port} · {confirmation[11..]}",
                enable ? InfoBarSeverity.Success : InfoBarSeverity.Warning);
        }
        catch (Exception exception)
        {
            ShowInfo("Не удалось изменить связь", CompactError(exception), InfoBarSeverity.Error);
        }
    }

    private void MainNavigation_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
    {
        if (args.IsSettingsSelected)
        {
            _settingsPage ??= new SettingsPage(this);
            ShowNavigationPage(_settingsPage);
            return;
        }

        var tag = (args.SelectedItem as NavigationViewItem)?.Tag?.ToString() ?? "overview";
        switch (tag)
        {
            case "servers":
                _serversPage ??= new ServersPage(this);
                ShowNavigationPage(_serversPage);
                break;
            case "links":
                _linksPage ??= new LinksPage(this);
                _linksPage.SetFirewallUnavailable(_meshFirewallUnavailable);
                ShowNavigationPage(_linksPage);
                break;
            case "sessions":
                _sessionsPage ??= new SessionsPage(this);
                ShowNavigationPage(_sessionsPage);
                break;
            default:
                ShowOverview();
                break;
        }
    }

    private void ShowOverview()
    {
        NavigationPageHost.Visibility = Visibility.Collapsed;
        NavigationPageHost.Content = null;
        OverviewCommandBar.Visibility = Visibility.Visible;
        OverviewSummary.Visibility = Visibility.Visible;
        WorkspaceScroll.Visibility = Visibility.Visible;
        ServerWorkspace.Visibility = Visibility.Visible;
        LinkInspector.Visibility = Visibility.Visible;
        Grid.SetColumnSpan(LinkInspector, 1);
        if (ActualWidth >= 1080)
        {
            Grid.SetRow(LinkInspector, 0);
            Grid.SetColumn(LinkInspector, 1);
            InspectorColumn.Width = new GridLength(360);
        }
        else
        {
            Grid.SetRow(LinkInspector, 1);
            Grid.SetColumn(LinkInspector, 0);
            InspectorColumn.Width = new GridLength(0);
        }
    }

    private void ShowNavigationPage(Page page)
    {
        OverviewCommandBar.Visibility = Visibility.Collapsed;
        OverviewSummary.Visibility = Visibility.Collapsed;
        WorkspaceScroll.Visibility = Visibility.Collapsed;
        NavigationPageHost.Content = page;
        NavigationPageHost.Visibility = Visibility.Visible;
    }
}
