using System.IO.MemoryMappedFiles;

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

/// <summary>
///     Configuration for page-based storage
/// </summary>
public readonly struct PageFileConfig
{
    /// <summary>
    ///     Gets the size of each page in bytes.
    /// </summary>
    public int PageSize { get; init; }

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

    /// <summary>
    ///     Gets the memory-mapped file access mode.
    /// </summary>
    public MemoryMappedFileAccess Access { get; init; }

    /// <summary>
    ///     Small pages for embedded scenarios with many tiny documents
    /// </summary>
    public static PageFileConfig Small => new()
    {
        PageSize = 8192, // 8KB pages
        InitialFileSize = 1024 * 1024, // 1MB initial
        Access = MemoryMappedFileAccess.ReadWrite
    };

    /// <summary>
    ///     Default balanced configuration for document databases (16KB like MySQL InnoDB)
    /// </summary>
    public static PageFileConfig Default => new()
    {
        PageSize = 16384, // 16KB pages
        InitialFileSize = 2 * 1024 * 1024, // 2MB initial
        Access = MemoryMappedFileAccess.ReadWrite
    };

    /// <summary>
    ///     Large pages for databases with big documents (32KB like MongoDB WiredTiger)
    /// </summary>
    public static PageFileConfig Large => new()
    {
        PageSize = 32768, // 32KB pages
        InitialFileSize = 4 * 1024 * 1024, // 4MB initial
        Access = MemoryMappedFileAccess.ReadWrite
    };
}

/// <summary>
///     Page-based file storage with memory-mapped I/O.
///     Manages fixed-size pages for efficient storage and retrieval.
/// </summary>
public sealed class PageFile : IDisposable
{
    private readonly PageFileConfig _config;
    private readonly object _lock = new();
    private bool _disposed;
    private FileStream? _fileStream;
    private uint _firstFreePageId;
    private MemoryMappedFile? _mappedFile;

    /// <summary>
    ///     Initializes a new instance of the <see cref="PageFile" /> class.
    /// </summary>
    /// <param name="filePath">The file path for the page file.</param>
    /// <param name="config">The page file configuration.</param>
    public PageFile(string filePath, PageFileConfig config)
    {
        FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
        _config = config;
    }

    /// <summary>
    ///     Gets the next page identifier that will be allocated.
    /// </summary>
    public uint NextPageId { get; private set; }

    /// <summary>
    ///     Indicates whether this file was newly created on the current open call.
    /// </summary>
    public bool WasCreated { get; private set; }

    /// <summary>
    ///     Gets the configured page size in bytes.
    /// </summary>
    public int PageSize => _config.PageSize;

    /// <summary>
    ///     Gets the underlying file path.
    /// </summary>
    public string FilePath { get; }

    /// <summary>
    ///     Gets the current physical file length in bytes.
    /// </summary>
    public long FileLengthBytes
    {
        get
        {
            lock (_lock)
            {
                EnsureFileOpen();
                return _fileStream!.Length;
            }
        }
    }

    /// <summary>
    ///     Gets the effective page-file configuration.
    /// </summary>
    public PageFileConfig Config => _config;

    /// <summary>
    ///     Releases resources used by the page file.
    /// </summary>
    public void Dispose()
    {
        if (_disposed)
            return;

        lock (_lock)
        {
            // 1. Flush any pending writes from memory-mapped file
            if (_fileStream != null) _fileStream.Flush(true);

            // 2. Close memory-mapped file first
            _mappedFile?.Dispose();

            // 3. Then close file stream
            _fileStream?.Dispose();

            _disposed = true;
        }

        GC.SuppressFinalize(this);
    }

