@page "/items"
@page "/items/folder/{FolderId:guid}"
@inherits MainBase
@inject ItemService ItemService
@using AliasVault.RazorComponents.Tables
@using AliasVault.RazorComponents.Models
@using AliasVault.Client.Main.Models
@using AliasVault.Client.Main.Components.Items
@using AliasClientDb.Models
@using AliasVault.Client.Main.Components.Folders
@using AliasClientDb
@using Microsoft.Extensions.Localization
@implements IAsyncDisposable

<LayoutPageTitle>Home</LayoutPageTitle>

<PageHeader
    BreadcrumbItems="@BreadcrumbItems"
    Title="@GetFilterTitle()"
    Description="@Localizer["PageDescription"]">
    <TitleActions>
        <ItemFilterDropdown
            InstanceId="home"
            Title="@GetFilterTitle()"
            Count="@TotalFilteredItems"
            ActiveFilter="@FilterType"
            RecentlyDeletedCount="@RecentlyDeletedCount"
            ShowFoldersToggle="@(!IsInFolder)"
            ShowFolders="@ShowFolders"
            OnSelectFilter="SetFilter"
            OnSelectRecentlyDeleted="NavigateToRecentlyDeleted"
            OnToggleShowFolders="ToggleShowFoldersAsync">
            <TitleActions>
                @if (IsInFolder)
                {
                    <button @onclick="ShowEditFolderModal" title="@Localizer["EditFolder"]" class="p-1.5 text-gray-400 hover:text-orange-500 dark:text-gray-500 dark:hover:text-orange-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">
                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
                        </svg>
                    </button>
                    <button @onclick="ShowDeleteFolderModal" title="@Localizer["DeleteFolder"]" class="p-1.5 text-gray-400 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">
                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <polyline points="3 6 5 6 21 6" />
                            <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
                        </svg>
                    </button>
                }
            </TitleActions>
        </ItemFilterDropdown>
    </TitleActions>
    <CustomActions>
        <div class="relative">
            <button @onclick="ToggleSettingsDropdown" id="settingsButton" class="p-2 text-gray-500 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-700">
                <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
                    <path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"></path>
                </svg>
            </button>

            @if (ShowSettingsDropdown)
            {
                <ClickOutsideHandler OnClose="CloseSettingsPopup" ContentId="settingsDropdown,settingsButton">
                    <div id="settingsDropdown" class="absolute right-0 z-10 mt-2 min-w-[220px] origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-700">
                        <div class="p-4">
                            <div class="mb-4">
                                <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["ViewModeLabel"]</label>
                                <select @bind="ViewMode" @bind:after="CloseSettingsPopup" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
                                    <option value="grid">@Localizer["GridViewOption"]</option>
                                    <option value="table">@Localizer["TableViewOption"]</option>
                                </select>
                            </div>
                            <div class="mb-4">
                                <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["SortOrderLabel"]</label>
                                <select @bind="SortOrder" @bind:after="CloseSettingsPopup" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
                                    <option value="@CredentialSortOrder.OldestFirst">@Localizer["OldestFirstOption"]</option>
                                    <option value="@CredentialSortOrder.NewestFirst">@Localizer["NewestFirstOption"]</option>
                                    <option value="@CredentialSortOrder.Alphabetical">@Localizer["AlphabeticalOption"]</option>
                                </select>
                            </div>
                        </div>
                    </div>
                </ClickOutsideHandler>
            }
        </div>
        <RefreshButton OnClick="RefreshVaultAsync" ButtonText="@SharedLocalizer["Refresh"]" />
    </CustomActions>
</PageHeader>

