using System.Buffers.Binary;
using System.IO.MemoryMappedFiles;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using ZB.MOM.WW.CBDD.Core.Indexing;
using ZB.MOM.WW.CBDD.Core.Indexing.Internal;
using ZB.MOM.WW.CBDD.Core.Transactions;

namespace ZB.MOM.WW.CBDD.Core.Storage;

/// <summary>
///     Options that control compaction/vacuum behavior.
/// </summary>
public sealed class CompactionOptions
{
    /// <summary>
    ///     Gets or sets a value indicating whether to run a bounded online pass instead of full offline compaction.
    /// </summary>
    public bool OnlineMode { get; init; }

    /// <summary>
    ///     Gets or sets a value indicating whether slotted-page defragmentation should be performed.
    /// </summary>
    public bool DefragmentSlottedPages { get; init; } = true;

    /// <summary>
    ///     Gets or sets a value indicating whether free-list normalization should be performed.
    /// </summary>
    public bool NormalizeFreeList { get; init; } = true;

    /// <summary>
    ///     Gets or sets a value indicating whether reclaimable tail pages should be truncated.
    /// </summary>
    public bool EnableTailTruncation { get; init; } = true;

    /// <summary>
    ///     Gets or sets the minimum page count that must remain after truncation.
    /// </summary>
    public uint MinimumRetainedPages { get; init; } = 2;

    /// <summary>
    ///     Gets or sets the maximum number of pages processed per online batch.
    /// </summary>
    public int OnlineBatchPageLimit { get; init; } = 16;

    /// <summary>
    ///     Gets or sets the sleep delay inserted between online batches.
    /// </summary>
    public TimeSpan OnlineBatchDelay { get; init; } = TimeSpan.FromMilliseconds(20);

    /// <summary>
    ///     Gets or sets the maximum duration of a single online pass.
    /// </summary>
    public TimeSpan MaxOnlineDuration { get; init; } = TimeSpan.FromSeconds(2);

    /// <summary>
    ///     Normalizes compaction options to safe runtime defaults.
    /// </summary>
    /// <param name="options">Optional compaction options.</param>
    internal static CompactionOptions Normalize(CompactionOptions? options)
    {
        var normalized = options ?? new CompactionOptions();

        uint minimumPages = normalized.MinimumRetainedPages < 2 ? 2u : normalized.MinimumRetainedPages;
        int pageLimit = normalized.OnlineBatchPageLimit <= 0 ? 1 : normalized.OnlineBatchPageLimit;
        var batchDelay = normalized.OnlineBatchDelay < TimeSpan.Zero ? TimeSpan.Zero : normalized.OnlineBatchDelay;
        var maxDuration = normalized.MaxOnlineDuration <= TimeSpan.Zero
            ? TimeSpan.FromMilliseconds(250)
            : normalized.MaxOnlineDuration;

        return new CompactionOptions
        {
            OnlineMode = normalized.OnlineMode,
            DefragmentSlottedPages = normalized.DefragmentSlottedPages,
            NormalizeFreeList = normalized.NormalizeFreeList,
            EnableTailTruncation = normalized.EnableTailTruncation,
            MinimumRetainedPages = minimumPages,
            OnlineBatchPageLimit = pageLimit,
            OnlineBatchDelay = batchDelay,
            MaxOnlineDuration = maxDuration
        };
    }
}

/// <summary>
///     Scheduling and trigger options for maintenance orchestration.
/// </summary>
public sealed class MaintenanceOptions
{
    /// <summary>
    ///     Gets or sets a value indicating whether a maintenance pass should be attempted at startup.
    /// </summary>
    public bool RunAtStartup { get; init; }

    /// <summary>
    ///     Gets or sets the minimum fragmentation percentage required before maintenance should run.
    /// </summary>
    public double MinFragmentationPercent { get; init; } = 15.0;

    /// <summary>
    ///     Gets or sets the minimum reclaimable bytes threshold.
    /// </summary>
    public long MinReclaimableBytes { get; init; } = 64 * 1024;

    /// <summary>
    ///     Gets or sets the maximum runtime for a maintenance pass.
    /// </summary>
    public TimeSpan MaxRunDuration { get; init; } = TimeSpan.FromSeconds(5);

    /// <summary>
    ///     Gets or sets the online throttle value in pages per batch.
    /// </summary>
    public int OnlineThrottlePagesPerBatch { get; init; } = 16;

    /// <summary>
    ///     Gets or sets the delay inserted between online batches.
    /// </summary>
    public TimeSpan OnlineThrottleDelay { get; init; } = TimeSpan.FromMilliseconds(20);
}

/// <summary>
///     Space accounting and execution stats produced by compaction/vacuum operations.
/// </summary>
public sealed class CompactionStats
{
    /// <summary>
    ///     Gets or sets the operation start timestamp in UTC.
    /// </summary>
    public DateTimeOffset StartedAtUtc { get; init; }

    /// <summary>
    ///     Gets or sets the operation completion timestamp in UTC.
    /// </summary>
    public DateTimeOffset CompletedAtUtc { get; init; }

    /// <summary>
    ///     Gets or sets a value indicating whether this run used online mode.
    /// </summary>
    public bool OnlineMode { get; init; }

    /// <summary>
    ///     Gets or sets a value indicating whether this run resumed from an existing compaction marker.
    /// </summary>
    public bool ResumedFromMarker { get; init; }

    /// <summary>
    ///     Gets or sets the pre-compaction file size in bytes.
    /// </summary>
    public long PreFileSizeBytes { get; init; }

    /// <summary>
    ///     Gets or sets the post-compaction file size in bytes.
    /// </summary>
    public long PostFileSizeBytes { get; init; }

    /// <summary>
    ///     Gets or sets the pre-compaction page count.
    /// </summary>
    public uint PrePageCount { get; init; }

    /// <summary>
    ///     Gets or sets the post-compaction page count.
    /// </summary>
    public uint PostPageCount { get; init; }

    /// <summary>
    ///     Gets or sets the pre-compaction free page count.
    /// </summary>
    public int PreFreePages { get; init; }

    /// <summary>
    ///     Gets or sets the post-compaction free page count.
    /// </summary>
    public int PostFreePages { get; init; }

    /// <summary>
    ///     Gets or sets the pre-compaction estimated free bytes.
    /// </summary>
    public long PreFreeBytes { get; init; }

    /// <summary>
    ///     Gets or sets the post-compaction estimated free bytes.
    /// </summary>
    public long PostFreeBytes { get; init; }

    /// <summary>
    ///     Gets or sets the pre-compaction estimated live bytes.
    /// </summary>
    public long PreLiveBytes { get; init; }

    /// <summary>
    ///     Gets or sets the post-compaction estimated live bytes.
    /// </summary>
    public long PostLiveBytes { get; init; }

    /// <summary>
    ///     Gets or sets the pre-compaction fragmentation percentage.
    /// </summary>
    public double PreFragmentationPercent { get; init; }

    /// <summary>
    ///     Gets or sets the post-compaction fragmentation percentage.
    /// </summary>
    public double PostFragmentationPercent { get; init; }

    /// <summary>
    ///     Gets or sets the number of scanned pages.
    /// </summary>
    public int PagesScanned { get; init; }

    /// <summary>
    ///     Gets or sets the number of defragmented pages.
    /// </summary>
    public int PagesDefragmented { get; init; }

    /// <summary>
    ///     Gets or sets bytes reclaimed by slot compaction inside slotted pages.
    /// </summary>
    public long BytesReclaimedByDefragmentation { get; init; }

    /// <summary>
    ///     Gets or sets the normalized free-list page count.
    /// </summary>
    public int FreeListPagesNormalized { get; init; }

    /// <summary>
    ///     Gets or sets the number of pages truncated from EOF.
    /// </summary>
    public uint TailPagesTruncated { get; init; }

    /// <summary>
    ///     Gets or sets the number of bytes truncated from EOF.
    /// </summary>
    public long TailBytesTruncated { get; init; }

    /// <summary>
    ///     Gets or sets the count of documents relocated during compaction.
    /// </summary>
    public long DocumentsRelocated { get; init; }

    /// <summary>
    ///     Gets or sets the count of pages where at least one document was relocated.
    /// </summary>
    public int PagesRelocated { get; init; }

    /// <summary>
    ///     Gets reclaimed file bytes (`PreFileSizeBytes - PostFileSizeBytes`).
    /// </summary>
    public long ReclaimedFileBytes => PreFileSizeBytes - PostFileSizeBytes;

    /// <summary>
    ///     Gets reclaimed pages (`PrePageCount - PostPageCount`).
    /// </summary>
    public long ReclaimedPages => (long)PrePageCount - PostPageCount;

    /// <summary>
    ///     Gets total runtime.
    /// </summary>
    public TimeSpan Duration => CompletedAtUtc - StartedAtUtc;

    /// <summary>
    ///     Gets average processed bytes per second using pre-compaction file size and elapsed runtime.
    /// </summary>
    public double ThroughputBytesPerSecond => Duration.TotalSeconds <= 0
        ? 0
        : PreFileSizeBytes / Duration.TotalSeconds;

    /// <summary>
    ///     Gets average scanned pages per second during compaction.
    /// </summary>
    public double ThroughputPagesPerSecond => Duration.TotalSeconds <= 0
        ? 0
        : PagesScanned / Duration.TotalSeconds;

    /// <summary>
    ///     Gets average relocated documents per second during compaction.
    /// </summary>
    public double ThroughputDocumentsPerSecond => Duration.TotalSeconds <= 0
        ? 0
        : DocumentsRelocated / Duration.TotalSeconds;
}