    /// <summary>
    ///     Opens the page file, creating it if it doesn't exist
    /// </summary>
    public void Open()
    {
        lock (_lock)
        {
            if (_fileStream != null)
                return; // Already open

            bool fileExists = File.Exists(FilePath);

            _fileStream = new FileStream(
                FilePath,
                FileMode.OpenOrCreate,
                FileAccess.ReadWrite,
                FileShare.None,
                4096,
                FileOptions.RandomAccess);

            WasCreated = !fileExists || _fileStream.Length == 0;
            if (WasCreated)
            {
                // Initialize new file with 2 pages (Header + Collection Metadata)
                _fileStream.SetLength(_config.InitialFileSize < _config.PageSize * 2
                    ? _config.PageSize * 2
                    : _config.InitialFileSize);
                InitializeHeader();
            }

            // Initialize next page ID based on file length
            NextPageId = (uint)(_fileStream.Length / _config.PageSize);

            _mappedFile = MemoryMappedFile.CreateFromFile(
                _fileStream,
                null,
                _fileStream.Length,
                _config.Access,
                HandleInheritability.None,
                true);

            // Read free list head from Page 0
            if (_fileStream.Length >= _config.PageSize)
            {
                var headerSpan = new byte[32]; // PageHeader.Size
                using var accessor = _mappedFile.CreateViewAccessor(0, 32, MemoryMappedFileAccess.Read);
                accessor.ReadArray(0, headerSpan, 0, 32);
                var header = PageHeader.ReadFrom(headerSpan);
                _firstFreePageId = header.NextPageId;
            }
        }
    }

    /// <summary>
    ///     Initializes the file header (page 0) and collection metadata (page 1)
    /// </summary>
    private void InitializeHeader()
    {
        // 1. Initialize Header (Page 0)
        var header = new PageHeader
        {
            PageId = 0,
            PageType = PageType.Header,
            FreeBytes = (ushort)(_config.PageSize - 32),
            NextPageId = 0, // No free pages initially
            TransactionId = 0,
            Checksum = 0
        };

        Span<byte> buffer = stackalloc byte[_config.PageSize];
        header.WriteTo(buffer);

        _fileStream!.Position = 0;
        _fileStream.Write(buffer);

        // 2. Initialize Collection Metadata (Page 1)
        // This page is reserved for storing index definitions
        buffer.Clear();
        var metaHeader = new SlottedPageHeader
        {
            PageId = 1,
            PageType = PageType.Collection,
            SlotCount = 0,
            FreeSpaceStart = SlottedPageHeader.Size,
            FreeSpaceEnd = (ushort)_config.PageSize,
            NextOverflowPage = 0,
            TransactionId = 0
        };
        metaHeader.WriteTo(buffer);

        _fileStream.Position = _config.PageSize;
        _fileStream.Write(buffer);

        _fileStream.Flush();
    }

    // ... (ReadPage / WritePage unchanged) ...
    /// <summary>
    ///     Reads a page by ID into the provided span
    /// </summary>
    /// <param name="pageId">The page identifier to read.</param>
    /// <param name="destination">The destination span that receives page bytes.</param>
    public void ReadPage(uint pageId, Span<byte> destination)
    {
        if (destination.Length < _config.PageSize)
            throw new ArgumentException($"Destination must be at least {_config.PageSize} bytes");

        if (_mappedFile == null)
            throw new InvalidOperationException("File not open");

        long offset = pageId * _config.PageSize;

        using var accessor = _mappedFile.CreateViewAccessor(offset, _config.PageSize, MemoryMappedFileAccess.Read);
        var temp = new byte[_config.PageSize];
        accessor.ReadArray(0, temp, 0, _config.PageSize);
        temp.CopyTo(destination);
    }