@if (IsLoading)
{
    <LoadingIndicator />
}
else
{
    <PageContent Class="px-4 mb-4">
        @* Active filter indicator - show persistently when a filter is active *@
        @if (FilterType != ItemFilterType.All)
        {
            <div class="flex flex-wrap items-center gap-2 mb-4">
                <span class="text-sm text-gray-500 dark:text-gray-400">@Localizer["FilteringBy"]</span>
                <button @onclick="ClearFilter" class="inline-flex items-center gap-1 px-3 py-1.5 text-sm bg-orange-100 dark:bg-orange-900/30 hover:bg-orange-200 dark:hover:bg-orange-900/50 text-orange-700 dark:text-orange-300 rounded-lg transition-colors">
                    <span>@GetFilterTitle()</span>
                    <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <line x1="18" y1="6" x2="6" y2="18" />
                        <line x1="6" y1="6" x2="18" y2="18" />
                    </svg>
                </button>
            </div>
        }

        @* Folders section - show at root OR inside folders to display subfolders *@
        @if (!IsSearching && ShowFolders)
        {
            <div class="flex flex-wrap items-center gap-2 mb-4">
                @foreach (var folder in CurrentLevelFolders)
                {
                    <FolderPill Folder="@folder" OnClick="() => NavigateToFolder(folder.Id)" />
                }
                @if (CanCreateSubfolder)
                {
                    <button @onclick="ShowCreateFolderModal" class="@GetAddFolderButtonClass()">
                        <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                            <path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
                        </svg>
                        <svg class="w-3.5 h-3.5 -ml-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
                            <line x1="12" y1="5" x2="12" y2="19" />
                            <line x1="5" y1="12" x2="19" y2="12" />
                        </svg>
                        @if (CurrentLevelFolders.Count == 0)
                        {
                            <span>@Localizer["NewFolder"]</span>
                        }
                    </button>
                }
            </div>
        }

        @if (DbService.Settings.CredentialsViewMode == "table")
        {
            <ItemsTable
                Credentials="@FilteredAndSortedItems.ToList()"
                SortColumn="@TableSortColumn"
                CurrentSortDirection="@TableSortDirection"
                OnTableSortChanged="HandleTableSortChanged" />
        }
        else
        {
            <div class="grid gap-4 md:grid-cols-4 xl:grid-cols-6">
                @if (Items.Count == 0 && !IsInFolder)
                {
                    <div class="credential-card col-span-full p-4 space-y-2 bg-amber-50 border border-primary-500 rounded-lg shadow-sm dark:border-primary-700 dark:bg-gray-800">
                        <div class="px-4 py-6 text-gray-700 dark:text-gray-200 rounded text-center flex flex-col items-center">
                            <p class="mb-2 text-lg font-semibold text-primary-700 dark:text-primary-400">@Localizer["NoItemsTitle"]</p>

                            <div class="max-w-md mx-auto">
                                <div class="mb-6">
                                    <p class="text-sm mb-2">@Localizer["CreateFirstItemText"] <span class="hidden md:inline">@Localizer["NewAliasButtonText"]</span><span class="md:hidden">@Localizer["NewAliasButtonTextMobile"]</span> @Localizer["ButtonLocationText"]</p>
                                </div>

                                <div class="flex items-center my-6">
                                    <div class="flex-1 h-px bg-gray-300 dark:bg-gray-600"></div>
                                    <span class="px-4 text-sm text-gray-500 dark:text-gray-400">@Localizer["OrText"]</span>
                                    <div class="flex-1 h-px bg-gray-300 dark:bg-gray-600"></div>
                                </div>

                                <div>
                                    <p class="text-sm mb-2">@Localizer["ImportItemsText"]</p>
                                    <a href="/settings/import-export" class="inline-block text-sm px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors dark:bg-primary-700 dark:hover:bg-primary-600">
                                        @Localizer["ImportButtonText"]
                                    </a>
                                </div>
                            </div>
                        </div>
                    </div>
                }
                else if (!FilteredAndSortedItems.Any())
                {
                    <div class="credential-card col-span-full p-4 space-y-2 bg-amber-50 border border-primary-500 rounded-lg shadow-sm dark:border-primary-700 dark:bg-gray-800">
                        <div class="px-4 py-6 text-gray-700 dark:text-gray-200 rounded text-center">
                            @if (FilterType != ItemFilterType.All)
                            {
                                <p>@Localizer["NoItemsFound"]</p>
                            }
                            else if (IsInFolder)
                            {
                                <p>@Localizer["EmptyFolderMessage"]</p>
                            }
                            else if (HasItemsInFoldersOnly)
                            {
                                <p>@Localizer["AllItemsInFoldersMessage"]</p>
                            }
                            else
                            {
                                <p>@Localizer["NoItemsFound"]</p>
                            }
                        </div>
                    </div>
                }
                @foreach (var item in FilteredAndSortedItems)
                {
                    <ItemCard @key="item.Id" Obj="@item" ShowFolderPath="@IsSearching" />
                }
            </div>
        }

        @* Infinite scroll sentinel - triggers loading more items when visible *@
        @if (HasMoreItems)
        {
            <div @ref="_scrollSentinel" class="flex justify-center py-4 min-h-[40px]">
                @if (IsLoadingMore)
                {
                    <div class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
                        <svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
                            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                            <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                        </svg>
                        <span>@Localizer["LoadingMore"]</span>
                    </div>
                }
            </div>
        }
    </PageContent>
}

@* Create Folder Modal *@
<FolderModal
    IsOpen="@ShowFolderModal"
    OnClose="CloseFolderModal"
    OnSave="CreateFolderAsync"
    Mode="create" />

@* Edit Folder Modal *@
<FolderModal
    IsOpen="@ShowEditFolderModalVisible"
    OnClose="CloseEditFolderModal"
    OnSave="EditFolderAsync"
    InitialName="@CurrentFolderName"
    Mode="edit" />