public sealed partial class StorageEngine
{
    private static readonly JsonSerializerOptions CompactionMarkerSerializerOptions = new()
    {
        WriteIndented = false,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        Converters = { new JsonStringEnumConverter() }
    };

    private readonly SemaphoreSlim _maintenanceGate = new(1, 1);
    private uint _onlineCompactionCursor = 2;

    /// <summary>
    ///     Gets maintenance scheduling options configured for this engine instance.
    /// </summary>
    public MaintenanceOptions MaintenanceOptions { get; }

    /// <summary>
    ///     Runs compaction in offline mode by default. Set <see cref="CompactionOptions.OnlineMode" /> to run a bounded online
    ///     pass.
    /// </summary>
    /// <param name="options">Optional compaction options.</param>
    public CompactionStats Compact(CompactionOptions? options = null)
    {
        return CompactAsync(options).GetAwaiter().GetResult();
    }

    /// <summary>
    ///     Runs compaction in offline mode by default. Set <see cref="CompactionOptions.OnlineMode" /> to run a bounded online
    ///     pass.
    /// </summary>
    /// <param name="options">Optional compaction options.</param>
    /// <param name="ct">Cancellation token.</param>
    public async Task<CompactionStats> CompactAsync(CompactionOptions? options = null, CancellationToken ct = default)
    {
        var normalizedOptions = CompactionOptions.Normalize(options);
        if (normalizedOptions.OnlineMode) return await RunOnlineCompactionPassAsync(normalizedOptions, ct);

        return await RunOfflineCompactionAsync(normalizedOptions, false, ct);
    }

    /// <summary>
    ///     Alias for <see cref="Compact(CompactionOptions?)" />.
    /// </summary>
    /// <param name="options">Optional compaction options.</param>
    public CompactionStats Vacuum(CompactionOptions? options = null)
    {
        return Compact(options);
    }

    /// <summary>
    ///     Async alias for <see cref="CompactAsync(CompactionOptions?, CancellationToken)" />.
    /// </summary>
    /// <param name="options">Optional compaction options.</param>
    /// <param name="ct">Cancellation token.</param>
    public Task<CompactionStats> VacuumAsync(CompactionOptions? options = null, CancellationToken ct = default)
    {
        return CompactAsync(options, ct);
    }

    /// <summary>
    ///     Runs a bounded, throttled online compaction pass.
    /// </summary>
    /// <param name="options">Optional online compaction options.</param>
    public CompactionStats RunOnlineCompactionPass(CompactionOptions? options = null)
    {
        var normalized = CompactionOptions.Normalize(options);
        if (!normalized.OnlineMode)
            normalized = new CompactionOptions
            {
                OnlineMode = true,
                DefragmentSlottedPages = normalized.DefragmentSlottedPages,
                NormalizeFreeList = normalized.NormalizeFreeList,
                EnableTailTruncation = normalized.EnableTailTruncation,
                MinimumRetainedPages = normalized.MinimumRetainedPages,
                OnlineBatchPageLimit = normalized.OnlineBatchPageLimit,
                OnlineBatchDelay = normalized.OnlineBatchDelay,
                MaxOnlineDuration = normalized.MaxOnlineDuration
            };

        return RunOnlineCompactionPassAsync(normalized).GetAwaiter().GetResult();
    }

    /// <summary>
    ///     Runs a bounded, throttled online compaction pass.
    /// </summary>
    /// <param name="options">Optional online compaction options.</param>
    /// <param name="ct">Cancellation token.</param>
    public async Task<CompactionStats> RunOnlineCompactionPassAsync(CompactionOptions? options = null,
        CancellationToken ct = default)
    {
        var normalizedOptions = CompactionOptions.Normalize(options);
        if (!normalizedOptions.OnlineMode)
            normalizedOptions = new CompactionOptions
            {
                OnlineMode = true,
                DefragmentSlottedPages = normalizedOptions.DefragmentSlottedPages,
                NormalizeFreeList = normalizedOptions.NormalizeFreeList,
                EnableTailTruncation = normalizedOptions.EnableTailTruncation,
                MinimumRetainedPages = normalizedOptions.MinimumRetainedPages,
                OnlineBatchPageLimit = normalizedOptions.OnlineBatchPageLimit,
                OnlineBatchDelay = normalizedOptions.OnlineBatchDelay,
                MaxOnlineDuration = normalizedOptions.MaxOnlineDuration
            };

        return await ExecuteOnlinePassAsync(normalizedOptions, ct);
    }

    /// <summary>
    ///     If a compaction marker exists, resumes or finalizes the in-progress compaction idempotently.
    /// </summary>
    /// <param name="options">Optional compaction options.</param>
    public CompactionStats? ResumeCompactionIfNeeded(CompactionOptions? options = null)
    {
        return ResumeCompactionIfNeededAsync(options).GetAwaiter().GetResult();
    }

    /// <summary>
    ///     If a compaction marker exists, resumes or finalizes the in-progress compaction idempotently.
    /// </summary>
    /// <param name="options">Optional compaction options.</param>
    /// <param name="ct">Cancellation token.</param>
    public async Task<CompactionStats?> ResumeCompactionIfNeededAsync(CompactionOptions? options = null,
        CancellationToken ct = default)
    {
        var normalizedOptions = CompactionOptions.Normalize(options);
        if (normalizedOptions.OnlineMode)
            normalizedOptions = new CompactionOptions
            {
                OnlineMode = false,
                DefragmentSlottedPages = normalizedOptions.DefragmentSlottedPages,
                NormalizeFreeList = normalizedOptions.NormalizeFreeList,
                EnableTailTruncation = normalizedOptions.EnableTailTruncation,
                MinimumRetainedPages = normalizedOptions.MinimumRetainedPages,
                OnlineBatchPageLimit = normalizedOptions.OnlineBatchPageLimit,
                OnlineBatchDelay = normalizedOptions.OnlineBatchDelay,
                MaxOnlineDuration = normalizedOptions.MaxOnlineDuration
            };

        string markerPath = GetCompactionMarkerPath();
        if (!File.Exists(markerPath))
            return null;

        return await RunOfflineCompactionAsync(normalizedOptions, true, ct);
    }

    private async Task<CompactionStats> RunOfflineCompactionAsync(CompactionOptions options, bool resumeOnly,
        CancellationToken ct)
    {
        await _maintenanceGate.WaitAsync(ct);
        try
        {
            await _commitLock.WaitAsync(ct);
            try
            {
                string markerPath = GetCompactionMarkerPath();
                var markerState = TryReadCompactionMarker(markerPath, out var parsedMarker)
                    ? parsedMarker
                    : null;

                if (resumeOnly && markerState == null)
                    throw new InvalidOperationException("No in-progress compaction marker exists.");

                EnsureNoActiveTransactions();

                var startedAt = markerState?.StartedAtUtc ?? DateTimeOffset.UtcNow;
                var preSnapshot = CaptureCompactionSnapshot();
                string? tempPath = markerState?.TempDatabasePath;
                if (string.IsNullOrWhiteSpace(tempPath)) tempPath = GetCompactionTempPath();

                string? backupPath = markerState?.BackupDatabasePath;
                if (string.IsNullOrWhiteSpace(backupPath)) backupPath = GetCompactionBackupPath();

                markerState ??= new CompactionMarkerState
                {
                    Phase = CompactionMarkerPhase.Started,
                    DatabasePath = _pageFile.FilePath,
                    TempDatabasePath = tempPath,
                    BackupDatabasePath = backupPath,
                    StartedAtUtc = startedAt,
                    OnlineMode = false,
                    Mode = "CopySwap"
                };

                if (!TryReadCompactionMarker(markerPath, out _)) WriteCompactionMarker(markerPath, markerState);

                var work = new CompactionWork();
                TailTruncationResult tailResult = default;

                if (markerState.Phase <= CompactionMarkerPhase.Started)
                {
                    CheckpointInternal();
                    _pageFile.Flush();
                    ValidateCollectionMetadataAndPrimaryIndexPointers(ct);

                    EnsureCompactionArtifactDirectory(tempPath);
                    EnsureCompactionArtifactDirectory(backupPath);
                    DeleteFileIfExists(tempPath);
                    DeleteFileIfExists(GetCompactionTempWalPath(tempPath));

                    (work, tailResult) = RunCopySwapCompactionOnTemporaryFile(tempPath, options, ct);

                    markerState.Phase = CompactionMarkerPhase.Copied;
                    markerState.Mode = "CopySwap";
                    markerState.TempDatabasePath = tempPath;
                    markerState.BackupDatabasePath = backupPath;
                    WriteCompactionMarker(markerPath, markerState);
                }

                if (markerState.Phase <= CompactionMarkerPhase.Copied)
                {
                    if (!File.Exists(tempPath))
                    {
                        ValidateCollectionMetadataAndPrimaryIndexPointers(ct);
                        (work, tailResult) = RunCopySwapCompactionOnTemporaryFile(tempPath, options, ct);
                    }

                    DeleteFileIfExists(backupPath);
                    _pageFile.SnapshotToFile(backupPath);
                    _pageFile.ReplaceFromFile(tempPath);
                    ReloadDictionaryStateAfterCompactionSwap();

                    try
                    {
                        ValidateCollectionMetadataAndPrimaryIndexPointers(ct);
                    }
                    catch
                    {
                        _pageFile.ReplaceFromFile(backupPath);
                        ReloadDictionaryStateAfterCompactionSwap();
                        throw;
                    }

                    markerState.Phase = CompactionMarkerPhase.Swapped;
                    WriteCompactionMarker(markerPath, markerState);
                }

                if (markerState.Phase <= CompactionMarkerPhase.Swapped)
                {
                    CheckpointInternal();
                    _pageFile.Flush();

                    markerState.Phase = CompactionMarkerPhase.CleanupDone;
                    WriteCompactionMarker(markerPath, markerState);
                }

                var postSnapshot = CaptureCompactionSnapshot();
                DeleteCompactionMarkerIfExists(markerPath);
                DeleteFileIfExists(tempPath);
                DeleteFileIfExists(GetCompactionTempWalPath(tempPath));
                DeleteFileIfExists(backupPath);

                return BuildStats(
                    preSnapshot,
                    postSnapshot,
                    work,
                    tailResult,
                    startedAt,
                    DateTimeOffset.UtcNow,
                    false,
                    resumeOnly || parsedMarker != null);
            }
            finally
            {
                _commitLock.Release();
            }
        }
        finally
        {
            _maintenanceGate.Release();
        }
    }