    /// <summary>
    ///     Writes a page at the specified ID from the provided span
    /// </summary>
    /// <param name="pageId">The page identifier to write.</param>
    /// <param name="source">The source span that contains page bytes.</param>
    public void WritePage(uint pageId, ReadOnlySpan<byte> source)
    {
        if (source.Length < _config.PageSize)
            throw new ArgumentException($"Source must be at least {_config.PageSize} bytes");

        if (_mappedFile == null)
            throw new InvalidOperationException("File not open");

        long offset = pageId * _config.PageSize;

        // Ensure file is large enough
        if (offset + _config.PageSize > _fileStream!.Length)
            lock (_lock)
            {
                if (offset + _config.PageSize > _fileStream.Length)
                {
                    long newSize = Math.Max(offset + _config.PageSize, _fileStream.Length * 2);
                    _fileStream.SetLength(newSize);

                    // Recreate memory-mapped file with new size
                    _mappedFile.Dispose();
                    _mappedFile = MemoryMappedFile.CreateFromFile(
                        _fileStream,
                        null,
                        _fileStream.Length,
                        _config.Access,
                        HandleInheritability.None,
                        true);
                }
            }

        // Write to memory-mapped file
        using (var accessor = _mappedFile.CreateViewAccessor(offset, _config.PageSize, MemoryMappedFileAccess.Write))
        {
            accessor.WriteArray(0, source.ToArray(), 0, _config.PageSize);
        }
    }

    /// <summary>
    ///     Allocates a new page (reuses free page if available) and returns its ID
    /// </summary>
    public uint AllocatePage()
    {
        lock (_lock)
        {
            if (_fileStream == null)
                throw new InvalidOperationException("File not open");

            // 1. Try to reuse a free page
            if (_firstFreePageId != 0)
            {
                uint recycledPageId = _firstFreePageId;

                // Read the recycled page to update the free list head
                var buffer = new byte[_config.PageSize];
                ReadPage(recycledPageId, buffer);
                var header = PageHeader.ReadFrom(buffer);

                // The new head is what the recycled page pointed to
                _firstFreePageId = header.NextPageId;

                // Update file header (Page 0) to point to new head
                UpdateFileHeaderFreePtr(_firstFreePageId);

                return recycledPageId;
            }

            // 2. No free pages, append new one
            uint pageId = NextPageId++;

            // Extend file if necessary
            long requiredLength = (pageId + 1) * _config.PageSize;
            if (requiredLength > _fileStream.Length)
            {
                long newSize = Math.Max(requiredLength, _fileStream.Length * 2);
                _fileStream.SetLength(newSize);

                // Recreate memory-mapped file with new size
                _mappedFile?.Dispose();
                _mappedFile = MemoryMappedFile.CreateFromFile(
                    _fileStream,
                    null,
                    _fileStream.Length,
                    _config.Access,
                    HandleInheritability.None,
                    true);
            }

            return pageId;
        }
    }

    /// <summary>
    ///     Marks a page as free and adds it to the free list
    /// </summary>
    /// <param name="pageId">The page identifier to mark as free.</param>
    public void FreePage(uint pageId)
    {
        lock (_lock)
        {
            if (_fileStream == null) throw new InvalidOperationException("File not open");
            if (pageId == 0) throw new InvalidOperationException("Cannot free header page 0");

            // 1. Create a free page header pointing to current head
            var header = new PageHeader
            {
                PageId = pageId,
                PageType = PageType.Free,
                NextPageId = _firstFreePageId, // Point to previous head
                TransactionId = 0,
                Checksum = 0
            };

            var buffer = new byte[_config.PageSize];
            header.WriteTo(buffer);

            // 2. Write the freed page
            WritePage(pageId, buffer);

            // 3. Update head to point to this page
            _firstFreePageId = pageId;

            // 4. Update file header (Page 0)
            UpdateFileHeaderFreePtr(_firstFreePageId);
        }
    }

    private void UpdateFileHeaderFreePtr(uint newHead)
    {
        // Read Page 0
        var buffer = new byte[_config.PageSize];
        ReadPage(0, buffer);
        var header = PageHeader.ReadFrom(buffer);

        // Update NextPageId (which we use as FirstFreePageId)
        header.NextPageId = newHead;

        // Write back
        header.WriteTo(buffer);
        WritePage(0, buffer);
    }

