// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Commands;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
using Windows.System;

namespace Microsoft.CmdPal.UI;

public sealed partial class ListPage : Page,
    IRecipient<NavigateNextCommand>,
    IRecipient<NavigatePreviousCommand>,
    IRecipient<NavigateLeftCommand>,
    IRecipient<NavigateRightCommand>,
    IRecipient<NavigatePageDownCommand>,
    IRecipient<NavigatePageUpCommand>,
    IRecipient<ActivateSelectedListItemMessage>,
    IRecipient<ActivateSecondaryCommandMessage>
{
    private InputSource _lastInputSource;

    private int _itemsUpdatedVersion;
    private bool _suppressSelectionChanged;

    private bool _scrollOnNextSelectionChange;

    private ListItemViewModel? _stickySelectedItem;
    private ListItemViewModel? _lastPushedToVm;
    private long _pendingContextMenuOpenRequestId;
    private Action? _cancelPendingContextMenuOpen;

    // A single search-text change can produce multiple ItemsUpdated calls
    // dispatched as separate UI-thread callbacks. A later "soft" update
    // (ForceFirstItem = false) must not overwrite a prior force-first
    // intent. This flag latches true whenever any update requests
    // force-first and is only cleared once selection stabilizes.
    private bool _forceFirstPending;

    internal ListViewModel? ViewModel
    {
        get => (ListViewModel?)GetValue(ViewModelProperty);
        set => SetValue(ViewModelProperty, value);
    }

    // Using a DependencyProperty as the backing store for ViewModel.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ViewModelProperty =
        DependencyProperty.Register(nameof(ViewModel), typeof(ListViewModel), typeof(ListPage), new PropertyMetadata(null, OnViewModelChanged));

    private ListViewBase ItemView => ViewModel?.IsGridView == true ? ItemsGrid : ItemsList;

    public ListPage()
    {
        this.InitializeComponent();
        this.NavigationCacheMode = NavigationCacheMode.Disabled;
        this.ItemView.Loaded += Items_Loaded;
        this.ItemView.PreviewKeyDown += Items_PreviewKeyDown;
        this.ItemView.PointerPressed += Items_PointerPressed;
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        if (e.Parameter is not AsyncNavigationRequest navigationRequest)
        {
            throw new InvalidOperationException($"Invalid navigation parameter: {nameof(e.Parameter)} must be {nameof(AsyncNavigationRequest)}");
        }

        if (navigationRequest.TargetViewModel is not ListViewModel listViewModel)
        {
            throw new InvalidOperationException($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(ListViewModel)}");
        }

        ViewModel = listViewModel;

        if (e.NavigationMode == NavigationMode.Back)
        {
            // Must dispatch the selection to run at a lower priority; otherwise, GetFirstSelectableIndex
            // may return an incorrect index because item containers are not yet rendered.
            _ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
            {
                // Only do this if we truly have no selection.
                if (ItemView.SelectedItem is null)
                {
                    var firstUsefulIndex = GetFirstSelectableIndex();
                    if (firstUsefulIndex != -1)
                    {
                        using (SuppressSelectionChangedScope())
                        {
                            ItemView.SelectedIndex = firstUsefulIndex;
                        }
                    }
                }
            });
        }

        // RegisterAll isn't AOT compatible
        WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
        WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
        WeakReferenceMessenger.Default.Register<NavigateLeftCommand>(this);
        WeakReferenceMessenger.Default.Register<NavigateRightCommand>(this);
        WeakReferenceMessenger.Default.Register<NavigatePageDownCommand>(this);
        WeakReferenceMessenger.Default.Register<NavigatePageUpCommand>(this);
        WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
        WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);

        base.OnNavigatedTo(e);
    }

    protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
    {
        base.OnNavigatingFrom(e);

        CancelPendingContextMenuOpen();

        WeakReferenceMessenger.Default.Unregister<NavigateNextCommand>(this);
        WeakReferenceMessenger.Default.Unregister<NavigatePreviousCommand>(this);
        WeakReferenceMessenger.Default.Unregister<NavigateLeftCommand>(this);
        WeakReferenceMessenger.Default.Unregister<NavigateRightCommand>(this);
        WeakReferenceMessenger.Default.Unregister<NavigatePageDownCommand>(this);
        WeakReferenceMessenger.Default.Unregister<NavigatePageUpCommand>(this);
        WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
        WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);

        if (ViewModel is not null)
        {
            ViewModel.ItemsUpdated -= Page_ItemsUpdated;
        }

        if (e.NavigationMode != NavigationMode.New)
        {
            ViewModel?.SafeCleanup();
            CleanupHelper.Cleanup(this);
        }

        // Clean-up event listeners
        ViewModel = null;

        GC.Collect();
    }

    /// <summary>
    /// Finds the index of the first item in the list that is not a separator.
    /// Returns -1 if the list is empty or only contains separators.
    /// </summary>
    private int GetFirstSelectableIndex()
    {
        return FindSelectableIndex(0, 1);
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
    private void Items_ItemClick(object sender, ItemClickEventArgs e)
    {
        if (e.ClickedItem is ListItemViewModel item)
        {
            if (_lastInputSource == InputSource.Keyboard)
            {
                ViewModel?.InvokeItemCommand.Execute(item);
                return;
            }

            var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
            if (settings.SingleClickActivates)
            {
                ViewModel?.InvokeItemCommand.Execute(item);
            }
            else
            {
                // Click-driven selection should scroll into view (but only once).
                _scrollOnNextSelectionChange = true;

                ViewModel?.UpdateSelectedItemCommand.Execute(item);
                WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
            }
        }
    }

    private void Items_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
    {
        if (ItemView.SelectedItem is ListItemViewModel vm)
        {
            var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
            if (!settings.SingleClickActivates)
            {
                ViewModel?.InvokeItemCommand.Execute(vm);
            }
        }
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
    private void Items_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (_suppressSelectionChanged)
        {
            return;
        }

        var vm = ViewModel;
        var li = ItemView.SelectedItem as ListItemViewModel;

        // Transient null/separator selection can happen during in-place updates.
        // Do not push null into the VM; Page_ItemsUpdated will repair selection.
        if (li is null || IsSeparator(li))
        {
            return;
        }

        _stickySelectedItem = li;

        // User explicitly changed selection — any pending force-first intent
        // is superseded by the user's navigation.
        _forceFirstPending = false;

        // Do not Task.Run (it reorders selection updates).
        vm?.UpdateSelectedItemCommand.Execute(li);

        // Only scroll when explicitly requested by navigation/click handlers.
        if (_scrollOnNextSelectionChange)
        {
            _scrollOnNextSelectionChange = false;
            ScrollToItem(li);
        }

        // Automation notification for screen readers
        var listViewPeer = ListViewAutomationPeer.CreatePeerForElement(ItemView);
        if (listViewPeer is not null)
        {
            UIHelper.AnnounceActionForAccessibility(
                ItemsList,
                li.Title,
                "CommandPaletteSelectedItemChanged");
        }
    }

    private void ScrollToItem(ListItemViewModel li)
    {
        var scrollTarget = li;

        // If the previous item is a separator, also scroll it into view to provide
        // better context for the user
        var index = ItemView.Items.IndexOf(li);
        if (index > 0)
        {
            var prevItem = ItemView.Items[index - 1] as ListItemViewModel;
            if (prevItem?.Type == ListItemType.SectionHeader)
            {
                scrollTarget = prevItem;
            }
        }

        if (scrollTarget is not null)
        {
            ItemView.ScrollIntoView(scrollTarget);
        }
    }

    private void Items_RightTapped(object sender, RightTappedRoutedEventArgs e)
    {
        if (e.OriginalSource is FrameworkElement element &&
            element.DataContext is ListItemViewModel item)
        {
            if (ItemView.SelectedItem != item)
            {
                _scrollOnNextSelectionChange = true;

                using (SuppressSelectionChangedScope())
                {
                    ItemView.SelectedItem = item;
                }
            }

            ViewModel?.UpdateSelectedItemCommand.Execute(item);

            var pos = e.GetPosition(element);
            RequestContextMenuOpen(item, element, pos);
        }
    }

    private void Items_Loaded(object sender, RoutedEventArgs e)
    {
        // Find the ScrollViewer in the ItemView (ItemsList or ItemsGrid)
        var listViewScrollViewer = FindScrollViewer(ItemView);

        if (listViewScrollViewer is not null)
        {
            listViewScrollViewer.ViewChanged += ListViewScrollViewer_ViewChanged;
        }
    }

    private void ListViewScrollViewer_ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e)
    {
        var scrollView = sender as ScrollViewer;
        if (scrollView is null)
        {
            return;
        }

        // When we get to the bottom, request more from the extension, if they
        // have more to give us.
        // We're checking when we get to 80% of the scroll height, to give the
        // extension a bit of a heads-up before the user actually gets there.
        if (scrollView.VerticalOffset >= (scrollView.ScrollableHeight * .8))
        {
            ViewModel?.LoadMoreIfNeeded();
        }
    }

    // Message-driven navigation should count as keyboard.
    private void MarkKeyboardNavigation() => _lastInputSource = InputSource.Keyboard;

    private void PushSelectionToVm()
    {
        if (ViewModel is null)
        {
            return;
        }

        if (ItemView.SelectedItem is not ListItemViewModel li || IsSeparator(li))
        {
            ViewModel.UpdateSelectedItemCommand.Execute(null);
            return;
        }

        if (ReferenceEquals(_lastPushedToVm, li))
        {
            return;
        }

        _lastPushedToVm = li;
        _stickySelectedItem = li;
        ViewModel.UpdateSelectedItemCommand.Execute(li);
    }

    public void Receive(NavigateNextCommand message)
    {
        MarkKeyboardNavigation();
        _scrollOnNextSelectionChange = true;

        if (ViewModel?.IsGridView == true)
        {
            // For grid views, use spatial navigation (down)
            HandleGridArrowNavigation(VirtualKey.Down);
        }
        else
        {
            // For list views, use simple linear navigation
            NavigateDown();
        }

        PushSelectionToVm();
    }

    public void Receive(NavigatePreviousCommand message)
    {
        MarkKeyboardNavigation();
        _scrollOnNextSelectionChange = true;

        if (ViewModel?.IsGridView == true)
        {
            // For grid views, use spatial navigation (up)
            HandleGridArrowNavigation(VirtualKey.Up);
        }
        else
        {
            NavigateUp();
        }

        PushSelectionToVm();
    }

    public void Receive(NavigateLeftCommand message)
    {
        MarkKeyboardNavigation();
        _scrollOnNextSelectionChange = true;

        // For grid views, use spatial navigation. For list views, just move up.
        if (ViewModel?.IsGridView == true)
        {
            HandleGridArrowNavigation(VirtualKey.Left);
            PushSelectionToVm();
        }
        else
        {
            // In list view, left arrow doesn't navigate
            // This maintains consistency with the SearchBar behavior
        }
    }

    public void Receive(NavigateRightCommand message)
    {
        MarkKeyboardNavigation();
        _scrollOnNextSelectionChange = true;

        // For grid views, use spatial navigation. For list views, just move down.
        if (ViewModel?.IsGridView == true)
        {
            HandleGridArrowNavigation(VirtualKey.Right);
            PushSelectionToVm();
        }
        else
        {
            // In list view, right arrow doesn't navigate
            // This maintains consistency with the SearchBar behavior
        }
    }

    public void Receive(ActivateSelectedListItemMessage message)
    {
        if (ViewModel?.ShowEmptyContent ?? false)
        {
            ViewModel?.InvokeItemCommand.Execute(null);
        }
        else if (ItemView.SelectedItem is ListItemViewModel item)
        {
            ViewModel?.InvokeItemCommand.Execute(item);
        }
    }

    public void Receive(ActivateSecondaryCommandMessage message)
    {
        if (ViewModel?.ShowEmptyContent ?? false)
        {
            ViewModel?.InvokeSecondaryCommandCommand.Execute(null);
        }
        else if (ItemView.SelectedItem is ListItemViewModel item)
        {
            ViewModel?.InvokeSecondaryCommandCommand.Execute(item);
        }
    }

    public void Receive(NavigatePageDownCommand message)
    {
        MarkKeyboardNavigation();
        _scrollOnNextSelectionChange = true;

        var indexes = CalculateTargetIndexPageUpDownScrollTo(true);
        if (indexes is null)
        {
            return;
        }

        if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
        {
            ItemView.SelectedIndex = indexes.Value.TargetIndex;
        }

        PushSelectionToVm();
    }

    public void Receive(NavigatePageUpCommand message)
    {
        MarkKeyboardNavigation();
        _scrollOnNextSelectionChange = true;

        var indexes = CalculateTargetIndexPageUpDownScrollTo(false);
        if (indexes is null)
        {
            return;
        }

        if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
        {
            ItemView.SelectedIndex = indexes.Value.TargetIndex;
        }

        PushSelectionToVm();
    }

    /// <summary>
    /// Calculates the item index to target when performing a page up or page down
    /// navigation. The calculation attempts to estimate how many items fit into
    /// the visible viewport by measuring actual container heights currently visible
    /// within the internal ScrollViewer. If measurements are not available a
    /// fallback estimate is used.
    /// </summary>
    /// <param name="isPageDown">True to calculate a page-down target, false for page-up.</param>
    /// <returns>
    /// A tuple containing the current index and the calculated target index, or null
    /// if a valid calculation could not be performed (for example, missing ScrollViewer).
    /// </returns>
    private (int CurrentIndex, int TargetIndex)? CalculateTargetIndexPageUpDownScrollTo(bool isPageDown)
    {
        var scroll = FindScrollViewer(ItemView);
        if (scroll is null)
        {
            return null;
        }

        var viewportHeight = scroll.ViewportHeight;
        if (viewportHeight <= 0)
        {
            return null;
        }

        var currentIndex = ItemView.SelectedIndex < 0 ? 0 : ItemView.SelectedIndex;
        var itemCount = ItemView.Items.Count;

        // Compute visible item heights within the ScrollViewer viewport
        const int firstVisibleIndexNotFound = -1;
        var firstVisibleIndex = firstVisibleIndexNotFound;
        var visibleHeights = new List<double>(itemCount);

        for (var i = 0; i < itemCount; i++)
        {
            if (ItemView.ContainerFromIndex(i) is FrameworkElement container)
            {
                try
                {
                    var transform = container.TransformToVisual(scroll);
                    var topLeft = transform.TransformPoint(new Point(0, 0));
                    var bottom = topLeft.Y + container.ActualHeight;

                    // If any part of the container is inside the viewport, consider it visible
                    if (topLeft.Y >= 0 && bottom <= viewportHeight)
                    {
                        if (firstVisibleIndex == firstVisibleIndexNotFound)
                        {
                            firstVisibleIndex = i;
                        }

                        visibleHeights.Add(container.ActualHeight > 0 ? container.ActualHeight : 0);
                    }
                }
                catch
                {
                    // ignore transform errors and continue
                }
            }
        }

        var itemsPerPage = 0;

        // Calculate how many items fit in the viewport based on their actual heights
        if (visibleHeights.Count > 0)
        {
            double accumulated = 0;
            for (var i = 0; i < visibleHeights.Count; i++)
            {
                accumulated += visibleHeights[i] <= 0 ? 1 : visibleHeights[i];
                itemsPerPage++;
                if (accumulated >= viewportHeight)
                {
                    break;
                }
            }
        }
        else
        {
            // fallback: estimate using first measured container height
            double itemHeight = 0;
            for (var i = currentIndex; i < itemCount; i++)
            {
                if (ItemView.ContainerFromIndex(i) is FrameworkElement { ActualHeight: > 0 } c)
                {
                    itemHeight = c.ActualHeight;
                    break;
                }
            }

            if (itemHeight <= 0)
            {
                itemHeight = 1;
            }

            itemsPerPage = Math.Max(1, (int)Math.Floor(viewportHeight / itemHeight));
        }

        var targetIndex = isPageDown
            ? Math.Min(itemCount - 1, currentIndex + Math.Max(1, itemsPerPage))
            : Math.Max(0, currentIndex - Math.Max(1, itemsPerPage));

        targetIndex = FindSelectableIndexForPageNavigation(targetIndex, isPageDown);
        if (targetIndex == -1)
        {
            return null;
        }

        return (currentIndex, targetIndex);
    }

    private int FindSelectableIndex(int startIndex, int step)
    {
        var items = ItemView.Items;
        var count = items.Count;
        if (count == 0 || step == 0)
        {
            return -1;
        }

        startIndex = Math.Clamp(startIndex, 0, count - 1);

        for (var i = startIndex; i >= 0 && i < count; i += step)
        {
            if (!IsSeparator(items[i]))
            {
                return i;
            }
        }

        return -1;
    }

    private int FindSelectableIndexForPageNavigation(int targetIndex, bool isPageDown)
    {
        var step = isPageDown ? 1 : -1;
        var index = FindSelectableIndex(targetIndex, step);
        return index != -1 ? index : FindSelectableIndex(targetIndex - step, -step);
    }

    private static void OnViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is ListPage @this)
        {
            if (e.OldValue is ListViewModel old)
            {
                old.ItemsUpdated -= @this.Page_ItemsUpdated;
            }

            if (e.NewValue is ListViewModel page)
            {
                @this._forceFirstPending = false;
                page.ItemsUpdated += @this.Page_ItemsUpdated;
            }
            else if (e.NewValue is null)
            {
                @this._forceFirstPending = false;
                Logger.LogDebug("cleared view model");
            }
        }
    }

    // Called after we've finished updating the whole list for either a
    // GetItems or a change in the filter.
    private void Page_ItemsUpdated(ListViewModel sender, ItemsUpdatedEventArgs args)
    {
        var version = Interlocked.Increment(ref _itemsUpdatedVersion);

        // Latch: once any update requests force-first, keep it until consumed.
        _forceFirstPending |= args.ForceFirstItem;
        var forceFirstItem = _forceFirstPending;

        // Try to handle selection immediately — items should already be available
        // since FilteredItems is a direct ObservableCollection bound as ItemsSource.
        // TrySetSelectionAfterUpdate clears _forceFirstPending internally once
        // selection stabilizes (no repair needed), so we don't clear it here.
        if (TrySetSelectionAfterUpdate(sender, version, forceFirstItem))
        {
            return;
        }

        // Fallback: binding hasn't propagated yet, defer to next tick.
        _ = DispatcherQueue.TryEnqueue(
            Microsoft.UI.Dispatching.DispatcherQueuePriority.Low,
            () =>
            {
                if (version != Volatile.Read(ref _itemsUpdatedVersion))
                {
                    return;
                }

                TrySetSelectionAfterUpdate(sender, version, forceFirstItem);
            });
    }

    /// <summary>
    /// Applies selection after an items update. Returns false if ItemView.Items
    /// is not yet populated (caller should defer and retry).
    /// </summary>
    /// <param name="forceFirstItem">
    /// When true, always select the first selectable item and scroll to top
    /// (used for filter changes and top-level fetches).
    /// </param>
    private bool TrySetSelectionAfterUpdate(ListViewModel sender, long version, bool forceFirstItem)
    {
        if (version != Volatile.Read(ref _itemsUpdatedVersion))
        {
            return true; // superseded by a newer update, nothing to do
        }

        var vm = ViewModel;
        if (vm is null)
        {
            return true;
        }

        // Use the stable source of truth, not ItemView.Items (which can be transiently empty)
        if (vm.FilteredItems.Count == 0)
        {
            using (SuppressSelectionChangedScope())
            {
                ItemView.SelectedIndex = -1;
                _stickySelectedItem = null;
                _lastPushedToVm = null;
            }

            PushSelectionToVm();
            return true;
        }

        // If ItemView.Items hasn't caught up with the ObservableCollection yet,
        // signal the caller to defer and retry.
        var items = ItemView.Items;
        if (items is null || items.Count == 0)
        {
            return false;
        }

        var firstUsefulIndex = GetFirstSelectableIndex();
        if (firstUsefulIndex == -1)
        {
            using (SuppressSelectionChangedScope())
            {
                ItemView.SelectedIndex = -1;
                _stickySelectedItem = null;
                _lastPushedToVm = null;
            }

            PushSelectionToVm();
            return true;
        }

        var shouldUpdateSelection = forceFirstItem;

        if (!shouldUpdateSelection)
        {
            // Check if selection needs repair (item gone, null, or separator).
            if (ItemView.SelectedItem is null)
            {
                shouldUpdateSelection = true;
            }
            else if (IsSeparator(ItemView.SelectedItem))
            {
                shouldUpdateSelection = true;
            }
            else if (!items.Contains(ItemView.SelectedItem))
            {
                shouldUpdateSelection = true;
            }
        }

        if (shouldUpdateSelection)
        {
            using (SuppressSelectionChangedScope())
            {
                ListItemViewModel? stickyRestored = null;
                ListItemViewModel? firstSelected = null;

                if (!forceFirstItem &&
                    _stickySelectedItem is not null &&
                    items.Contains(_stickySelectedItem) &&
                    !IsSeparator(_stickySelectedItem))
                {
                    // Restore sticky selection only when force-first is not
                    // active. The latched _forceFirstPending flag guarantees
                    // that a prior force-first intent survives superseding
                    // soft updates, so we never accidentally restore a stale
                    // sticky item when the list was meant to reset.
                    ItemView.SelectedItem = _stickySelectedItem;
                    stickyRestored = _stickySelectedItem;
                }
                else
                {
                    // Select the first interactive item.
                    ItemView.SelectedItem = items[firstUsefulIndex];
                    firstSelected = ItemView.SelectedItem as ListItemViewModel;
                }

                // Prevent any pending "scroll on selection" logic from fighting this.
                _scrollOnNextSelectionChange = false;

                _ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
                {
                    if (version != Volatile.Read(ref _itemsUpdatedVersion))
                    {
                        return;
                    }

                    ItemView.UpdateLayout();

                    if (stickyRestored is not null)
                    {
                        ScrollToItem(stickyRestored);
                    }
                    else if (firstSelected is not null)
                    {
                        ScrollToItem(firstSelected);
                        ResetScrollToTop();
                    }
                    else
                    {
                        ResetScrollToTop();
                    }
                });
            }
        }
        else
        {
            // Selection is valid and unchanged: the force-first intent (if any)
            // has been fully delivered and selection has stabilized. Safe to clear.
            _forceFirstPending = false;

            // Just make sure the item is visible
            if (_stickySelectedItem is ListItemViewModel li)
            {
                _ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
                {
                    ItemView.UpdateLayout();
                    ScrollToItem(li);
                });
            }
        }

        PushSelectionToVm();
        return true;
    }

    private static ScrollViewer? FindScrollViewer(DependencyObject parent)
    {
        if (parent is ScrollViewer viewer)
        {
            return viewer;
        }

        for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
        {
            var child = VisualTreeHelper.GetChild(parent, i);
            var result = FindScrollViewer(child);
            if (result is not null)
            {
                return result;
            }
        }

        return null;
    }

    // Find a logical neighbor in the requested direction using containers' positions.
    private void HandleGridArrowNavigation(VirtualKey key)
    {
        if (ItemView.Items.Count == 0)
        {
            return;
        }

        var currentIndex = ItemView.SelectedIndex;
        if (currentIndex < 0)
        {
            // -1 is a valid value (no item currently selected)
            currentIndex = 0;
            ItemView.SelectedIndex = 0;
        }

        try
        {
            // Try to compute using container positions; if not available, fall back to simple +/-1.
            var currentContainer = ItemView.ContainerFromIndex(currentIndex) as FrameworkElement;
            if (currentContainer is not null && currentContainer.ActualWidth != 0 && currentContainer.ActualHeight != 0)
            {
                // Use center of current container as reference
                var curPoint = currentContainer.TransformToVisual(ItemView).TransformPoint(new Point(0, 0));
                var curCenterX = curPoint.X + (currentContainer.ActualWidth / 2.0);
                var curCenterY = curPoint.Y + (currentContainer.ActualHeight / 2.0);

                var bestScore = double.MaxValue;
                var bestIndex = currentIndex;

                for (var i = 0; i < ItemView.Items.Count; i++)
                {
                    if (i == currentIndex)
                    {
                        continue;
                    }

                    if (IsSeparator(ItemView.Items[i]))
                    {
                        continue;
                    }

                    if (ItemView.ContainerFromIndex(i) is FrameworkElement c && c.ActualWidth > 0 && c.ActualHeight > 0)
                    {
                        var p = c.TransformToVisual(ItemView).TransformPoint(new Point(0, 0));
                        var centerX = p.X + (c.ActualWidth / 2.0);
                        var centerY = p.Y + (c.ActualHeight / 2.0);

                        var dx = centerX - curCenterX;
                        var dy = centerY - curCenterY;

                        var candidate = false;
                        var score = double.MaxValue;

                        switch (key)
                        {
                            case VirtualKey.Left:
                                if (dx < 0)
                                {
                                    candidate = true;
                                    score = Math.Abs(dy) + (Math.Abs(dx) * 0.7);
                                }

                                break;
                            case VirtualKey.Right:
                                if (dx > 0)
                                {
                                    candidate = true;
                                    score = Math.Abs(dy) + (Math.Abs(dx) * 0.7);
                                }

                                break;
                            case VirtualKey.Up:
                                if (dy < 0)
                                {
                                    candidate = true;
                                    score = Math.Abs(dx) + (Math.Abs(dy) * 0.7);
                                }

                                break;
                            case VirtualKey.Down:
                                if (dy > 0)
                                {
                                    candidate = true;
                                    score = Math.Abs(dx) + (Math.Abs(dy) * 0.7);
                                }

                                break;
                        }

                        if (candidate && score < bestScore)
                        {
                            bestScore = score;
                            bestIndex = i;
                        }
                    }
                }

                if (bestIndex != currentIndex)
                {
                    ItemView.SelectedIndex = bestIndex;
                }

                return;
            }
        }
        catch
        {
            // ignore transform errors and fall back
        }

        // fallback linear behavior
        var fallback = key switch
        {
            VirtualKey.Left => Math.Max(0, currentIndex - 1),
            VirtualKey.Right => Math.Min(ItemView.Items.Count - 1, currentIndex + 1),
            VirtualKey.Up => Math.Max(0, currentIndex - 1),
            VirtualKey.Down => Math.Min(ItemView.Items.Count - 1, currentIndex + 1),
            _ => currentIndex,
        };
        if (fallback != currentIndex)
        {
            ItemView.SelectedIndex = fallback;
        }
    }

    private void Items_OnContextRequested(UIElement sender, ContextRequestedEventArgs e)
    {
        var (item, element) = e.OriginalSource switch
        {
            // caused by keyboard shortcut (e.g. Context menu key or Shift+F10)
            SelectorItem selectorItem => (ItemView.ItemFromContainer(selectorItem) as ListItemViewModel, selectorItem),

            // caused by right-click on the ListViewItem
            FrameworkElement { DataContext: ListItemViewModel itemViewModel } frameworkElement => (itemViewModel, frameworkElement),

            _ => (null, null),
        };

        if (item is null || element is null)
        {
            return;
        }

        if (ItemView.SelectedItem != item)
        {
            _scrollOnNextSelectionChange = true;

            using (SuppressSelectionChangedScope())
            {
                ItemView.SelectedItem = item;
            }
        }

        if (!e.TryGetPosition(element, out var pos))
        {
            pos = new(0, element.ActualHeight);
        }

        ViewModel?.UpdateSelectedItemCommand.Execute(item);
        RequestContextMenuOpen(item, element, pos);
        e.Handled = true;
    }

    private void Items_OnContextCanceled(UIElement sender, RoutedEventArgs e)
    {
        CancelPendingContextMenuOpen();
        _ = DispatcherQueue.TryEnqueue(() => WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>());
    }

    private void Items_PointerPressed(object sender, PointerRoutedEventArgs e) => _lastInputSource = InputSource.Pointer;

    private void Items_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
    {
        // Track keyboard as the last input source for activation logic.
        if (e.Key is VirtualKey.Enter or VirtualKey.Space)
        {
            _lastInputSource = InputSource.Keyboard;
            return;
        }

        // Handle arrow navigation when we're showing a grid.
        if (ViewModel?.IsGridView == true)
        {
            switch (e.Key)
            {
                case VirtualKey.Left:
                case VirtualKey.Right:
                case VirtualKey.Up:
                case VirtualKey.Down:
                    _lastInputSource = InputSource.Keyboard;
                    _scrollOnNextSelectionChange = true;
                    HandleGridArrowNavigation(e.Key);
                    e.Handled = true;
                    break;
            }
        }
    }

    /// <summary>
    ///  Code stealed from <see cref="Controls.ContextMenu.NavigateUp"/>
    /// </summary>
    private void NavigateUp()
    {
        if (ItemView.Items.Count == 0)
        {
            return;
        }

        var newIndex = ItemView.SelectedIndex > 0
            ? FindSelectableIndex(ItemView.SelectedIndex - 1, -1)
            : -1;

        if (newIndex == -1)
        {
            newIndex = FindSelectableIndex(ItemView.Items.Count - 1, -1);
        }

        if (newIndex != -1)
        {
            ItemView.SelectedIndex = newIndex;
        }
    }

    private void Items_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
    {
        try
        {
            if (e.Items.FirstOrDefault() is not ListItemViewModel item || item.DataPackage is null)
            {
                e.Cancel = true;
                return;
            }

            // copy properties
            foreach (var (key, value) in item.DataPackage.Properties)
            {
                try
                {
                    e.Data.Properties[key] = value;
                }
                catch (Exception)
                {
                    // noop - skip any properties that fail
                }
            }

            // setup e.Data formats as deferred renderers to read from the item's DataPackage
            foreach (var format in item.DataPackage.AvailableFormats)
            {
                try
                {
                    e.Data.SetDataProvider(format, request => DelayRenderer(request, item, format));
                }
                catch (Exception)
                {
                    // noop - skip any formats that fail
                }
            }

            WeakReferenceMessenger.Default.Send(new DragStartedMessage());
        }
        catch (Exception ex)
        {
            WeakReferenceMessenger.Default.Send(new DragCompletedMessage());
            Logger.LogError("Failed to start dragging an item", ex);
        }
    }

    private static void DelayRenderer(DataProviderRequest request, ListItemViewModel item, string format)
    {
        var deferral = request.GetDeferral();
        try
        {
            item.DataPackage?.GetDataAsync(format)
                .AsTask()
                .ContinueWith(dataTask =>
                {
                    try
                    {
                        if (dataTask.IsCompletedSuccessfully)
                        {
                            request.SetData(dataTask.Result);
                        }
                        else if (dataTask.IsFaulted && dataTask.Exception is not null)
                        {
                            Logger.LogError($"Failed to get data for format '{format}' during drag-and-drop", dataTask.Exception);
                        }
                    }
                    finally
                    {
                        deferral.Complete();
                    }
                });
        }
        catch (Exception ex)
        {
            Logger.LogError($"Failed to set data for format '{format}' during drag-and-drop", ex);
            deferral.Complete();
        }
    }

    private void Items_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
    {
        WeakReferenceMessenger.Default.Send(new DragCompletedMessage());
    }

    /// <summary>
    ///  Code stealed from <see cref="Controls.ContextMenu.NavigateDown"/>
    /// </summary>
    private void NavigateDown()
    {
        if (ItemView.Items.Count == 0)
        {
            return;
        }

        var newIndex = ItemView.SelectedIndex < ItemView.Items.Count - 1
            ? FindSelectableIndex(ItemView.SelectedIndex + 1, 1)
            : -1;

        if (newIndex == -1)
        {
            newIndex = FindSelectableIndex(0, 1);
        }

        if (newIndex != -1)
        {
            ItemView.SelectedIndex = newIndex;
        }
    }

    private bool IsSeparator(object? item) => item is ListItemViewModel li && !li.IsInteractive;

    private bool IsSectionHeader(object? item) => item is ListItemViewModel li && li.Type == ListItemType.SectionHeader;

    private void ResetScrollToTop()
    {
        var scroll = FindScrollViewer(ItemView);
        if (scroll is null)
        {
            return;
        }

        // disableAnimation: true prevents a visible jump animation
        scroll.ChangeView(horizontalOffset: null, verticalOffset: 0, zoomFactor: null, disableAnimation: true);
    }

    private void RequestContextMenuOpen(ListItemViewModel item, FrameworkElement element, Point pos)
    {
        // BEAR LOADING: Right-click can arrive before the selected item's slow
        // context-menu hydration completes, especially for out-of-proc
        // providers. Keep this exact open request alive until the same
        // selected item becomes context-openable instead of dropping the first
        // click.
        CancelPendingContextMenuOpen();
        var requestId = Interlocked.Increment(ref _pendingContextMenuOpenRequestId);

        System.ComponentModel.PropertyChangedEventHandler? onItemChanged = null;
        Action? detach = null;
        detach = () =>
        {
            if (onItemChanged is not null)
            {
                item.PropertyChanged -= onItemChanged;
            }

            if (ReferenceEquals(_cancelPendingContextMenuOpen, detach))
            {
                _cancelPendingContextMenuOpen = null;
            }
        };

        onItemChanged = (_, args) =>
        {
            if (args.PropertyName is nameof(ListItemViewModel.CanOpenContextMenu) or nameof(ListItemViewModel.AllCommands) &&
                TryOpenContextMenuIfReady(item, element, pos, requestId))
            {
                detach();
            }
        };

        item.PropertyChanged += onItemChanged;
        _cancelPendingContextMenuOpen = detach;

        if (TryOpenContextMenuIfReady(item, element, pos, requestId))
        {
            detach();
        }
    }

    private bool TryOpenContextMenuIfReady(ListItemViewModel item, FrameworkElement element, Point pos, long requestId)
    {
        // Ignore stale requests so rapid selection changes or cancelled opens
        // can't resurrect an old context menu on the wrong item.
        if (requestId != Volatile.Read(ref _pendingContextMenuOpenRequestId) ||
            !ReferenceEquals(ItemView.SelectedItem, item) ||
            !item.CanOpenContextMenu)
        {
            return false;
        }

        _ = DispatcherQueue.TryEnqueue(
            () =>
            {
                if (requestId != Volatile.Read(ref _pendingContextMenuOpenRequestId) ||
                    !ReferenceEquals(ItemView.SelectedItem, item))
                {
                    return;
                }

                WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
                    new OpenContextMenuMessage(
                        element,
                        FlyoutPlacementMode.BottomEdgeAlignedLeft,
                        pos,
                        ContextMenuFilterLocation.Top));
            });

        return true;
    }

    private void CancelPendingContextMenuOpen()
    {
        Interlocked.Increment(ref _pendingContextMenuOpenRequestId);
        _cancelPendingContextMenuOpen?.Invoke();
        _cancelPendingContextMenuOpen = null;
    }

    private IDisposable SuppressSelectionChangedScope()
    {
        _suppressSelectionChanged = true;
        return new ActionOnDispose(() => _suppressSelectionChanged = false);
    }

    private sealed partial class ActionOnDispose(Action action) : IDisposable
    {
        public void Dispose() => action();
    }

    private enum InputSource
    {
        None,
        Keyboard,
        Pointer,
    }
}