@* Delete Folder Modal *@
<DeleteFolderModal
    IsOpen="@ShowDeleteFolderModalVisible"
    OnClose="CloseDeleteFolderModal"
    OnDeleteFolderOnly="DeleteFolderOnlyAsync"
    OnDeleteFolderAndContents="DeleteFolderAndContentsAsync"
    FolderName="@CurrentFolderName"
    ItemCount="@CurrentFolderItemCount" />

@code {
    private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Items.Home", "AliasVault.Client");
    private IStringLocalizer ItemTypeSelectorLocalizer => LocalizerFactory.Create("Components.Main.Items.ItemTypeSelector", "AliasVault.Client");

    /// <summary>
    /// Gets or sets the folder ID from the URL.
    /// </summary>
    [Parameter]
    public Guid? FolderId { get; set; }

    /// <summary>
    /// Gets or sets the filter from the URL query string.
    /// Used to preserve the active filter when navigating into a folder so the folder view
    /// matches the count shown on its pill.
    /// </summary>
    [Parameter]
    [SupplyParameterFromQuery(Name = "filter")]
    public string? FilterQuery { get; set; }

    /// <summary>
    /// Gets or sets whether the items are being loaded.
    /// </summary>
    private bool IsLoading { get; set; } = true;

    /// <summary>
    /// Gets or sets the items.
    /// </summary>
    private List<ItemListEntry> Items { get; set; } = new();

    /// <summary>
    /// Gets or sets all folders in the system.
    /// </summary>
    private List<FolderWithCount> Folders { get; set; } = new();

    /// <summary>
    /// Gets all folders as Folder entities (for breadcrumb path computation).
    /// </summary>
    private List<Folder> AllFolders => Folders.Select(f => new Folder
    {
        Id = f.Id,
        Name = f.Name,
        ParentFolderId = f.ParentFolderId,
        Weight = f.Weight,
    }).ToList();

    /// <summary>
    /// Gets the folders to display at the current level (root folders if not in folder, subfolders if in folder).
    /// Item counts are recomputed against the active filter so that folder badges always
    /// reflect the number of items the user will actually see when navigating into the folder.
    /// </summary>
    private List<FolderWithCount> CurrentLevelFolders =>
        Folders
            .Where(f => f.ParentFolderId == FolderId)
            .Select(f => new FolderWithCount
            {
                Id = f.Id,
                Name = f.Name,
                ParentFolderId = f.ParentFolderId,
                Weight = f.Weight,
                ItemCount = GetFilteredItemCountForFolder(f.Id),
            })
            .ToList();

    /// <summary>
    /// Gets whether we can create a subfolder at the current level.
    /// Returns true if at root, or if current folder is not at max depth.
    /// </summary>
    private bool CanCreateSubfolder
    {
        get
        {
            if (!IsInFolder)
            {
                return true; // Can always create folders at root
            }

            if (!FolderId.HasValue || AllFolders.Count == 0)
            {
                return false;
            }

            return FolderTreeUtilities.CanHaveSubfolders(FolderId.Value, AllFolders);
        }
    }

    /// <summary>
    /// Gets or sets the current folder name (when in folder view).
    /// </summary>
    private string CurrentFolderName { get; set; } = string.Empty;

    /// <summary>
    /// Gets the total item count in the current folder tree (includes items in all subfolders recursively).
    /// Used for the delete folder modal to determine if "delete with contents" option should be shown.
    /// </summary>
    private int CurrentFolderItemCount
    {
        get
        {
            if (!FolderId.HasValue)
            {
                return FilteredAndSortedItems.Count();
            }

            // Find the current folder in the Folders list and return its recursive count
            var currentFolder = Folders.FirstOrDefault(f => f.Id == FolderId.Value);
            return currentFolder?.ItemCount ?? 0;
        }
    }

    /// <summary>
    /// Gets or sets whether the settings dropdown is shown.
    /// </summary>
    private bool ShowSettingsDropdown { get; set; }

    /// <summary>
    /// Gets or sets whether the folder modal is shown.
    /// </summary>
    private bool ShowFolderModal { get; set; }

    /// <summary>
    /// Gets or sets whether the delete folder modal is shown.
    /// </summary>
    private bool ShowDeleteFolderModalVisible { get; set; }

    /// <summary>
    /// Gets or sets whether the edit folder modal is shown.
    /// </summary>
    private bool ShowEditFolderModalVisible { get; set; }

    /// <summary>
    /// Gets or sets the count of recently deleted items.
    /// </summary>
    private int RecentlyDeletedCount { get; set; }

    /// <summary>
    /// Gets or sets the search term.
    /// </summary>
    private string SearchTerm { get; set; } = string.Empty;

    /// <summary>
    /// Gets or sets the filter type for the items.
    /// </summary>
    private ItemFilterType FilterType { get; set; } = ItemFilterType.All;

    /// <summary>
    /// LocalStorage key for the show folders preference.
    /// </summary>
    private const string ShowFoldersStorageKey = "items-show-folders";

    /// <summary>
    /// Gets or sets whether to show folders (true) or show all items flat (false).
    /// </summary>
    private bool ShowFolders { get; set; } = true;

    /// <summary>
    /// Gets or sets the number of visible items for infinite scroll.
    /// </summary>
    private int VisibleItemCount { get; set; } = 200;

    /// <summary>
    /// Gets or sets the batch size for loading more items.
    /// </summary>
    private const int BatchSize = 200;

    /// <summary>
    /// Gets or sets whether more items are currently being loaded.
    /// </summary>
    private bool IsLoadingMore { get; set; }

    /// <summary>
    /// Reference for the infinite scroll sentinel element.
    /// </summary>
    private ElementReference _scrollSentinel;

    /// <summary>
    /// Reference to the .NET object for JS interop callbacks.
    /// </summary>
    private DotNetObjectReference<Home>? _dotNetRef;

    /// <summary>
    /// Gets or sets the view mode for the items.
    /// </summary>
    private string ViewMode
    {
        get => DbService.Settings.CredentialsViewMode;
        set => DbService.Settings.SetCredentialsViewMode(value);
    }

    /// <summary>
    /// Gets or sets the sort order for the items.
    /// </summary>
    private CredentialSortOrder SortOrder
    {
        get => DbService.Settings.CredentialsSortOrder;
        set => DbService.Settings.SetCredentialsSortOrder(value);
    }

    /// <summary>
    /// Gets or sets the current table sort column (for table view column header sorting).
    /// </summary>
    private string TableSortColumn { get; set; } = "CreatedAt";

    /// <summary>
    /// Gets or sets the current table sort direction (for table view column header sorting).
    /// </summary>
    private SortDirection TableSortDirection { get; set; } = SortDirection.Ascending;

    /// <summary>
    /// Gets whether we're currently in a folder view.
    /// </summary>
    private bool IsInFolder => FolderId.HasValue;

    /// <summary>
    /// Gets whether we're currently searching.
    /// </summary>
    private bool IsSearching => !string.IsNullOrEmpty(SearchTerm);

    /// <summary>
    /// Gets whether all items are in folders (no items at root level but items exist in folders).
    /// </summary>
    private bool HasItemsInFoldersOnly => Items.Count > 0 && Items.All(x => x.FolderId != null);

    /// <summary>
    /// Gets whether there are items at the root level (not in any folder).
    /// </summary>
    private bool HasRootItems => Items.Any(x => x.FolderId == null);

    /// <summary>
    /// Gets the items filtered and sorted according to the current filter and sort order (all items, not paginated).
    /// </summary>
    private IEnumerable<ItemListEntry> AllFilteredAndSortedItems
    {
        get
        {
            // First filter by folder
            var filtered = Items.AsEnumerable();

            if (IsInFolder)
            {
                // Show only items in this folder
                filtered = filtered.Where(x => x.FolderId == FolderId);
            }
            else if (!IsSearching && ShowFolders)
            {
                // When showing folders (checkbox ON): only show root items (exclude items in folders)
                // When not showing folders (checkbox OFF): show all items flat
                filtered = filtered.Where(x => x.FolderId == null);
            }

            // Then apply type/feature filter
            filtered = ApplyTypeFilter(filtered);

            // Apply sort - use table column sort if in table view, otherwise use settings dropdown sort
            if (ViewMode == "table")
            {
                return ApplyTableSort(filtered);
            }

            return SortOrder switch
            {
                CredentialSortOrder.NewestFirst => filtered.OrderByDescending(x => x.CreatedAt),
                CredentialSortOrder.Alphabetical => filtered.OrderBy(x => x.Service ?? string.Empty),
                _ => filtered.OrderBy(x => x.CreatedAt), // OldestFirst (default)
            };
        }
    }

    /// <summary>
    /// Applies the active type/feature filter to a sequence of items.
    /// </summary>
    /// <param name="items">The items to filter.</param>
    /// <returns>The filtered items.</returns>
    private IEnumerable<ItemListEntry> ApplyTypeFilter(IEnumerable<ItemListEntry> items)
    {
        return FilterType switch
        {
            ItemFilterType.Passkeys => items.Where(x => x.HasPasskey),
            ItemFilterType.Attachments => items.Where(x => x.HasAttachment),
            ItemFilterType.Totp => items.Where(x => x.HasTotp),
            ItemFilterType.Login => items.Where(x => x.ItemType == ItemType.Login),
            ItemFilterType.Alias => items.Where(x => x.ItemType == ItemType.Alias),
            ItemFilterType.CreditCard => items.Where(x => x.ItemType == ItemType.CreditCard),
            ItemFilterType.Note => items.Where(x => x.ItemType == ItemType.Note),
            _ => items, // All
        };
    }

    /// <summary>
    /// Counts the items in a folder (and all of its descendants) that pass the active type/feature filter.
    /// </summary>
    /// <param name="folderId">The folder ID.</param>
    /// <returns>The filtered, recursive item count.</returns>
    private int GetFilteredItemCountForFolder(Guid folderId)
    {
        var descendantIds = FolderTreeUtilities.GetDescendantFolderIds(folderId, AllFolders);
        var folderIds = new HashSet<Guid>(descendantIds) { folderId };

        return ApplyTypeFilter(Items)
            .Count(item => item.FolderId.HasValue && folderIds.Contains(item.FolderId.Value));
    }

    /// <summary>
    /// Applies table column sorting to the filtered items.
    /// </summary>
    /// <param name="items">The items to sort.</param>
    /// <returns>The sorted items.</returns>
    private IEnumerable<ItemListEntry> ApplyTableSort(IEnumerable<ItemListEntry> items)
    {
        return TableSortColumn switch
        {
            "Service" => TableSortDirection == SortDirection.Ascending
                ? items.OrderBy(x => x.Service ?? string.Empty)
                : items.OrderByDescending(x => x.Service ?? string.Empty),
            "Username" => TableSortDirection == SortDirection.Ascending
                ? items.OrderBy(x => x.Username ?? string.Empty)
                : items.OrderByDescending(x => x.Username ?? string.Empty),
            "Email" => TableSortDirection == SortDirection.Ascending
                ? items.OrderBy(x => x.Email ?? string.Empty)
                : items.OrderByDescending(x => x.Email ?? string.Empty),
            "CreatedAt" => TableSortDirection == SortDirection.Ascending
                ? items.OrderBy(x => x.CreatedAt)
                : items.OrderByDescending(x => x.CreatedAt),
            _ => items.OrderBy(x => x.CreatedAt), // Default
        };
    }

    /// <summary>
    /// Gets the total number of filtered items.
    /// </summary>
    private int TotalFilteredItems => AllFilteredAndSortedItems.Count();

    /// <summary>
    /// Gets whether there are more items to load.
    /// </summary>
    private bool HasMoreItems => VisibleItemCount < TotalFilteredItems;

    /// <summary>
    /// Gets the visible items (limited by VisibleItemCount for infinite scroll).
    /// </summary>
    private IEnumerable<ItemListEntry> FilteredAndSortedItems =>
        AllFilteredAndSortedItems.Take(VisibleItemCount);

    /// <summary>
    /// Gets the title based on the active filter and folder.
    /// An active filter takes precedence over the folder name so the title (and the
    /// "Filtering by" pill) reflects what's actually being filtered — matching the
    /// filter shown in the dropdown and the count in the header.
    /// </summary>
    private string GetFilterTitle()
    {
        if (FilterType != ItemFilterType.All)
        {
            return FilterType switch
            {
                ItemFilterType.Passkeys => Localizer["FilterPasskeysOption"],
                ItemFilterType.Attachments => Localizer["FilterAttachmentsOption"],
                ItemFilterType.Totp => Localizer["FilterTotpOption"],
                ItemFilterType.Login => ItemTypeSelectorLocalizer["TypeLogin"],
                ItemFilterType.Alias => ItemTypeSelectorLocalizer["TypeAlias"],
                ItemFilterType.CreditCard => ItemTypeSelectorLocalizer["TypeCreditCard"],
                ItemFilterType.Note => ItemTypeSelectorLocalizer["TypeNote"],
                _ => Localizer["PageTitle"],
            };
        }

        if (IsInFolder && !string.IsNullOrEmpty(CurrentFolderName))
        {
            return CurrentFolderName;
        }

        return Localizer["PageTitle"];
    }

    /// <summary>
    /// Gets the CSS class for the add folder button.
    /// </summary>
    private string GetAddFolderButtonClass()
    {
        if (Folders.Count > 0)
        {
            return "inline-flex items-center gap-1.5 px-5 py-2.5 text-sm rounded-lg transition-colors focus:outline-none text-gray-500 dark:text-gray-400 hover:text-orange-600 dark:hover:text-orange-400 hover:bg-gray-100 dark:hover:bg-gray-700/50";
        }

        return "inline-flex items-center gap-1.5 px-5 py-2.5 text-sm rounded-lg transition-colors focus:outline-none text-gray-400 dark:text-gray-500 border border-dashed border-gray-300 dark:border-gray-600 hover:border-orange-400 dark:hover:border-orange-500 hover:text-orange-600 dark:hover:text-orange-400";
    }

    /// <summary>
    /// Toggles the settings dropdown.
    /// </summary>
    private void ToggleSettingsDropdown()
    {
        ShowSettingsDropdown = !ShowSettingsDropdown;
        StateHasChanged();
    }

    /// <summary>
    /// Sets the filter type. The filter is reflected in the URL query string so it
    /// persists across folder navigation and browser back/forward.
    /// </summary>
    private void SetFilter(ItemFilterType filterType)
    {
        FilterType = filterType;
        VisibleItemCount = BatchSize; // Reset visible items when filter changes
        NavigationManager.NavigateTo(BuildItemsUrl(FolderId, filterType), replace: true);
    }

    /// <summary>
    /// Clears the active filter and resets to showing all items.
    /// </summary>
    private void ClearFilter()
    {
        FilterType = ItemFilterType.All;
        VisibleItemCount = BatchSize; // Reset visible items when filter changes
        NavigationManager.NavigateTo(BuildItemsUrl(FolderId, ItemFilterType.All), replace: true);
    }

    /// <summary>
    /// Builds the items list URL for a given folder and filter. The filter is always
    /// included in the query string so the active filter persists across navigation,
    /// including when navigating between folder views.
    /// </summary>
    /// <param name="folderId">The folder ID, or null for the root view.</param>
    /// <param name="filterType">The active filter type.</param>
    /// <returns>The URL to navigate to.</returns>
    private static string BuildItemsUrl(Guid? folderId, ItemFilterType filterType)
    {
        var basePath = folderId.HasValue ? $"/items/folder/{folderId.Value}" : "/items";
        return $"{basePath}?filter={filterType}";
    }

    /// <summary>
    /// Toggles the show folders setting.
    /// </summary>
    private async Task ToggleShowFoldersAsync()
    {
        ShowFolders = !ShowFolders;
        VisibleItemCount = BatchSize; // Reset visible items when setting changes
        await LocalStorage.SetItemAsync(ShowFoldersStorageKey, ShowFolders);
        StateHasChanged();
    }

    /// <summary>
    /// Handles the table column sort changed event.
    /// This applies sorting to ALL items before pagination.
    /// </summary>
    /// <param name="sort">The sort column and direction.</param>
    private void HandleTableSortChanged((string Column, SortDirection Direction) sort)
    {
        TableSortColumn = sort.Column;
        TableSortDirection = sort.Direction;
        VisibleItemCount = BatchSize; // Reset visible items when sort changes to show first N sorted items
        StateHasChanged();
    }

    /// <summary>
    /// Loads more items for infinite scroll. Called from JavaScript via IntersectionObserver.
    /// </summary>
    [JSInvokable]
    public async Task LoadMoreItems()
    {
        if (HasMoreItems && !IsLoadingMore)
        {
            IsLoadingMore = true;
            StateHasChanged();

            // Brief delay to show loading indicator
            await Task.Delay(300);

            VisibleItemCount += BatchSize;
            IsLoadingMore = false;
            StateHasChanged();
        }
    }

    /// <summary>
    /// Closes the settings dropdown.
    /// </summary>
    private async Task CloseSettingsPopup()
    {
        ShowSettingsDropdown = false;

        // Sync table sort state with the dropdown sort order
        SyncTableSortWithSortOrder();

        // Reload the items in case the settings were changed.
        await LoadItemsAsync();
    }

    /// <summary>
    /// Synchronizes the table sort column and direction with the settings dropdown sort order.
    /// Called when the user changes the sort order via the settings dropdown.
    /// </summary>
    private void SyncTableSortWithSortOrder()
    {
        (TableSortColumn, TableSortDirection) = SortOrder switch
        {
            CredentialSortOrder.NewestFirst => ("CreatedAt", SortDirection.Descending),
            CredentialSortOrder.Alphabetical => ("Service", SortDirection.Ascending),
            _ => ("CreatedAt", SortDirection.Ascending), // OldestFirst (default)
        };
    }

    /// <summary>
    /// Navigate to a folder, preserving the active filter so the folder view matches
    /// the count shown on the folder pill.
    /// </summary>
    private void NavigateToFolder(Guid folderId)
    {
        VisibleItemCount = BatchSize; // Reset visible items when navigating to folder
        NavigationManager.NavigateTo(BuildItemsUrl(folderId, FilterType));
    }

    /// <summary>
    /// Navigate back to root, preserving the active filter.
    /// </summary>
    private void NavigateToRoot()
    {
        VisibleItemCount = BatchSize; // Reset visible items when navigating to root
        NavigationManager.NavigateTo(BuildItemsUrl(null, FilterType));
    }

    /// <summary>
    /// Navigate to the recently deleted page.
    /// </summary>
    private void NavigateToRecentlyDeleted()
    {
        NavigationManager.NavigateTo("/items/recently-deleted");
    }

    /// <summary>
    /// Show the create folder modal.
    /// </summary>
    private void ShowCreateFolderModal()
    {
        ShowFolderModal = true;
        StateHasChanged();
    }

    /// <summary>
    /// Close the folder modal.
    /// </summary>
    private void CloseFolderModal()
    {
        ShowFolderModal = false;
        StateHasChanged();
    }

    /// <summary>
    /// Create a new folder. If inside a folder, creates as subfolder.
    /// </summary>
    private async Task CreateFolderAsync(string folderName)
    {
        // Use current folder as parent if we're inside a folder, otherwise null for root
        var parentFolderId = IsInFolder ? FolderId : null;
        var folderId = await FolderService.CreateAsync(folderName, parentFolderId);
        if (folderId != Guid.Empty)
        {
            // Reload folders
            Folders = await FolderService.GetAllWithCountsAsync();
            StateHasChanged();
        }
        else
        {
            GlobalNotificationService.AddErrorMessage(Localizer["FailedToCreateFolder"], true);
        }
    }

    /// <summary>
    /// Show the edit folder modal.
    /// </summary>
    private void ShowEditFolderModal()
    {
        ShowEditFolderModalVisible = true;
        StateHasChanged();
    }

    /// <summary>
    /// Close the edit folder modal.
    /// </summary>
    private void CloseEditFolderModal()
    {
        ShowEditFolderModalVisible = false;
        StateHasChanged();
    }

    /// <summary>
    /// Edit the current folder's name.
    /// </summary>
    private async Task EditFolderAsync(string newName)
    {
        if (FolderId.HasValue)
        {
            var success = await FolderService.UpdateAsync(FolderId.Value, newName);
            if (success)
            {
                // Update the current folder name to reflect the change
                CurrentFolderName = newName;

                // Reload folders to update the name in the list
                Folders = await FolderService.GetAllWithCountsAsync();
                StateHasChanged();
            }
            else
            {
                GlobalNotificationService.AddErrorMessage(SharedLocalizer["ErrorGeneric"], true);
            }
        }
    }

    /// <summary>
    /// Show the delete folder modal.
    /// </summary>
    private void ShowDeleteFolderModal()
    {
        ShowDeleteFolderModalVisible = true;
        StateHasChanged();
    }

    /// <summary>
    /// Close the delete folder modal.
    /// </summary>
    private void CloseDeleteFolderModal()
    {
        ShowDeleteFolderModalVisible = false;
        StateHasChanged();
    }

    /// <summary>
    /// Delete the current folder only (move items to parent folder).
    /// </summary>
    private async Task DeleteFolderOnlyAsync()
    {
        if (FolderId.HasValue)
        {
            // Get the parent folder ID before deletion
            var folder = await FolderService.GetByIdAsync(FolderId.Value);
            var parentFolderId = folder?.ParentFolderId;

            var success = await FolderService.DeleteAsync(FolderId.Value);
            if (success)
            {
                // Navigate to parent folder if it exists, otherwise root
                NavigationManager.NavigateTo(BuildItemsUrl(parentFolderId, FilterType));
            }
            else
            {
                GlobalNotificationService.AddErrorMessage(Localizer["FailedToDeleteFolder"], true);
            }
        }
    }

    /// <summary>
    /// Delete the current folder and all its contents.
    /// </summary>
    private async Task DeleteFolderAndContentsAsync()
    {
        if (FolderId.HasValue)
        {
            // Get the parent folder ID before deletion
            var folder = await FolderService.GetByIdAsync(FolderId.Value);
            var parentFolderId = folder?.ParentFolderId;

            var success = await FolderService.DeleteWithContentsAsync(FolderId.Value);
            if (success)
            {
                // Navigate to parent folder if it exists, otherwise root
                NavigationManager.NavigateTo(BuildItemsUrl(parentFolderId, FilterType));
            }
            else
            {
                GlobalNotificationService.AddErrorMessage(Localizer["FailedToDeleteFolder"], true);
            }
        }
    }

    /// <inheritdoc />
    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();

        // Reload the item list after a full vault reload from the server so changes made on other
        // devices appear without a hard page refresh.
        DbService.GetState().StateChanged += OnVaultStateChanged;
    }

    /// <summary>
    /// Tracks the previous database status so we can detect a completed vault reload (Loading -> Ready).
    /// </summary>
    private DbServiceState.DatabaseStatus _lastVaultStatus = DbServiceState.DatabaseStatus.Uninitialized;

    /// <summary>
    /// Reloads the item list when the vault finishes reloading from the server. The vault reload swaps in
    /// a fresh DbContext, which auto-invalidates the ItemService list cache, so the re-query picks up any
    /// newly synced data.
    /// </summary>
    private async void OnVaultStateChanged(object? sender, DbServiceState.DatabaseState newState)
    {
        if (newState.Status == DbServiceState.DatabaseStatus.Ready && _lastVaultStatus == DbServiceState.DatabaseStatus.Loading)
        {
            await InvokeAsync(LoadItemsAsync);
        }

        _lastVaultStatus = newState.Status;
    }

    /// <summary>
    /// Refreshes the vault by re-fetching it from the server, mirroring the header vault-refresh button.
    /// </summary>
    private async Task RefreshVaultAsync()
    {
        await DbService.InitializeDatabaseAsync();
    }

    /// <inheritdoc />
    protected override async Task OnParametersSetAsync()
    {
        await base.OnParametersSetAsync();

        // Sync the in-memory filter with the URL query so that browser back/forward and
        // folder navigation preserve the active filter. Unknown values fall back to All.
        FilterType = Enum.TryParse<ItemFilterType>(FilterQuery, ignoreCase: true, out var parsed)
            ? parsed
            : ItemFilterType.All;

        // Initialize table sort state from saved sort order
        SyncTableSortWithSortOrder();

        // Load show folders preference from local storage
        var storedShowFolders = await LocalStorage.GetItemAsync<bool?>(ShowFoldersStorageKey);
        ShowFolders = storedShowFolders ?? true;

        // Load folder name and build breadcrumbs if in folder view
        if (FolderId.HasValue)
        {
            var folder = await FolderService.GetByIdAsync(FolderId.Value);
            CurrentFolderName = folder?.Name ?? string.Empty;

            // Build breadcrumbs for folder hierarchy using shared helper
            // Make last folder non-clickable since we're on that folder page
            BreadcrumbItems.RemoveAll(b => !b.ShowHomeIcon);
            await BuildFolderBreadcrumbsAsync(FolderId.Value, makeLastClickable: false);
        }
        else
        {
            CurrentFolderName = string.Empty;
            // Clear breadcrumbs at root level (only show Home from base class)
            BreadcrumbItems.RemoveAll(b => !b.ShowHomeIcon);
        }

        await LoadItemsAsync();
    }

    /// <summary>
    /// Tracks whether the IntersectionObserver is currently set up.
    /// </summary>
    private bool _isObserverSetup;

    /// <inheritdoc />
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);

        if (firstRender)
        {
            // Add JS interop reference for the infinite-scroll observer.
            _dotNetRef = DotNetObjectReference.Create(this);
        }

        // Set up or re-attach the IntersectionObserver after every render if there are more items.
        // The sentinel element is only in the DOM when HasMoreItems is true, and it gets
        // re-created on each render, so we need to re-attach the observer.
        if (HasMoreItems && !IsLoading && !IsLoadingMore && _dotNetRef != null)
        {
            // Always tear down and re-setup to handle the new sentinel element reference
            if (_isObserverSetup)
            {
                await JsInteropService.TeardownInfiniteScroll(_scrollSentinel);
            }

            await JsInteropService.SetupInfiniteScroll(_scrollSentinel, _dotNetRef);
            _isObserverSetup = true;
        }
        else if (!HasMoreItems && _isObserverSetup)
        {
            // No more items - clean up the observer
            await JsInteropService.TeardownInfiniteScroll(_scrollSentinel);
            _isObserverSetup = false;
        }
    }

    /// <inheritdoc />
    public async ValueTask DisposeAsync()
    {
        DbService.GetState().StateChanged -= OnVaultStateChanged;

        if (_dotNetRef != null)
        {
            if (_isObserverSetup)
            {
                await JsInteropService.TeardownInfiniteScroll(_scrollSentinel);
                _isObserverSetup = false;
            }

            _dotNetRef.Dispose();
            _dotNetRef = null;
        }
    }

    /// <summary>
    /// Loads and/or refreshes the items.
    /// </summary>
    private async Task LoadItemsAsync()
    {
        IsLoading = true;
        VisibleItemCount = BatchSize; // Reset visible items when loading
        StateHasChanged();

        // Load the items from the database via ItemService.
        var itemListEntries = await ItemService.GetListAsync();
        if (itemListEntries is null)
        {
            // Error loading items.
            GlobalNotificationService.AddErrorMessage(Localizer["FailedToLoadItemsMessage"], true);
            return;
        }

        if (itemListEntries.Count == 0 && !DbService.Settings.TutorialDone && !IsInFolder)
        {
            // Redirect to the welcome page.
            NavigationManager.NavigateTo("/welcome");
            return;
        }

        // Load folders
        Folders = await FolderService.GetAllWithCountsAsync();

        // Load recently deleted count
        var recentlyDeleted = await ItemService.GetRecentlyDeletedAsync();
        RecentlyDeletedCount = recentlyDeleted.Count;

        // Pass unsorted list to the view - sorting will be handled by the property
        Items = itemListEntries;
        IsLoading = false;
        StateHasChanged();
    }
}