    /// <summary>
    ///     Reads bytes from the page 0 extension region (immediately after the 32-byte file header).
    /// </summary>
    /// <param name="extensionOffset">Offset into the extension region, relative to byte 32.</param>
    /// <param name="destination">Destination span receiving bytes.</param>
    public void ReadPageZeroExtension(int extensionOffset, Span<byte> destination)
    {
        if (extensionOffset < 0)
            throw new ArgumentOutOfRangeException(nameof(extensionOffset));

        if (destination.Length == 0)
            return;

        if (_mappedFile == null)
            throw new InvalidOperationException("File not open");

        int absoluteOffset = 32 + extensionOffset;
        if (absoluteOffset + destination.Length > _config.PageSize)
            throw new ArgumentOutOfRangeException(nameof(destination),
                "Requested range exceeds page 0 extension region.");

        using var accessor =
            _mappedFile.CreateViewAccessor(absoluteOffset, destination.Length, MemoryMappedFileAccess.Read);
        var temp = new byte[destination.Length];
        accessor.ReadArray(0, temp, 0, temp.Length);
        temp.CopyTo(destination);
    }

    /// <summary>
    ///     Writes bytes to the page 0 extension region (immediately after the 32-byte file header).
    /// </summary>
    /// <param name="extensionOffset">Offset into the extension region, relative to byte 32.</param>
    /// <param name="source">Source bytes to write.</param>
    public void WritePageZeroExtension(int extensionOffset, ReadOnlySpan<byte> source)
    {
        if (extensionOffset < 0)
            throw new ArgumentOutOfRangeException(nameof(extensionOffset));

        if (source.Length == 0)
            return;

        if (_mappedFile == null)
            throw new InvalidOperationException("File not open");

        int absoluteOffset = 32 + extensionOffset;
        if (absoluteOffset + source.Length > _config.PageSize)
            throw new ArgumentOutOfRangeException(nameof(source), "Requested range exceeds page 0 extension region.");

        using var accessor =
            _mappedFile.CreateViewAccessor(absoluteOffset, source.Length, MemoryMappedFileAccess.Write);
        accessor.WriteArray(0, source.ToArray(), 0, source.Length);
    }

    /// <summary>
    ///     Flushes all pending writes to disk.
    ///     Called by CheckpointManager after applying WAL changes.
    /// </summary>
    public void Flush()
    {
        lock (_lock)
        {
            _fileStream?.Flush(true);
        }
    }

    /// <summary>
    ///     Writes a durable snapshot of the currently opened page file to a separate path.
    /// </summary>
    /// <param name="destinationPath">Destination file path for the snapshot.</param>
    public void SnapshotToFile(string destinationPath)
    {
        if (string.IsNullOrWhiteSpace(destinationPath))
            throw new ArgumentNullException(nameof(destinationPath));

        lock (_lock)
        {
            EnsureFileOpen();

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

            _fileStream!.Flush(true);
            long originalPosition = _fileStream.Position;
            try
            {
                _fileStream.Position = 0;
                using var destination = new FileStream(
                    destinationPath,
                    FileMode.Create,
                    FileAccess.Write,
                    FileShare.None,
                    128 * 1024,
                    FileOptions.SequentialScan | FileOptions.WriteThrough);
                _fileStream.CopyTo(destination);
                destination.Flush(true);
            }
            finally
            {
                _fileStream.Position = originalPosition;
            }
        }
    }