    private async Task<CompactionStats> ExecuteOnlinePassAsync(CompactionOptions options, CancellationToken ct)
    {
        await _maintenanceGate.WaitAsync(ct);
        try
        {
            var startedAt = DateTimeOffset.UtcNow;
            var deadline = startedAt + options.MaxOnlineDuration;
            var work = new CompactionWork();
            TailTruncationResult tailResult = default;

            await _commitLock.WaitAsync(ct);
            CompactionSnapshot preSnapshot;
            try
            {
                preSnapshot = CaptureCompactionSnapshot();
            }
            finally
            {
                _commitLock.Release();
            }

            while (DateTimeOffset.UtcNow < deadline)
            {
                ct.ThrowIfCancellationRequested();

                await _commitLock.WaitAsync(ct);
                try
                {
                    if (!_activeTransactions.IsEmpty) continue;

                    CheckpointInternal();
                    var batch = RunInPlaceMaintenanceBatch(
                        _onlineCompactionCursor,
                        options.OnlineBatchPageLimit,
                        options,
                        true,
                        ct);

                    work.PagesScanned += batch.PagesScanned;
                    work.PagesDefragmented += batch.PagesDefragmented;
                    work.BytesReclaimedByDefragmentation += batch.BytesReclaimedByDefragmentation;
                    work.FreeListPagesNormalized = batch.FreeListPagesNormalized;

                    if (options.EnableTailTruncation)
                    {
                        var currentTail = _pageFile.TruncateReclaimableTailPages(options.MinimumRetainedPages);
                        tailResult = new TailTruncationResult(
                            tailResult.PrePageCount == 0 ? currentTail.PrePageCount : tailResult.PrePageCount,
                            currentTail.PostPageCount,
                            tailResult.TruncatedPages + currentTail.TruncatedPages,
                            tailResult.TruncatedBytes + currentTail.TruncatedBytes);
                    }

                    _pageFile.Flush();

                    if (batch.PagesScanned == 0 &&
                        currentTailNoProgress(options.EnableTailTruncation ? tailResult : default)) break;
                }
                finally
                {
                    _commitLock.Release();
                }

                if (options.OnlineBatchDelay > TimeSpan.Zero)
                    await Task.Delay(options.OnlineBatchDelay, ct);
            }

            await _commitLock.WaitAsync(ct);
            CompactionSnapshot postSnapshot;
            try
            {
                postSnapshot = CaptureCompactionSnapshot();
            }
            finally
            {
                _commitLock.Release();
            }

            return BuildStats(
                preSnapshot,
                postSnapshot,
                work,
                tailResult,
                startedAt,
                DateTimeOffset.UtcNow,
                true,
                false);
        }
        finally
        {
            _maintenanceGate.Release();
        }

        static bool currentTailNoProgress(TailTruncationResult currentTail)
        {
            return currentTail.TruncatedPages == 0 && currentTail.TruncatedBytes == 0;
        }
    }

    private CompactionWork RunInPlaceMaintenanceBatch(uint pageStart, int maxPages, CompactionOptions options,
        bool updateCursor, CancellationToken ct)
    {
        var work = new CompactionWork();
        uint pageCount = _pageFile.NextPageId;
        if (pageCount <= 1 || maxPages <= 0)
            return work;

        if (options.NormalizeFreeList) work.FreeListPagesNormalized = _pageFile.NormalizeFreeList();

        if (!options.DefragmentSlottedPages)
            return work;

        if (pageStart < 1 || pageStart >= pageCount)
            pageStart = 1;

        int remaining = Math.Min(maxPages, (int)Math.Max(0, pageCount - 1));
        uint pageId = pageStart;

        while (remaining > 0)
        {
            ct.ThrowIfCancellationRequested();
            if (pageId >= pageCount) pageId = 1;

            var result = _pageFile.DefragmentSlottedPageWithStats(pageId);
            if (result.Changed)
            {
                work.PagesDefragmented++;
                work.BytesReclaimedByDefragmentation += result.ReclaimedBytes;
                if (result.RelocatedSlotCount > 0)
                {
                    work.DocumentsRelocated += result.RelocatedSlotCount;
                    work.PagesRelocated++;
                }
            }

            work.PagesScanned++;
            remaining--;
            pageId++;
        }

        if (updateCursor) _onlineCompactionCursor = pageId >= pageCount ? 1u : pageId;

        return work;
    }

    private CompactionSnapshot CaptureCompactionSnapshot()
    {
        long fileSizeBytes = _pageFile.FileLengthBytes;
        uint logicalPageCount = _pageFile.NextPageId;
        int pageSize = _pageFile.PageSize;
        var pageCount = (uint)(fileSizeBytes / pageSize);

        var freePages = _pageFile.EnumerateFreePages();
        var freePageSet = new HashSet<uint>(freePages);

        if (pageCount > logicalPageCount)
            for (uint pageId = logicalPageCount; pageId < pageCount; pageId++)
                freePageSet.Add(pageId);

        int freePageCount = freePageSet.Count;

        long slottedFreeBytes = 0;
        if (logicalPageCount > 1)
        {
            var pageBuffer = new byte[pageSize];
            for (uint pageId = 1; pageId < logicalPageCount; pageId++)
            {
                _pageFile.ReadPage(pageId, pageBuffer);
                if (!TryReadSlottedFreeSpace(pageBuffer, out int availableFreeSpace))
                    continue;

                slottedFreeBytes += availableFreeSpace;
            }
        }

        long freePageBytes = (long)freePageCount * pageSize;
        long totalFreeBytes = freePageBytes + slottedFreeBytes;
        double fragmentationPercent = fileSizeBytes <= 0
            ? 0
            : totalFreeBytes * 100d / fileSizeBytes;

        uint tailReclaimablePages = 0;
        for (uint pageId = pageCount; pageId > 2; pageId--)
        {
            uint candidate = pageId - 1;
            if (!freePageSet.Contains(candidate))
                break;

            tailReclaimablePages++;
        }

        return new CompactionSnapshot(
            fileSizeBytes,
            pageCount,
            freePageCount,
            totalFreeBytes,
            fragmentationPercent,
            tailReclaimablePages);
    }

    private static bool TryReadSlottedFreeSpace(ReadOnlySpan<byte> pageBuffer, out int freeSpace)
    {
        freeSpace = 0;
        if (pageBuffer.Length < SlottedPageHeader.Size)
            return false;

        var header = SlottedPageHeader.ReadFrom(pageBuffer);
        if (!IsSlottedPageType(header.PageType))
            return false;

        int slotArrayEnd = SlottedPageHeader.Size + header.SlotCount * SlotEntry.Size;
        if (slotArrayEnd > pageBuffer.Length)
            return false;

        if (header.FreeSpaceStart < slotArrayEnd || header.FreeSpaceEnd > pageBuffer.Length ||
            header.FreeSpaceEnd < header.FreeSpaceStart)
            return false;

        freeSpace = header.AvailableFreeSpace;
        return true;
    }

    private static bool IsSlottedPageType(PageType pageType)
    {
        return pageType is PageType.Collection or PageType.Data;
    }

    private static CompactionStats BuildStats(
        CompactionSnapshot pre,
        CompactionSnapshot post,
        CompactionWork work,
        TailTruncationResult tail,
        DateTimeOffset startedAt,
        DateTimeOffset completedAt,
        bool onlineMode,
        bool resumed)
    {
        return new CompactionStats
        {
            StartedAtUtc = startedAt,
            CompletedAtUtc = completedAt,
            OnlineMode = onlineMode,
            ResumedFromMarker = resumed,
            PreFileSizeBytes = pre.FileSizeBytes,
            PostFileSizeBytes = post.FileSizeBytes,
            PrePageCount = pre.PageCount,
            PostPageCount = post.PageCount,
            PreFreePages = pre.FreePageCount,
            PostFreePages = post.FreePageCount,
            PreFreeBytes = pre.TotalFreeBytes,
            PostFreeBytes = post.TotalFreeBytes,
            PreLiveBytes = Math.Max(0, pre.FileSizeBytes - pre.TotalFreeBytes),
            PostLiveBytes = Math.Max(0, post.FileSizeBytes - post.TotalFreeBytes),
            PreFragmentationPercent = pre.FragmentationPercent,
            PostFragmentationPercent = post.FragmentationPercent,
            PagesScanned = work.PagesScanned,
            PagesDefragmented = work.PagesDefragmented,
            BytesReclaimedByDefragmentation = work.BytesReclaimedByDefragmentation,
            FreeListPagesNormalized = work.FreeListPagesNormalized,
            TailPagesTruncated = tail.TruncatedPages,
            TailBytesTruncated = tail.TruncatedBytes,
            DocumentsRelocated = work.DocumentsRelocated,
            PagesRelocated = work.PagesRelocated
        };
    }