    /// <summary>
    ///     Replaces the current file bytes with bytes from a source file and remaps the memory-mapped view.
    /// </summary>
    /// <param name="sourcePath">The source file path used as replacement content.</param>
    public void ReplaceFromFile(string sourcePath)
    {
        if (string.IsNullOrWhiteSpace(sourcePath))
            throw new ArgumentNullException(nameof(sourcePath));
        if (!File.Exists(sourcePath))
            throw new FileNotFoundException("Replacement source file was not found.", sourcePath);

        lock (_lock)
        {
            EnsureFileOpen();

            using var source = new FileStream(
                sourcePath,
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                128 * 1024,
                FileOptions.SequentialScan);

            if (source.Length <= 0 || source.Length % _config.PageSize != 0)
                throw new InvalidDataException(
                    $"Replacement file length must be a positive multiple of page size ({_config.PageSize}).");

            _mappedFile?.Dispose();
            _mappedFile = null;

            _fileStream!.SetLength(source.Length);
            _fileStream.Position = 0;
            source.CopyTo(_fileStream);
            _fileStream.Flush(true);

            _mappedFile = MemoryMappedFile.CreateFromFile(
                _fileStream,
                null,
                _fileStream.Length,
                _config.Access,
                HandleInheritability.None,
                true);

            NextPageId = (uint)(_fileStream.Length / _config.PageSize);
            _firstFreePageId = 0;

            if (_fileStream.Length >= _config.PageSize)
            {
                const int pageHeaderSizeBytes = 32;
                var headerSpan = new byte[pageHeaderSizeBytes];
                using var accessor =
                    _mappedFile.CreateViewAccessor(0, pageHeaderSizeBytes, MemoryMappedFileAccess.Read);
                accessor.ReadArray(0, headerSpan, 0, pageHeaderSizeBytes);
                var header = PageHeader.ReadFrom(headerSpan);
                _firstFreePageId = header.NextPageId;
            }
        }
    }

    /// <summary>
    ///     Enumerates all free pages, combining explicit free-list entries and reclaimable empty pages.
    /// </summary>
    /// <param name="includeEmptyPages">If set to <see langword="true" />, includes all-zero pages as reclaimable.</param>
    /// <returns>A sorted list of free page identifiers.</returns>
    public IReadOnlyList<uint> EnumerateFreePages(bool includeEmptyPages = true)
    {
        lock (_lock)
        {
            EnsureFileOpen();
            var freePages = CollectFreePageIds(includeEmptyPages);
            return freePages;
        }
    }

    /// <summary>
    ///     Normalizes the free-list by rebuilding it from a deterministic sorted free page set.
    /// </summary>
    /// <param name="includeEmptyPages">
    ///     If set to <see langword="true" />, all-zero pages are converted into explicit free-list
    ///     pages.
    /// </param>
    /// <returns>The number of pages in the normalized free-list.</returns>
    public int NormalizeFreeList(bool includeEmptyPages = true)
    {
        lock (_lock)
        {
            EnsureFileOpen();
            var freePages = CollectFreePageIds(includeEmptyPages);
            RebuildFreeList(freePages);
            return freePages.Count;
        }
    }

    /// <summary>
    ///     Truncates contiguous reclaimable pages at the end of the file.
    ///     Reclaimable tail pages include explicit free pages and truly empty pages.
    /// </summary>
    /// <param name="minimumPageCount">
    ///     Minimum number of pages that must remain after truncation (defaults to header + metadata
    ///     pages).
    /// </param>
    /// <returns>Details about the truncation operation.</returns>
    public TailTruncationResult TruncateReclaimableTailPages(uint minimumPageCount = 2)
    {
        lock (_lock)
        {
            EnsureFileOpen();

            if (NextPageId <= minimumPageCount) return TailTruncationResult.None(NextPageId);

            var freePages = new HashSet<uint>(CollectFreePageIds(true));
            uint originalPageCount = NextPageId;
            uint newPageCount = NextPageId;
            var pageBuffer = new byte[_config.PageSize];

            while (newPageCount > minimumPageCount)
            {
                uint candidatePageId = newPageCount - 1;
                if (!IsReclaimableTailPage(candidatePageId, freePages, pageBuffer)) break;

                newPageCount--;
            }

            if (newPageCount == originalPageCount) return TailTruncationResult.None(originalPageCount);

            freePages.RemoveWhere(pageId => pageId >= newPageCount);
            var remainingFreePages = freePages.ToList();
            remainingFreePages.Sort();
            RebuildFreeList(remainingFreePages);

            _mappedFile?.Dispose();
            _mappedFile = null;

            long previousLengthBytes = _fileStream!.Length;
            long newLengthBytes = newPageCount * _config.PageSize;
            _fileStream.SetLength(newLengthBytes);
            _fileStream.Flush(true);

            _mappedFile = MemoryMappedFile.CreateFromFile(
                _fileStream,
                null,
                newLengthBytes,
                _config.Access,
                HandleInheritability.None,
                true);

            NextPageId = newPageCount;

            return new TailTruncationResult(
                originalPageCount,
                newPageCount,
                originalPageCount - newPageCount,
                previousLengthBytes - newLengthBytes);
        }
    }

    /// <summary>
    ///     Trims excess physical file capacity beyond the current logical page count.
    /// </summary>
    /// <returns>The number of bytes removed from the file.</returns>
    public long TrimExcessCapacityToLogicalPageCount()
    {
        lock (_lock)
        {
            EnsureFileOpen();

            long targetLengthBytes = NextPageId * _config.PageSize;
            long currentLengthBytes = _fileStream!.Length;
            if (currentLengthBytes <= targetLengthBytes)
                return 0;

            _mappedFile?.Dispose();
            _mappedFile = null;

            _fileStream.SetLength(targetLengthBytes);
            _fileStream.Flush(true);

            _mappedFile = MemoryMappedFile.CreateFromFile(
                _fileStream,
                null,
                targetLengthBytes,
                _config.Access,
                HandleInheritability.None,
                true);

            return currentLengthBytes - targetLengthBytes;
        }
    }

    /// <summary>
    ///     Defragments a slotted page in place by packing live slot payloads densely at the end of the page.
    /// </summary>
    /// <param name="pageId">The page identifier to defragment.</param>
    /// <param name="reclaimedBytes">The number of free bytes reclaimed by defragmentation.</param>
    /// <returns><see langword="true" /> when the page layout changed; otherwise, <see langword="false" />.</returns>
    public bool DefragmentSlottedPage(uint pageId, out int reclaimedBytes)
    {
        var result = DefragmentSlottedPageWithStats(pageId);
        reclaimedBytes = result.ReclaimedBytes;
        return result.Changed;
    }

    /// <summary>
    ///     Defragments a slotted page in place and returns detailed relocation stats.
    /// </summary>
    /// <param name="pageId">The page identifier to defragment.</param>
    public SlottedPageDefragmentationResult DefragmentSlottedPageWithStats(uint pageId)
    {
        lock (_lock)
        {
            EnsureFileOpen();

            if (pageId >= NextPageId)
                throw new ArgumentOutOfRangeException(nameof(pageId));

            var pageBuffer = new byte[_config.PageSize];
            ReadPage(pageId, pageBuffer);

            var result = TryDefragmentSlottedPageWithStats(pageBuffer);
            if (!result.Changed)
                return result;

            WritePage(pageId, pageBuffer);
            return result;
        }
    }

    /// <summary>
    ///     Defragments a slotted-page buffer in memory by rewriting live slots densely.
    /// </summary>
    /// <param name="pageBuffer">The page buffer to compact in place.</param>
    /// <param name="reclaimedBytes">The number of free bytes reclaimed by compaction.</param>
    /// <returns><see langword="true" /> when compaction modified the page; otherwise, <see langword="false" />.</returns>
    public static bool TryDefragmentSlottedPage(Span<byte> pageBuffer, out int reclaimedBytes)
    {
        var result = TryDefragmentSlottedPageWithStats(pageBuffer);
        reclaimedBytes = result.ReclaimedBytes;
        return result.Changed;
    }

    /// <summary>
    ///     Defragments a slotted-page buffer in memory and returns detailed relocation stats.
    /// </summary>
    /// <param name="pageBuffer">The page buffer to compact in place.</param>
    public static SlottedPageDefragmentationResult TryDefragmentSlottedPageWithStats(Span<byte> pageBuffer)
    {
        if (pageBuffer.Length < SlottedPageHeader.Size)
            return SlottedPageDefragmentationResult.None;

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

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

        var activeSlots = new List<(ushort SlotIndex, SlotEntry Slot, byte[] Data)>(header.SlotCount);

        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 || slot.Length == 0)
                continue;

            int dataEnd = slot.Offset + slot.Length;
            if (slot.Offset < slotArrayEnd || dataEnd > pageBuffer.Length)
                return SlottedPageDefragmentationResult.None;