    private string GetCompactionTempPath()
    {
        return $"{_pageFile.FilePath}.compact.tmp";
    }

    private string GetCompactionBackupPath()
    {
        return $"{_pageFile.FilePath}.compact.bak";
    }

    private string GetCompactionMarkerPath()
    {
        return $"{_pageFile.FilePath}.compact.state";
    }

    private static string GetCompactionTempWalPath(string tempPath)
    {
        return Path.ChangeExtension(tempPath, ".wal");
    }

    private static void EnsureCompactionArtifactDirectory(string path)
    {
        string? directory = Path.GetDirectoryName(path);
        if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory);
    }

    private static void DeleteFileIfExists(string path)
    {
        if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) File.Delete(path);
    }

    private void ValidateCollectionMetadataAndPrimaryIndexPointers(CancellationToken ct)
    {
        var metadataEntries = GetAllCollectionMetadata();
        var pageBuffer = new byte[_pageFile.PageSize];

        foreach (var metadata in metadataEntries)
        {
            ct.ThrowIfCancellationRequested();

            ValidateRootPage(
                metadata.Name,
                "primary index",
                metadata.PrimaryRootPageId,
                PageType.Index,
                pageBuffer);
            ValidateRootPage(
                metadata.Name,
                "schema",
                metadata.SchemaRootPageId,
                PageType.Schema,
                pageBuffer);

            foreach (var indexMetadata in metadata.Indexes)
            {
                ct.ThrowIfCancellationRequested();
                ValidateRootPage(
                    metadata.Name,
                    $"index '{indexMetadata.Name}'",
                    indexMetadata.RootPageId,
                    GetExpectedIndexRootPageType(indexMetadata.Type),
                    pageBuffer);
            }

            foreach (var location in EnumeratePrimaryLocations(metadata))
            {
                ct.ThrowIfCancellationRequested();
                if (!TryReadStoredPayload(location, out _, out _))
                    throw new InvalidDataException(
                        $"Compaction validation failed for collection '{metadata.Name}': " +
                        $"primary index location (page {location.PageId}, slot {location.SlotIndex}) is invalid or deleted.");
            }
        }
    }

    private void ValidateRootPage(
        string collectionName,
        string rootLabel,
        uint rootPageId,
        PageType expectedPageType,
        byte[] pageBuffer)
    {
        if (rootPageId == 0) return;

        if (rootPageId >= _pageFile.NextPageId)
            throw new InvalidDataException(
                $"Compaction validation failed for collection '{collectionName}': " +
                $"{rootLabel} root page id {rootPageId} is out of range.");

        _pageFile.ReadPage(rootPageId, pageBuffer);
        var pageHeader = PageHeader.ReadFrom(pageBuffer);
        if (pageHeader.PageType != expectedPageType)
            throw new InvalidDataException(
                $"Compaction validation failed for collection '{collectionName}': " +
                $"{rootLabel} root page id {rootPageId} has type {pageHeader.PageType}, expected {expectedPageType}.");
    }

    private static PageType GetExpectedIndexRootPageType(IndexType indexType)
    {
        return indexType switch
        {
            IndexType.Vector => PageType.Vector,
            IndexType.Spatial => PageType.Spatial,
            _ => PageType.Index
        };
    }

    private (CompactionWork Work, TailTruncationResult TailResult) RunCopySwapCompactionOnTemporaryFile(
        string tempPath,
        CompactionOptions options,
        CancellationToken ct)
    {
        DeleteFileIfExists(tempPath);
        DeleteFileIfExists(GetCompactionTempWalPath(tempPath));
        var work = new CompactionWork();
        TailTruncationResult tailResult = default;

        long logicalSourceSizeBytes = Math.Max(_pageFile.NextPageId * _pageFile.PageSize, _pageFile.PageSize * 2L);
        var tempConfig = CreateWritableCompactionPageFileConfig(_pageFile.Config, logicalSourceSizeBytes);
        var tempMaintenance = CreateCompactionTempMaintenanceOptions();
        using var tempEngine = new StorageEngine(
            tempPath,
            tempConfig,
            CompressionOptions,
            tempMaintenance);

        tempEngine.WriteStorageFormatMetadata(StorageFormatMetadata);
        _ = tempEngine._pageFile.NormalizeFreeList();
        CopyDictionaryMappingDeterministically(tempEngine, ct);

        var sourceCollections = GetAllCollectionMetadata();

        var relocatedSourcePages = new HashSet<uint>();

        foreach (var sourceMetadata in sourceCollections)
        {
            ct.ThrowIfCancellationRequested();
            var collectionResult = RebuildCollectionForCompaction(tempEngine, sourceMetadata, ct);
            tempEngine.SaveCollectionMetadata(collectionResult.Metadata);

            work.DocumentsRelocated += collectionResult.DocumentsRelocated;
            foreach (uint relocatedPageId in collectionResult.RelocatedSourcePageIds)
                relocatedSourcePages.Add(relocatedPageId);
        }

        work.PagesRelocated = relocatedSourcePages.Count;
        work.PagesScanned = Math.Max(0, (int)Math.Max(0, _pageFile.NextPageId - 1));

        if (options.NormalizeFreeList) work.FreeListPagesNormalized = tempEngine._pageFile.NormalizeFreeList();

        if (options.EnableTailTruncation)
            tailResult = tempEngine._pageFile.TruncateReclaimableTailPages(options.MinimumRetainedPages);

        _ = tempEngine._pageFile.TrimExcessCapacityToLogicalPageCount();
        tempEngine.CheckpointInternal();
        tempEngine._pageFile.Flush();
        return (work, tailResult);
    }

    private static PageFileConfig CreateWritableCompactionPageFileConfig(PageFileConfig currentConfig,
        long sourceFileSize)
    {
        long minimumFileSize = Math.Max(currentConfig.PageSize * 2L, 0L);
        long initialFileSize = Math.Max(sourceFileSize, minimumFileSize);

        if (currentConfig.Access == MemoryMappedFileAccess.ReadWrite)
            return new PageFileConfig
            {
                PageSize = currentConfig.PageSize,
                InitialFileSize = initialFileSize,
                Access = currentConfig.Access
            };

        return new PageFileConfig
        {
            PageSize = currentConfig.PageSize,
            InitialFileSize = initialFileSize,
            Access = MemoryMappedFileAccess.ReadWrite
        };
    }

    private MaintenanceOptions CreateCompactionTempMaintenanceOptions()
    {
        return new MaintenanceOptions
        {
            RunAtStartup = false,
            MinFragmentationPercent = MaintenanceOptions.MinFragmentationPercent,
            MinReclaimableBytes = MaintenanceOptions.MinReclaimableBytes,
            MaxRunDuration = MaintenanceOptions.MaxRunDuration,
            OnlineThrottlePagesPerBatch = MaintenanceOptions.OnlineThrottlePagesPerBatch,
            OnlineThrottleDelay = MaintenanceOptions.OnlineThrottleDelay
        };
    }

    private CompactionCollectionRebuildResult RebuildCollectionForCompaction(
        StorageEngine tempEngine,
        CollectionMetadata sourceMetadata,
        CancellationToken ct)
    {
        var targetMetadata = new CollectionMetadata
        {
            Name = sourceMetadata.Name
        };

        targetMetadata.SchemaRootPageId = CopySchemaChainForCompaction(tempEngine, sourceMetadata.SchemaRootPageId, ct);

        if (sourceMetadata.PrimaryRootPageId == 0)
        {
            foreach (var sourceIndexMetadata in sourceMetadata.Indexes)
            {
                if (sourceIndexMetadata.RootPageId != 0)
                    throw new InvalidDataException(
                        $"Compaction validation failed for collection '{sourceMetadata.Name}': " +
                        $"secondary index '{sourceIndexMetadata.Name}' has a root but primary index root is missing.");

                targetMetadata.Indexes.Add(CloneIndexMetadata(sourceIndexMetadata, 0));
            }

            return new CompactionCollectionRebuildResult(targetMetadata, 0, Array.Empty<uint>());
        }

        var dataWriter = new CompactionDataWriterState();
        var relocatedSourcePages = new HashSet<uint>();
        var locationRemap = new Dictionary<DocumentLocation, DocumentLocation>();

        uint resolvedPrimaryRoot = ResolveCurrentBTreeRootPageId(
            sourceMetadata.PrimaryRootPageId,
            sourceMetadata.Name,
            "primary index");
        var sourcePrimary = new BTreeIndex(this, IndexOptions.CreateUnique("_id"), resolvedPrimaryRoot);
        var targetPrimary = CreateCompactionBTreeAtRoot(
            tempEngine,
            IndexOptions.CreateUnique("_id"),
            0);
        targetMetadata.PrimaryRootPageId = targetPrimary.RootPageId;

        long relocatedDocuments = 0;
        using (var transaction = tempEngine.BeginTransaction())
        {
            foreach (var primaryEntry in sourcePrimary.Range(
                         IndexKey.MinKey,
                         IndexKey.MaxKey,
                         IndexDirection.Forward,
                         0))
            {
                ct.ThrowIfCancellationRequested();

                if (!TryReadStoredPayload(primaryEntry.Location, out byte[] storedPayload, out bool isCompressed))
                    throw new InvalidDataException(
                        $"Compaction rebuild failed for collection '{sourceMetadata.Name}': " +
                        $"cannot read source payload at (page {primaryEntry.Location.PageId}, slot {primaryEntry.Location.SlotIndex}).");

                var rebuiltLocation =
                    InsertStoredPayloadForCompaction(tempEngine, storedPayload, isCompressed, dataWriter);
                targetPrimary.Insert(primaryEntry.Key, rebuiltLocation, transaction.TransactionId);
                locationRemap[primaryEntry.Location] = rebuiltLocation;
                relocatedSourcePages.Add(primaryEntry.Location.PageId);
                relocatedDocuments++;
            }

            foreach (var sourceIndexMetadata in sourceMetadata.Indexes)
            {
                ct.ThrowIfCancellationRequested();
                var rebuiltIndexMetadata = RebuildSecondaryIndexForCompaction(
                    tempEngine,
                    sourceMetadata.Name,
                    sourceIndexMetadata,
                    locationRemap,
                    transaction,
                    ct);
                targetMetadata.Indexes.Add(rebuiltIndexMetadata);
            }

            transaction.Commit();
        }

        tempEngine.CheckpointInternal();
        tempEngine._pageFile.Flush();

        return new CompactionCollectionRebuildResult(targetMetadata, relocatedDocuments,
            relocatedSourcePages.ToArray());
    }

    private static IndexMetadata CloneIndexMetadata(IndexMetadata source, uint rootPageId)
    {
        return new IndexMetadata
        {
            Name = source.Name,
            IsUnique = source.IsUnique,
            Type = source.Type,
            PropertyPaths = source.PropertyPaths.ToArray(),
            Dimensions = source.Dimensions,
            Metric = source.Metric,
            RootPageId = rootPageId
        };
    }

    private uint CopySchemaChainForCompaction(StorageEngine tempEngine, uint sourceSchemaRootPageId,
        CancellationToken ct)
    {
        if (sourceSchemaRootPageId == 0)
            return 0;

        var schemas = GetSchemas(sourceSchemaRootPageId);
        uint rebuiltSchemaRoot = 0;
        foreach (var schema in schemas)
        {
            ct.ThrowIfCancellationRequested();
            rebuiltSchemaRoot = tempEngine.AppendSchema(rebuiltSchemaRoot, schema);
        }

        return rebuiltSchemaRoot;
    }

    private IndexMetadata RebuildSecondaryIndexForCompaction(
        StorageEngine tempEngine,
        string collectionName,
        IndexMetadata sourceIndexMetadata,
        IReadOnlyDictionary<DocumentLocation, DocumentLocation> locationRemap,
        Transaction tempTransaction,
        CancellationToken ct)
    {
        if (sourceIndexMetadata.RootPageId == 0)
            return CloneIndexMetadata(sourceIndexMetadata, 0);

        switch (sourceIndexMetadata.Type)
        {
            case IndexType.BTree:
            case IndexType.Unique:
            case IndexType.Hash:
            {
                var sourceOptions = BuildIndexOptionsForCompaction(sourceIndexMetadata);
                uint resolvedSourceRoot = ResolveCurrentBTreeRootPageId(
                    sourceIndexMetadata.RootPageId,
                    collectionName,
                    $"index '{sourceIndexMetadata.Name}'");
                var sourceIndex = new BTreeIndex(this, sourceOptions, resolvedSourceRoot);
                var rebuiltIndex = CreateCompactionBTreeAtRoot(
                    tempEngine,
                    sourceOptions,
                    0);

                foreach (var entry in sourceIndex.Range(
                             IndexKey.MinKey,
                             IndexKey.MaxKey,
                             IndexDirection.Forward,
                             0))
                {
                    ct.ThrowIfCancellationRequested();
                    var mapped = RemapLocationOrThrow(
                        locationRemap,
                        entry.Location,
                        collectionName,
                        sourceIndexMetadata.Name);
                    rebuiltIndex.Insert(entry.Key, mapped, tempTransaction.TransactionId);
                }

                return CloneIndexMetadata(sourceIndexMetadata, rebuiltIndex.RootPageId);
            }
            case IndexType.Spatial:
            {
                var options = BuildIndexOptionsForCompaction(sourceIndexMetadata);
                using var rebuiltIndex = new RTreeIndex(tempEngine, options, 0);
                foreach (var spatialEntry in EnumerateSpatialLeafEntriesForCompaction(sourceIndexMetadata.RootPageId,
                             ct))
                {
                    var mapped = RemapLocationOrThrow(
                        locationRemap,
                        spatialEntry.Location,
                        collectionName,
                        sourceIndexMetadata.Name);
                    rebuiltIndex.Insert(spatialEntry.Mbr, mapped, tempTransaction);
                }

                return CloneIndexMetadata(sourceIndexMetadata, rebuiltIndex.RootPageId);
            }
            case IndexType.Vector:
            {
                var options = ResolveVectorIndexOptionsForCompaction(sourceIndexMetadata);
                var rebuiltIndex = new VectorSearchIndex(tempEngine, options);
                foreach (var vectorNode in EnumerateVectorNodesForCompaction(sourceIndexMetadata.RootPageId, options,
                             ct))
                {
                    var mapped = RemapLocationOrThrow(
                        locationRemap,
                        vectorNode.Location,
                        collectionName,
                        sourceIndexMetadata.Name);
                    rebuiltIndex.Insert(vectorNode.Vector, mapped, tempTransaction);
                }

                return CloneIndexMetadata(sourceIndexMetadata, rebuiltIndex.RootPageId);
            }
            default:
                throw new InvalidDataException(
                    $"Compaction rebuild failed for collection '{collectionName}', index '{sourceIndexMetadata.Name}': " +
                    $"unsupported index type '{sourceIndexMetadata.Type}'.");
        }
    }

    private static DocumentLocation RemapLocationOrThrow(
        IReadOnlyDictionary<DocumentLocation, DocumentLocation> locationRemap,
        in DocumentLocation sourceLocation,
        string collectionName,
        string indexName)
    {
        if (locationRemap.TryGetValue(sourceLocation, out var mapped))
            return mapped;

        throw new InvalidDataException(
            $"Compaction rebuild failed for collection '{collectionName}', index '{indexName}': " +
            $"source location (page {sourceLocation.PageId}, slot {sourceLocation.SlotIndex}) was not found in the rebuilt primary index.");
    }

    private uint ResolveCurrentBTreeRootPageId(uint configuredRootPageId, string collectionName, string rootLabel)
    {
        if (configuredRootPageId == 0)
            return 0;

        uint current = configuredRootPageId;
        var visited = new HashSet<uint>();
        var pageBuffer = new byte[_pageFile.PageSize];

        while (visited.Add(current))
        {
            if (current >= _pageFile.NextPageId)
                throw new InvalidDataException(
                    $"Compaction validation failed for collection '{collectionName}': " +
                    $"{rootLabel} root page id {current} is out of range.");

            _pageFile.ReadPage(current, pageBuffer);
            var pageHeader = PageHeader.ReadFrom(pageBuffer);
            if (pageHeader.PageType != PageType.Index)
                throw new InvalidDataException(
                    $"Compaction validation failed for collection '{collectionName}': " +
                    $"{rootLabel} root page id {current} has type {pageHeader.PageType}, expected {PageType.Index}.");

            var nodeHeader = BTreeNodeHeader.ReadFrom(pageBuffer.AsSpan(32));
            if (nodeHeader.ParentPageId == 0)
                return current;

            current = nodeHeader.ParentPageId;
        }

        throw new InvalidDataException(
            $"Compaction validation failed for collection '{collectionName}': " +
            $"{rootLabel} root page chain contains a cycle.");
    }

    private static void EnsureRootPagesAllocatedForCompaction(StorageEngine target, IEnumerable<uint> rootPageIds)
    {
        uint maxRootPageId = rootPageIds
            .Where(pageId => pageId > 0)
            .DefaultIfEmpty(0u)
            .Max();

        while (maxRootPageId > 0 && target._pageFile.NextPageId <= maxRootPageId) target.AllocatePage();
    }

    private static BTreeIndex CreateCompactionBTreeAtRoot(StorageEngine target, IndexOptions options, uint rootPageId)
    {
        if (rootPageId == 0)
            return new BTreeIndex(target, options);

        if (target._pageFile.NextPageId <= rootPageId)
            EnsureRootPagesAllocatedForCompaction(target, new[] { rootPageId });

        InitializeBTreeRootPageForCompaction(target, rootPageId);
        return new BTreeIndex(target, options, rootPageId);
    }

    private static void InitializeBTreeRootPageForCompaction(StorageEngine target, uint rootPageId)
    {
        var pageBuffer = new byte[target.PageSize];
        var pageHeader = new PageHeader
        {
            PageId = rootPageId,
            PageType = PageType.Index,
            FreeBytes = (ushort)(target.PageSize - 32),
            NextPageId = 0,
            TransactionId = 0,
            Checksum = 0
        };
        pageHeader.WriteTo(pageBuffer);

        var nodeHeader = new BTreeNodeHeader
        {
            PageId = rootPageId,
            IsLeaf = true,
            EntryCount = 0,
            ParentPageId = 0,
            NextLeafPageId = 0,
            PrevLeafPageId = 0
        };
        nodeHeader.WriteTo(pageBuffer.AsSpan(32));
        target.WritePageImmediate(rootPageId, pageBuffer);
    }

    private static IndexOptions BuildIndexOptionsForCompaction(IndexMetadata metadata)
    {
        string[] fields = metadata.PropertyPaths ?? Array.Empty<string>();
        return metadata.Type switch
        {
            IndexType.Unique => IndexOptions.CreateUnique(fields),
            IndexType.Hash => IndexOptions.CreateHash(fields),
            IndexType.Spatial => IndexOptions.CreateSpatial(fields),
            IndexType.Vector => IndexOptions.CreateVector(
                Math.Max(1, metadata.Dimensions),
                metadata.Metric,
                16,
                200,
                fields),
            _ => metadata.IsUnique ? IndexOptions.CreateUnique(fields) : IndexOptions.CreateBTree(fields)
        };
    }

    private IndexOptions ResolveVectorIndexOptionsForCompaction(IndexMetadata metadata)
    {
        int dimensions = metadata.Dimensions;
        var maxM = 16;

        if (TryReadVectorPageLayout(metadata.RootPageId, out int pageDimensions, out int pageMaxM))
        {
            if (pageDimensions > 0)
                dimensions = pageDimensions;
            if (pageMaxM > 0)
                maxM = pageMaxM;
        }

        if (dimensions <= 0)
            throw new InvalidDataException(
                $"Compaction rebuild failed for index '{metadata.Name}': vector dimensions are invalid.");

        return IndexOptions.CreateVector(
            dimensions,
            metadata.Metric,
            maxM,
            200,
            metadata.PropertyPaths ?? Array.Empty<string>());
    }

    private bool TryReadVectorPageLayout(uint rootPageId, out int dimensions, out int maxM)
    {
        dimensions = 0;
        maxM = 0;
        if (rootPageId == 0 || rootPageId >= _pageFile.NextPageId)
            return false;

        var buffer = new byte[_pageFile.PageSize];
        _pageFile.ReadPage(rootPageId, buffer);
        var pageHeader = PageHeader.ReadFrom(buffer);
        if (pageHeader.PageType != PageType.Vector)
            return false;

        dimensions = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(32, 4));
        maxM = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(36, 4));
        return dimensions > 0 && maxM > 0;
    }

    private IEnumerable<(GeoBox Mbr, DocumentLocation Location)> EnumerateSpatialLeafEntriesForCompaction(
        uint rootPageId,
        CancellationToken ct)
    {
        if (rootPageId == 0)
            yield break;

        var pending = new SortedSet<uint> { rootPageId };
        var visitedPages = new HashSet<uint>();
        var pageBuffer = new byte[_pageFile.PageSize];

        while (pending.Count > 0)
        {
            ct.ThrowIfCancellationRequested();
            uint pageId = pending.Min;
            pending.Remove(pageId);

            if (!visitedPages.Add(pageId))
                continue;

            if (pageId >= _pageFile.NextPageId)
                throw new InvalidDataException(
                    $"Compaction rebuild failed: spatial page id {pageId} is out of range.");

            _pageFile.ReadPage(pageId, pageBuffer);
            var pageHeader = PageHeader.ReadFrom(pageBuffer);
            if (pageHeader.PageType != PageType.Spatial)
                throw new InvalidDataException(
                    $"Compaction rebuild failed: page {pageId} in spatial index chain is not a spatial page.");

            bool isLeaf = SpatialPage.GetIsLeaf(pageBuffer);
            ushort entryCount = SpatialPage.GetEntryCount(pageBuffer);

            for (var i = 0; i < entryCount; i++)
            {
                SpatialPage.ReadEntry(pageBuffer, i, out var mbr, out var pointer);
                if (isLeaf)
                {
                    yield return (mbr, pointer);
                }
                else
                {
                    if (pointer.PageId == 0 || pointer.PageId >= _pageFile.NextPageId)
                        throw new InvalidDataException(
                            $"Compaction rebuild failed: spatial child pointer page {pointer.PageId} is invalid.");

                    if (!visitedPages.Contains(pointer.PageId)) pending.Add(pointer.PageId);
                }
            }
        }
    }

    private IEnumerable<CompactionVectorNode> EnumerateVectorNodesForCompaction(
        uint rootPageId,
        IndexOptions vectorOptions,
        CancellationToken ct)
    {
        if (rootPageId == 0)
            yield break;

        int dimensions = vectorOptions.Dimensions;
        int maxM = Math.Max(1, vectorOptions.M);
        var pending = new SortedSet<uint> { rootPageId };
        var visitedPages = new HashSet<uint>();
        var pageBuffer = new byte[_pageFile.PageSize];

        while (pending.Count > 0)
        {
            ct.ThrowIfCancellationRequested();
            uint pageId = pending.Min;
            pending.Remove(pageId);

            if (!visitedPages.Add(pageId))
                continue;

            if (pageId >= _pageFile.NextPageId)
                throw new InvalidDataException(
                    $"Compaction rebuild failed: vector page id {pageId} is out of range.");

            _pageFile.ReadPage(pageId, pageBuffer);
            var pageHeader = PageHeader.ReadFrom(pageBuffer);
            if (pageHeader.PageType != PageType.Vector)
                throw new InvalidDataException(
                    $"Compaction rebuild failed: page {pageId} in vector index chain is not a vector page.");

            int nodeCount = VectorPage.GetNodeCount(pageBuffer);
            if (nodeCount < 0 || nodeCount > VectorPage.GetMaxNodes(pageBuffer))
                throw new InvalidDataException(
                    $"Compaction rebuild failed: vector page {pageId} has an invalid node count ({nodeCount}).");

            for (var nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++)
            {
                var vector = new float[dimensions];
                VectorPage.ReadNodeData(pageBuffer, nodeIndex, out var location, out int maxLevel, vector);
                yield return new CompactionVectorNode(pageId, nodeIndex, location, vector);

                int levelUpperBound = Math.Min(15, Math.Max(0, maxLevel));
                for (var level = 0; level <= levelUpperBound; level++)
                {
                    var links = VectorPage.GetLinksSpan(pageBuffer, nodeIndex, level, dimensions, maxM);
                    for (var offset = 0;
                         offset + DocumentLocation.SerializedSize <= links.Length;
                         offset += DocumentLocation.SerializedSize)
                    {
                        var link = DocumentLocation.ReadFrom(links.Slice(offset, DocumentLocation.SerializedSize));
                        if (link.PageId == 0)
                            break;

                        if (link.PageId >= _pageFile.NextPageId)
                            throw new InvalidDataException(
                                $"Compaction rebuild failed: vector link points to out-of-range page {link.PageId}.");

                        if (!visitedPages.Contains(link.PageId)) pending.Add(link.PageId);
                    }
                }
            }
        }
    }

    private static DocumentLocation InsertStoredPayloadForCompaction(
        StorageEngine target,
        ReadOnlySpan<byte> storedPayload,
        bool isCompressed,
        CompactionDataWriterState writerState)
    {
        var baseFlags = isCompressed ? SlotFlags.Compressed : SlotFlags.None;
        int maxSinglePagePayload = target.PageSize - SlottedPageHeader.Size - SlotEntry.Size;
        if (storedPayload.Length + SlotEntry.Size <= maxSinglePagePayload)
        {
            uint pageId =
                FindDataPageWithSpaceForCompaction(target, writerState, storedPayload.Length + SlotEntry.Size);
            if (pageId == 0) pageId = AllocateDataPageForCompaction(target, writerState);

            ushort slotIndex =
                InsertPayloadIntoDataPageForCompaction(target, pageId, storedPayload, baseFlags, writerState);
            return new DocumentLocation(pageId, slotIndex);
        }

        const int overflowMetadataSize = 8;
        int primaryChunkSize = maxSinglePagePayload - overflowMetadataSize;
        if (primaryChunkSize <= 0)
            throw new InvalidOperationException(
                "Compaction rebuild cannot allocate overflow payload on current page size.");

        uint nextOverflowPageId = 0;
        int overflowChunkSize = target.PageSize - SlottedPageHeader.Size;
        int overflowBytes = storedPayload.Length - primaryChunkSize;
        int fullPages = overflowBytes / overflowChunkSize;
        int tailBytes = overflowBytes % overflowChunkSize;

        if (tailBytes > 0)
        {
            int tailOffset = primaryChunkSize + fullPages * overflowChunkSize;
            nextOverflowPageId = AllocateOverflowPageForCompaction(target, storedPayload.Slice(tailOffset, tailBytes),
                nextOverflowPageId);
        }

        for (int i = fullPages - 1; i >= 0; i--)
        {
            int chunkOffset = primaryChunkSize + i * overflowChunkSize;
            nextOverflowPageId = AllocateOverflowPageForCompaction(
                target,
                storedPayload.Slice(chunkOffset, overflowChunkSize),
                nextOverflowPageId);
        }

        var primaryPayload = new byte[overflowMetadataSize + primaryChunkSize];
        BinaryPrimitives.WriteInt32LittleEndian(primaryPayload.AsSpan(0, 4), storedPayload.Length);
        BinaryPrimitives.WriteUInt32LittleEndian(primaryPayload.AsSpan(4, 4), nextOverflowPageId);
        storedPayload.Slice(0, primaryChunkSize).CopyTo(primaryPayload.AsSpan(8));

        uint primaryPageId =
            FindDataPageWithSpaceForCompaction(target, writerState, primaryPayload.Length + SlotEntry.Size);
        if (primaryPageId == 0) primaryPageId = AllocateDataPageForCompaction(target, writerState);

        var slotFlags = baseFlags | SlotFlags.HasOverflow;
        ushort primarySlotIndex =
            InsertPayloadIntoDataPageForCompaction(target, primaryPageId, primaryPayload, slotFlags, writerState);
        return new DocumentLocation(primaryPageId, primarySlotIndex);
    }

    private static uint AllocateOverflowPageForCompaction(StorageEngine target, ReadOnlySpan<byte> payloadChunk,
        uint nextOverflowPageId)
    {
        uint overflowPageId = target.AllocatePage();
        var pageBuffer = new byte[target.PageSize];
        var header = new SlottedPageHeader
        {
            PageId = overflowPageId,
            PageType = PageType.Overflow,
            SlotCount = 0,
            FreeSpaceStart = SlottedPageHeader.Size,
            FreeSpaceEnd = (ushort)target.PageSize,
            NextOverflowPage = nextOverflowPageId,
            TransactionId = 0
        };
        header.WriteTo(pageBuffer);
        payloadChunk.CopyTo(pageBuffer.AsSpan(SlottedPageHeader.Size));
        target.WritePageImmediate(overflowPageId, pageBuffer);
        return overflowPageId;
    }

    private static uint FindDataPageWithSpaceForCompaction(StorageEngine target, CompactionDataWriterState writerState,
        int requiredBytes)
    {
        if (writerState.CurrentDataPageId != 0 &&
            writerState.FreeSpaceByPage.TryGetValue(writerState.CurrentDataPageId, out ushort currentFree) &&
            currentFree >= requiredBytes)
            return writerState.CurrentDataPageId;

        foreach ((uint pageId, ushort freeBytes) in writerState.FreeSpaceByPage)
            if (freeBytes >= requiredBytes)
                return pageId;

        return 0;
    }

    private static uint AllocateDataPageForCompaction(StorageEngine target, CompactionDataWriterState writerState)
    {
        uint pageId = target.AllocatePage();
        var pageBuffer = new byte[target.PageSize];
        var header = new SlottedPageHeader
        {
            PageId = pageId,
            PageType = PageType.Data,
            SlotCount = 0,
            FreeSpaceStart = SlottedPageHeader.Size,
            FreeSpaceEnd = (ushort)target.PageSize,
            NextOverflowPage = 0,
            TransactionId = 0
        };
        header.WriteTo(pageBuffer);
        target.WritePageImmediate(pageId, pageBuffer);
        writerState.FreeSpaceByPage[pageId] = (ushort)header.AvailableFreeSpace;
        writerState.CurrentDataPageId = pageId;
        return pageId;
    }

    private static ushort InsertPayloadIntoDataPageForCompaction(
        StorageEngine target,
        uint pageId,
        ReadOnlySpan<byte> payload,
        SlotFlags slotFlags,
        CompactionDataWriterState writerState)
    {
        var pageBuffer = new byte[target.PageSize];
        target.ReadPage(pageId, null, pageBuffer);
        var header = SlottedPageHeader.ReadFrom(pageBuffer);

        if (header.PageType == PageType.Empty && header.FreeSpaceEnd == 0)
        {
            header = new SlottedPageHeader
            {
                PageId = pageId,
                PageType = PageType.Data,
                SlotCount = 0,
                FreeSpaceStart = SlottedPageHeader.Size,
                FreeSpaceEnd = (ushort)target.PageSize,
                NextOverflowPage = 0,
                TransactionId = 0
            };
            header.WriteTo(pageBuffer);
        }

        int requiredSpace = payload.Length + SlotEntry.Size;
        if (header.AvailableFreeSpace < requiredSpace)
            throw new InvalidOperationException(
                $"Compaction rebuild failed: not enough free space on page {pageId} (required {requiredSpace}, available {header.AvailableFreeSpace}).");

        ushort slotIndex = FindReusableSlotForCompaction(pageBuffer, ref header);
        int payloadOffset = header.FreeSpaceEnd - payload.Length;
        payload.CopyTo(pageBuffer.AsSpan(payloadOffset, payload.Length));

        int slotOffset = SlottedPageHeader.Size + slotIndex * SlotEntry.Size;
        var slot = new SlotEntry
        {
            Offset = (ushort)payloadOffset,
            Length = (ushort)payload.Length,
            Flags = slotFlags
        };
        slot.WriteTo(pageBuffer.AsSpan(slotOffset, SlotEntry.Size));

        if (slotIndex >= header.SlotCount)
            header.SlotCount = (ushort)(slotIndex + 1);

        header.FreeSpaceStart = (ushort)(SlottedPageHeader.Size + header.SlotCount * SlotEntry.Size);
        header.FreeSpaceEnd = (ushort)payloadOffset;
        header.WriteTo(pageBuffer);

        target.WritePageImmediate(pageId, pageBuffer);
        writerState.FreeSpaceByPage[pageId] = (ushort)header.AvailableFreeSpace;
        writerState.CurrentDataPageId = pageId;
        return slotIndex;
    }

    private static ushort FindReusableSlotForCompaction(Span<byte> pageBuffer, ref SlottedPageHeader header)
    {
        for (ushort i = 0; i < header.SlotCount; i++)
        {
            int slotOffset = SlottedPageHeader.Size + i * SlotEntry.Size;
            var slot = SlotEntry.ReadFrom(pageBuffer.Slice(slotOffset, SlotEntry.Size));
            if ((slot.Flags & SlotFlags.Deleted) != 0)
                return i;
        }

        return header.SlotCount;
    }

    private void CopyDictionaryMappingDeterministically(StorageEngine target, CancellationToken ct)
    {
        var sourceEntries = _dictionaryReverseCache
            .OrderBy(entry => entry.Key)
            .ToArray();

        ReinitializeDictionaryForCompaction(target, ct);

        ushort maxId = DictionaryPage.ReservedValuesEnd;
        foreach (var entry in sourceEntries)
        {
            ct.ThrowIfCancellationRequested();
            string key = entry.Value.ToLowerInvariant();
            ushort id = entry.Key;
            if (!target.InsertDictionaryEntryGlobal(key, id))
                throw new InvalidOperationException(
                    $"Compaction rebuild failed: unable to copy dictionary entry '{key}' with id {id}.");

            target._dictionaryCache[key] = id;
            target._dictionaryReverseCache[id] = key;
            if (id > maxId) maxId = id;
        }

        target._nextDictionaryId = (ushort)(maxId + 1);
    }

    private static void ReinitializeDictionaryForCompaction(StorageEngine target, CancellationToken ct)
    {
        var chainPages = EnumerateDictionaryChainPages(target, target._dictionaryRootPageId, ct);
        foreach (uint pageId in chainPages) target.FreePage(pageId);

        uint newRootPageId = target.AllocatePage();
        var dictionaryRootBuffer = new byte[target.PageSize];
        DictionaryPage.Initialize(dictionaryRootBuffer, newRootPageId);
        target.WritePageImmediate(newRootPageId, dictionaryRootBuffer);

        var fileHeaderBuffer = new byte[target.PageSize];
        target.ReadPage(0, null, fileHeaderBuffer);
        var fileHeader = PageHeader.ReadFrom(fileHeaderBuffer);
        fileHeader.DictionaryRootPageId = newRootPageId;
        fileHeader.WriteTo(fileHeaderBuffer);
        target.WritePageImmediate(0, fileHeaderBuffer);

        target._dictionaryCache.Clear();
        target._dictionaryReverseCache.Clear();
        target._dictionaryRootPageId = newRootPageId;
    }

    private static IReadOnlyList<uint> EnumerateDictionaryChainPages(StorageEngine target, uint rootPageId,
        CancellationToken ct)
    {
        var result = new List<uint>();
        if (rootPageId == 0)
            return result;

        var visited = new HashSet<uint>();
        var pageBuffer = new byte[target.PageSize];
        uint current = rootPageId;

        while (current != 0 && visited.Add(current))
        {
            ct.ThrowIfCancellationRequested();
            if (current >= target._pageFile.NextPageId)
                break;

            result.Add(current);
            target._pageFile.ReadPage(current, pageBuffer);
            var header = PageHeader.ReadFrom(pageBuffer);
            current = header.NextPageId;
        }

        return result;
    }

    private void ReloadDictionaryStateAfterCompactionSwap()
    {
        lock (_dictionaryLock)
        {
            _dictionaryCache.Clear();
            _dictionaryReverseCache.Clear();
            _dictionaryRootPageId = 0;
            _nextDictionaryId = DictionaryPage.ReservedValuesEnd + 1;
        }

        InitializeDictionary();
    }

    private bool TryReadCompactionMarker(string markerPath, out CompactionMarkerState? markerState)
    {
        markerState = null;
        if (!File.Exists(markerPath))
            return false;

        try
        {
            string json = File.ReadAllText(markerPath);
            markerState = JsonSerializer.Deserialize<CompactionMarkerState>(json, CompactionMarkerSerializerOptions);

            if (markerState == null)
                return false;

            if (markerState.Version <= 0)
                markerState.Version = 1;

            if (string.IsNullOrWhiteSpace(markerState.DatabasePath))
                markerState.DatabasePath = _pageFile.FilePath;

            if (string.IsNullOrWhiteSpace(markerState.TempDatabasePath))
                markerState.TempDatabasePath = GetCompactionTempPath();

            if (string.IsNullOrWhiteSpace(markerState.BackupDatabasePath))
                markerState.BackupDatabasePath = GetCompactionBackupPath();

            if (markerState.StartedAtUtc == default)
                markerState.StartedAtUtc = DateTimeOffset.UtcNow;

            if (string.IsNullOrWhiteSpace(markerState.Mode))
                markerState.Mode = "CopySwap";

            return true;
        }
        catch
        {
            markerState = new CompactionMarkerState
            {
                Version = 1,
                Phase = CompactionMarkerPhase.Started,
                DatabasePath = _pageFile.FilePath,
                TempDatabasePath = GetCompactionTempPath(),
                BackupDatabasePath = GetCompactionBackupPath(),
                StartedAtUtc = DateTimeOffset.UtcNow,
                LastUpdatedUtc = DateTimeOffset.UtcNow,
                OnlineMode = false,
                Mode = "CopySwap"
            };
            return true;
        }
    }

    private static void WriteCompactionMarker(string markerPath, CompactionMarkerState markerState)
    {
        markerState.LastUpdatedUtc = DateTimeOffset.UtcNow;
        string json = JsonSerializer.Serialize(markerState, CompactionMarkerSerializerOptions);

        string? directory = Path.GetDirectoryName(markerPath);
        if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory);

        using var stream = new FileStream(
            markerPath,
            FileMode.Create,
            FileAccess.Write,
            FileShare.None,
            4096,
            FileOptions.WriteThrough);

        using var writer = new StreamWriter(stream, new UTF8Encoding(false));
        writer.Write(json);
        writer.Flush();
        stream.Flush(true);
    }

    private static void DeleteCompactionMarkerIfExists(string markerPath)
    {
        if (File.Exists(markerPath)) File.Delete(markerPath);
    }

    private void EnsureNoActiveTransactions()
    {
        foreach (ulong transactionId in _activeTransactions.Keys)
            if (_walCache.TryGetValue(transactionId, out var pendingPages) && !pendingPages.IsEmpty)
                throw new InvalidOperationException("Compaction requires no active write transactions.");
    }

    private void TryRunStartupMaintenance()
    {
        if (!MaintenanceOptions.RunAtStartup)
            return;

        try
        {
            var snapshot = CaptureCompactionSnapshot();
            if (snapshot.FragmentationPercent < MaintenanceOptions.MinFragmentationPercent &&
                snapshot.TailReclaimablePages * _pageFile.PageSize < MaintenanceOptions.MinReclaimableBytes)
                return;

            var options = new CompactionOptions
            {
                OnlineMode = true,
                DefragmentSlottedPages = true,
                NormalizeFreeList = true,
                EnableTailTruncation = true,
                MinimumRetainedPages = 2,
                OnlineBatchPageLimit = Math.Max(1, MaintenanceOptions.OnlineThrottlePagesPerBatch),
                OnlineBatchDelay = MaintenanceOptions.OnlineThrottleDelay < TimeSpan.Zero
                    ? TimeSpan.Zero
                    : MaintenanceOptions.OnlineThrottleDelay,
                MaxOnlineDuration = MaintenanceOptions.MaxRunDuration <= TimeSpan.Zero
                    ? TimeSpan.FromSeconds(1)
                    : MaintenanceOptions.MaxRunDuration
            };

            _ = RunOnlineCompactionPass(options);
        }
        catch
        {
            // Startup maintenance is best-effort only.
        }
    }

    private enum CompactionMarkerPhase
    {
        Started = 1,
        Copied = 2,
        Swapped = 3,
        CleanupDone = 4
    }

    private sealed class CompactionMarkerState
    {
        /// <summary>
        ///     Gets or sets the compaction marker schema version.
        /// </summary>
        public int Version { get; set; } = 1;

        /// <summary>
        ///     Gets or sets the current compaction marker phase.
        /// </summary>
        public CompactionMarkerPhase Phase { get; set; }

        /// <summary>
        ///     Gets or sets the primary database path.
        /// </summary>
        public string DatabasePath { get; set; } = string.Empty;

        /// <summary>
        ///     Gets or sets the temporary database path.
        /// </summary>
        public string TempDatabasePath { get; set; } = string.Empty;

        /// <summary>
        ///     Gets or sets the backup database path.
        /// </summary>
        public string BackupDatabasePath { get; set; } = string.Empty;

        /// <summary>
        ///     Gets or sets the compaction start timestamp in UTC.
        /// </summary>
        public DateTimeOffset StartedAtUtc { get; set; }

        /// <summary>
        ///     Gets or sets the last marker update timestamp in UTC.
        /// </summary>
        public DateTimeOffset LastUpdatedUtc { get; set; }

        /// <summary>
        ///     Gets or sets a value indicating whether online mode was used.
        /// </summary>
        public bool OnlineMode { get; set; }

        /// <summary>
        ///     Gets or sets the compaction execution mode.
        /// </summary>
        public string Mode { get; set; } = "CopySwap";
    }

    private readonly struct CompactionSnapshot
    {
        /// <summary>
        ///     Initializes a new instance of the <see cref="CompactionSnapshot" /> struct.
        /// </summary>
        /// <param name="fileSizeBytes">The file size in bytes.</param>
        /// <param name="pageCount">The total page count.</param>
        /// <param name="freePageCount">The free page count.</param>
        /// <param name="totalFreeBytes">The total estimated free bytes.</param>
        /// <param name="fragmentationPercent">The estimated fragmentation percentage.</param>
        /// <param name="tailReclaimablePages">The reclaimable tail page count.</param>
        public CompactionSnapshot(
            long fileSizeBytes,
            uint pageCount,
            int freePageCount,
            long totalFreeBytes,
            double fragmentationPercent,
            uint tailReclaimablePages)
        {
            FileSizeBytes = fileSizeBytes;
            PageCount = pageCount;
            FreePageCount = freePageCount;
            TotalFreeBytes = totalFreeBytes;
            FragmentationPercent = fragmentationPercent;
            TailReclaimablePages = tailReclaimablePages;
        }

        /// <summary>
        ///     Gets the file size in bytes.
        /// </summary>
        public long FileSizeBytes { get; }

        /// <summary>
        ///     Gets the total page count.
        /// </summary>
        public uint PageCount { get; }

        /// <summary>
        ///     Gets the free page count.
        /// </summary>
        public int FreePageCount { get; }

        /// <summary>
        ///     Gets the estimated total free bytes.
        /// </summary>
        public long TotalFreeBytes { get; }

        /// <summary>
        ///     Gets the estimated fragmentation percentage.
        /// </summary>
        public double FragmentationPercent { get; }

        /// <summary>
        ///     Gets the number of reclaimable tail pages.
        /// </summary>
        public uint TailReclaimablePages { get; }
    }

    private struct CompactionWork
    {
        public int PagesScanned;
        public int PagesDefragmented;
        public long BytesReclaimedByDefragmentation;
        public int FreeListPagesNormalized;
        public long DocumentsRelocated;
        public int PagesRelocated;
    }

    private readonly struct CompactionCollectionRebuildResult
    {
        /// <summary>
        ///     Initializes a new instance of the <see cref="CompactionCollectionRebuildResult" /> struct.
        /// </summary>
        /// <param name="metadata">The rebuilt collection metadata.</param>
        /// <param name="documentsRelocated">The number of relocated documents.</param>
        /// <param name="relocatedSourcePageIds">The relocated source page identifiers.</param>
        public CompactionCollectionRebuildResult(
            CollectionMetadata metadata,
            long documentsRelocated,
            IReadOnlyCollection<uint> relocatedSourcePageIds)
        {
            Metadata = metadata;
            DocumentsRelocated = documentsRelocated;
            RelocatedSourcePageIds = relocatedSourcePageIds;
        }

        /// <summary>
        ///     Gets rebuilt collection metadata.
        /// </summary>
        public CollectionMetadata Metadata { get; }

        /// <summary>
        ///     Gets the number of relocated documents.
        /// </summary>
        public long DocumentsRelocated { get; }

        /// <summary>
        ///     Gets relocated source page identifiers.
        /// </summary>
        public IReadOnlyCollection<uint> RelocatedSourcePageIds { get; }
    }

    private sealed class CompactionDataWriterState
    {
        /// <summary>
        ///     Gets free space tracked by page identifier.
        /// </summary>
        public Dictionary<uint, ushort> FreeSpaceByPage { get; } = new();

        /// <summary>
        ///     Gets or sets the current data page identifier.
        /// </summary>
        public uint CurrentDataPageId { get; set; }
    }

    private readonly struct CompactionVectorNode
    {
        /// <summary>
        ///     Initializes a new instance of the <see cref="CompactionVectorNode" /> struct.
        /// </summary>
        /// <param name="pageId">The page identifier.</param>
        /// <param name="nodeIndex">The node index within the page.</param>
        /// <param name="location">The mapped document location.</param>
        /// <param name="vector">The vector payload.</param>
        public CompactionVectorNode(uint pageId, int nodeIndex, DocumentLocation location, float[] vector)
        {
            PageId = pageId;
            NodeIndex = nodeIndex;
            Location = location;
            Vector = vector;
        }

        /// <summary>
        ///     Gets the page identifier containing the vector node.
        /// </summary>
        public uint PageId { get; }

        /// <summary>
        ///     Gets the node index within the page.
        /// </summary>
        public int NodeIndex { get; }

        /// <summary>
        ///     Gets the document location associated with the node.
        /// </summary>
        public DocumentLocation Location { get; }

        /// <summary>
        ///     Gets the vector payload.
        /// </summary>
        public float[] Vector { get; }
    }
}