            byte[] slotData = pageBuffer.Slice(slot.Offset, slot.Length).ToArray();
            activeSlots.Add((i, slot, slotData));
        }

        var newFreeSpaceStart = (ushort)slotArrayEnd;
        int writeCursor = pageBuffer.Length;
        var changed = false;
        var relocatedSlots = 0;
        int oldFreeBytes = header.AvailableFreeSpace;

        for (var i = 0; i < activeSlots.Count; i++)
        {
            (ushort slotIndex, var slot, byte[] slotData) = activeSlots[i];
            writeCursor -= slotData.Length;
            if (writeCursor < newFreeSpaceStart)
                return SlottedPageDefragmentationResult.None;

            slotData.CopyTo(pageBuffer.Slice(writeCursor, slotData.Length));

            if (slot.Offset != writeCursor)
            {
                slot.Offset = (ushort)writeCursor;
                relocatedSlots++;
                changed = true;
            }

            int slotOffset = SlottedPageHeader.Size + slotIndex * SlotEntry.Size;
            slot.WriteTo(pageBuffer.Slice(slotOffset, SlotEntry.Size));
        }

        if (writeCursor > newFreeSpaceStart)
            pageBuffer.Slice(newFreeSpaceStart, writeCursor - newFreeSpaceStart).Clear();

        if (header.FreeSpaceStart != newFreeSpaceStart || header.FreeSpaceEnd != writeCursor) changed = true;

        header.FreeSpaceStart = newFreeSpaceStart;
        header.FreeSpaceEnd = (ushort)writeCursor;
        header.WriteTo(pageBuffer);

        int newFreeBytes = header.AvailableFreeSpace;
        int reclaimedBytes = Math.Max(0, newFreeBytes - oldFreeBytes);
        return new SlottedPageDefragmentationResult(changed, reclaimedBytes, relocatedSlots);
    }

    private void EnsureFileOpen()
    {
        if (_fileStream == null || _mappedFile == null)
            throw new InvalidOperationException("File not open");
    }

    private List<uint> CollectFreePageIds(bool includeEmptyPages)
    {
        var freePages = new HashSet<uint>();
        if (NextPageId <= 2)
            return [];

        var pageBuffer = new byte[_config.PageSize];

        var seen = new HashSet<uint>();
        uint current = _firstFreePageId;
        while (current != 0 && current < NextPageId && seen.Add(current))
        {
            if (current > 1) freePages.Add(current);

            ReadPage(current, pageBuffer);
            var header = PageHeader.ReadFrom(pageBuffer);
            current = header.NextPageId;
        }

        for (uint pageId = 2; pageId < NextPageId; pageId++)
        {
            ReadPage(pageId, pageBuffer);
            var header = PageHeader.ReadFrom(pageBuffer);

            if (header.PageType is PageType.Free or PageType.FreeList)
            {
                freePages.Add(pageId);
                continue;
            }

            if (includeEmptyPages && IsTrulyEmptyPage(pageBuffer)) freePages.Add(pageId);
        }

        var ordered = freePages.ToList();
        ordered.Sort();
        return ordered;
    }

    private void RebuildFreeList(IReadOnlyList<uint> sortedFreePageIds)
    {
        if (sortedFreePageIds.Count == 0)
        {
            _firstFreePageId = 0;
            UpdateFileHeaderFreePtr(0);
            return;
        }

        var pageBuffer = new byte[_config.PageSize];

        for (var i = 0; i < sortedFreePageIds.Count; i++)
        {
            uint pageId = sortedFreePageIds[i];
            uint nextPageId = i + 1 < sortedFreePageIds.Count ? sortedFreePageIds[i + 1] : 0;

            Array.Clear(pageBuffer, 0, pageBuffer.Length);
            var freeHeader = new PageHeader
            {
                PageId = pageId,
                PageType = PageType.Free,
                NextPageId = nextPageId,
                TransactionId = 0,
                Checksum = 0
            };

            freeHeader.WriteTo(pageBuffer);
            WritePage(pageId, pageBuffer);
        }

        _firstFreePageId = sortedFreePageIds[0];
        UpdateFileHeaderFreePtr(_firstFreePageId);
    }

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

    private bool IsReclaimableTailPage(uint pageId, HashSet<uint> explicitFreePages, byte[] pageBuffer)
    {
        if (pageId <= 1 || pageId >= NextPageId)
            return false;

        if (explicitFreePages.Contains(pageId))
            return true;

        ReadPage(pageId, pageBuffer);
        var header = PageHeader.ReadFrom(pageBuffer);
        if (header.PageType is PageType.Free or PageType.FreeList)
            return true;

        return IsTrulyEmptyPage(pageBuffer);
    }

    private static bool IsTrulyEmptyPage(ReadOnlySpan<byte> pageBuffer)
    {
        for (var i = 0; i < pageBuffer.Length; i++)
            if (pageBuffer[i] != 0)
                return false;

        return true;
    }
}

/// <summary>
///     Detailed result from slotted-page defragmentation.
/// </summary>
public readonly struct SlottedPageDefragmentationResult
{
    /// <summary>
    ///     No-op result for non-slotted or invalid buffers.
    /// </summary>
    public static SlottedPageDefragmentationResult None => new(false, 0, 0);

    /// <summary>
    ///     Initializes a new instance of the <see cref="SlottedPageDefragmentationResult" /> struct.
    /// </summary>
    /// <param name="changed">Indicates whether the page layout changed.</param>
    /// <param name="reclaimedBytes">The number of bytes reclaimed.</param>
    /// <param name="relocatedSlotCount">The number of slots relocated during defragmentation.</param>
    public SlottedPageDefragmentationResult(bool changed, int reclaimedBytes, int relocatedSlotCount)
    {
        Changed = changed;
        ReclaimedBytes = reclaimedBytes;
        RelocatedSlotCount = relocatedSlotCount;
    }

    /// <summary>
    ///     Gets a value indicating whether the page layout changed.
    /// </summary>
    public bool Changed { get; }

    /// <summary>
    ///     Gets reclaimed free bytes after defragmentation.
    /// </summary>
    public int ReclaimedBytes { get; }

    /// <summary>
    ///     Gets the number of live slots that were moved to a new offset.
    /// </summary>
    public int RelocatedSlotCount { get; }
}

/// <summary>
///     Result of a reclaimable tail truncation operation.
/// </summary>
public readonly struct TailTruncationResult
{
    /// <summary>
    ///     Initializes a new instance of the <see cref="TailTruncationResult" /> struct.
    /// </summary>
    /// <param name="prePageCount">The page count before truncation.</param>
    /// <param name="postPageCount">The page count after truncation.</param>
    /// <param name="truncatedPages">The number of truncated pages.</param>
    /// <param name="truncatedBytes">The number of truncated bytes.</param>
    public TailTruncationResult(uint prePageCount, uint postPageCount, uint truncatedPages, long truncatedBytes)
    {
        PrePageCount = prePageCount;
        PostPageCount = postPageCount;
        TruncatedPages = truncatedPages;
        TruncatedBytes = truncatedBytes;
    }

    /// <summary>
    ///     Gets the page count before truncation.
    /// </summary>
    public uint PrePageCount { get; }

    /// <summary>
    ///     Gets the page count after truncation.
    /// </summary>
    public uint PostPageCount { get; }

    /// <summary>
    ///     Gets the number of truncated pages.
    /// </summary>
    public uint TruncatedPages { get; }

    /// <summary>
    ///     Gets the number of truncated bytes.
    /// </summary>
    public long TruncatedBytes { get; }

    /// <summary>
    ///     Creates a no-op truncation result.
    /// </summary>
    /// <param name="pageCount">The page count to assign before and after truncation.</param>
    public static TailTruncationResult None(uint pageCount)
    {
        return new TailTruncationResult(pageCount, pageCount, 0, 0);
    }
}