using System.Buffers.Binary;
using System.IO.Compression;
using ZB.MOM.WW.CBDD.Core.Compression;

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

/// <summary>
/// Options controlling compression migration.
/// </summary>
public sealed class CompressionMigrationOptions
{
    /// <summary>
    /// Enables dry-run estimation without mutating database contents.
    /// </summary>
    public bool DryRun { get; init; } = true;

    /// <summary>
    /// Target codec for migrated payloads.
    /// </summary>
    public CompressionCodec Codec { get; init; } = CompressionCodec.Brotli;

    /// <summary>
    /// Target compression level.
    /// </summary>
    public CompressionLevel Level { get; init; } = CompressionLevel.Fastest;

    /// <summary>
    /// Minimum logical payload size required before compression is attempted.
    /// </summary>
    public int MinSizeBytes { get; init; } = 1024;

    /// <summary>
    /// Minimum savings percent required to keep compressed output.
    /// </summary>
    public int MinSavingsPercent { get; init; } = 10;

    /// <summary>
    /// Optional include-only collection list (case-insensitive).
    /// </summary>
    public IReadOnlyList<string>? IncludeCollections { get; init; }

    /// <summary>
    /// Optional exclusion collection list (case-insensitive).
    /// </summary>
    public IReadOnlyList<string>? ExcludeCollections { get; init; }
}

/// <summary>
/// Result of a compression migration run.
/// </summary>
public sealed class CompressionMigrationResult
{
    public bool DryRun { get; init; }
    public CompressionCodec Codec { get; init; }
    public CompressionLevel Level { get; init; }
    public int CollectionsProcessed { get; init; }
    public long DocumentsScanned { get; init; }
    public long DocumentsRewritten { get; init; }
    public long DocumentsSkipped { get; init; }
    public long BytesBefore { get; init; }
    public long BytesEstimatedAfter { get; init; }
    public long BytesActualAfter { get; init; }
}

public sealed partial class StorageEngine
{
    /// <summary>
    /// Estimates or applies a one-time compression migration.
    /// </summary>
    public CompressionMigrationResult MigrateCompression(CompressionMigrationOptions? options = null)
    {
        return MigrateCompressionAsync(options).GetAwaiter().GetResult();
    }

    /// <summary>
    /// Estimates or applies a one-time compression migration.
    /// </summary>
    public async Task<CompressionMigrationResult> MigrateCompressionAsync(CompressionMigrationOptions? options = null, CancellationToken ct = default)
    {
        var normalized = NormalizeMigrationOptions(options);

        await _maintenanceGate.WaitAsync(ct);
        try
        {
            await _commitLock.WaitAsync(ct);
            try
            {
                EnsureNoActiveTransactions();
                CheckpointInternal();

                var collections = ResolveMigrationCollections(normalized);

                long docsScanned = 0;
                long docsRewritten = 0;
                long docsSkipped = 0;
                long bytesBefore = 0;
                long bytesEstimatedAfter = 0;
                long bytesActualAfter = 0;

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

                    foreach (var location in EnumeratePrimaryLocations(metadata))
                    {
                        ct.ThrowIfCancellationRequested();

                        if (!TryReadStoredPayload(location, out var storedPayload, out var isCompressed))
                        {
                            docsSkipped++;
                            continue;
                        }

                        if (!TryGetLogicalPayload(storedPayload, isCompressed, out var logicalPayload))
                        {
                            docsSkipped++;
                            continue;
                        }

                        docsScanned++;
                        bytesBefore += logicalPayload.Length;

                        var targetStored = BuildTargetStoredPayload(logicalPayload, normalized, out var targetCompressed);
                        bytesEstimatedAfter += targetStored.Length;

                        if (normalized.DryRun)
                        {
                            continue;
                        }

                        if (!TryRewriteStoredPayloadAtLocation(location, targetStored, targetCompressed, out var actualStoredBytes))
                        {
                            docsSkipped++;
                            continue;
                        }

                        docsRewritten++;
                        bytesActualAfter += actualStoredBytes;
                    }
                }

                if (!normalized.DryRun)
                {
                    var metadata = StorageFormatMetadata.Present(
                        version: 1,
                        featureFlags: StorageFeatureFlags.CompressionCapability,
                        defaultCodec: normalized.Codec);
                    WriteStorageFormatMetadata(metadata);
                    _pageFile.Flush();
                }

                return new CompressionMigrationResult
                {
                    DryRun = normalized.DryRun,
                    Codec = normalized.Codec,
                    Level = normalized.Level,
                    CollectionsProcessed = collections.Count,
                    DocumentsScanned = docsScanned,
                    DocumentsRewritten = docsRewritten,
                    DocumentsSkipped = docsSkipped,
                    BytesBefore = bytesBefore,
                    BytesEstimatedAfter = bytesEstimatedAfter,
                    BytesActualAfter = normalized.DryRun ? 0 : bytesActualAfter
                };
            }
            finally
            {
                _commitLock.Release();
            }
        }
        finally
        {
            _maintenanceGate.Release();
        }
    }

    private static CompressionMigrationOptions NormalizeMigrationOptions(CompressionMigrationOptions? options)
    {
        var normalized = options ?? new CompressionMigrationOptions();

        if (!Enum.IsDefined(normalized.Codec) || normalized.Codec == CompressionCodec.None)
            throw new ArgumentOutOfRangeException(nameof(options), "Migration codec must be a supported non-None codec.");

        if (normalized.MinSizeBytes < 0)
            throw new ArgumentOutOfRangeException(nameof(options), "MinSizeBytes must be non-negative.");

        if (normalized.MinSavingsPercent is < 0 or > 100)
            throw new ArgumentOutOfRangeException(nameof(options), "MinSavingsPercent must be between 0 and 100.");

        return normalized;
    }

    private List<CollectionMetadata> ResolveMigrationCollections(CompressionMigrationOptions options)
    {
        var collections = GetAllCollectionMetadata();

        var includes = options.IncludeCollections is { Count: > 0 }
            ? new HashSet<string>(options.IncludeCollections, StringComparer.OrdinalIgnoreCase)
            : null;

        var excludes = options.ExcludeCollections is { Count: > 0 }
            ? new HashSet<string>(options.ExcludeCollections, StringComparer.OrdinalIgnoreCase)
            : null;

        return collections
            .Where(c => includes == null || includes.Contains(c.Name))
            .Where(c => excludes == null || !excludes.Contains(c.Name))
            .ToList();
    }

    private byte[] BuildTargetStoredPayload(ReadOnlySpan<byte> logicalPayload, CompressionMigrationOptions options, out bool compressed)
    {
        compressed = false;

        if (logicalPayload.Length < options.MinSizeBytes)
            return logicalPayload.ToArray();

        try
        {
            var compressedPayload = _compressionService.Compress(logicalPayload, options.Codec, options.Level);
            var storedLength = CompressedPayloadHeader.Size + compressedPayload.Length;
            var savings = logicalPayload.Length - storedLength;
            var savingsPercent = logicalPayload.Length == 0 ? 0 : (int)((savings * 100L) / logicalPayload.Length);
            if (savings <= 0 || savingsPercent < options.MinSavingsPercent)
                return logicalPayload.ToArray();

            var output = new byte[storedLength];
            var header = CompressedPayloadHeader.Create(options.Codec, logicalPayload.Length, compressedPayload);
            header.WriteTo(output.AsSpan(0, CompressedPayloadHeader.Size));
            compressedPayload.CopyTo(output.AsSpan(CompressedPayloadHeader.Size));
            compressed = true;
            return output;
        }
        catch
        {
            return logicalPayload.ToArray();
        }
    }

    private bool TryGetLogicalPayload(ReadOnlySpan<byte> storedPayload, bool isCompressed, out byte[] logicalPayload)
    {
        logicalPayload = Array.Empty<byte>();

        if (!isCompressed)
        {
            logicalPayload = storedPayload.ToArray();
            return true;
        }

        if (storedPayload.Length < CompressedPayloadHeader.Size)
            return false;

        var header = CompressedPayloadHeader.ReadFrom(storedPayload.Slice(0, CompressedPayloadHeader.Size));
        if (!Enum.IsDefined(header.Codec) || header.Codec == CompressionCodec.None)
            return false;

        if (header.OriginalLength < 0 || header.CompressedLength < 0)
            return false;

        if (CompressedPayloadHeader.Size + header.CompressedLength > storedPayload.Length)
            return false;

        var compressedPayload = storedPayload.Slice(CompressedPayloadHeader.Size, header.CompressedLength);
        if (!header.ValidateChecksum(compressedPayload))
            return false;

        try
        {
            logicalPayload = _compressionService.Decompress(
                compressedPayload,
                header.Codec,
                header.OriginalLength,
                Math.Max(header.OriginalLength, _compressionOptions.MaxDecompressedSizeBytes));
            return true;
        }
        catch
        {
            return false;
        }
    }

    private bool TryReadStoredPayload(in DocumentLocation location, out byte[] storedPayload, out bool isCompressed)
    {
        storedPayload = Array.Empty<byte>();
        isCompressed = false;

        if (location.PageId >= _pageFile.NextPageId)
            return false;

        var pageBuffer = new byte[_pageFile.PageSize];
        _pageFile.ReadPage(location.PageId, pageBuffer);

        var header = SlottedPageHeader.ReadFrom(pageBuffer);
        if (location.SlotIndex >= header.SlotCount)
            return false;

        var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size);
        var slot = SlotEntry.ReadFrom(pageBuffer.AsSpan(slotOffset, SlotEntry.Size));
        if ((slot.Flags & SlotFlags.Deleted) != 0)
            return false;

        isCompressed = (slot.Flags & SlotFlags.Compressed) != 0;
        var hasOverflow = (slot.Flags & SlotFlags.HasOverflow) != 0;

        if (!hasOverflow)
        {
            storedPayload = pageBuffer.AsSpan(slot.Offset, slot.Length).ToArray();
            return true;
        }

        if (slot.Length < 8)
            return false;

        var primaryPayload = pageBuffer.AsSpan(slot.Offset, slot.Length);
        var totalStoredLength = BinaryPrimitives.ReadInt32LittleEndian(primaryPayload.Slice(0, 4));
        var nextOverflow = BinaryPrimitives.ReadUInt32LittleEndian(primaryPayload.Slice(4, 4));
        if (totalStoredLength < 0)
            return false;

        var output = new byte[totalStoredLength];
        var primaryChunk = primaryPayload.Slice(8);
        var copied = Math.Min(primaryChunk.Length, output.Length);
        primaryChunk.Slice(0, copied).CopyTo(output);

        var overflowBuffer = new byte[_pageFile.PageSize];
        while (copied < output.Length && nextOverflow != 0 && nextOverflow < _pageFile.NextPageId)
        {
            _pageFile.ReadPage(nextOverflow, overflowBuffer);
            var overflowHeader = SlottedPageHeader.ReadFrom(overflowBuffer);
            if (overflowHeader.PageType != PageType.Overflow)
                return false;

            var chunk = Math.Min(output.Length - copied, _pageFile.PageSize - SlottedPageHeader.Size);
            overflowBuffer.AsSpan(SlottedPageHeader.Size, chunk).CopyTo(output.AsSpan(copied));
            copied += chunk;
            nextOverflow = overflowHeader.NextOverflowPage;
        }

        if (copied != output.Length)
            return false;

        storedPayload = output;
        return true;
    }

    private bool TryRewriteStoredPayloadAtLocation(
        in DocumentLocation location,
        ReadOnlySpan<byte> newStoredPayload,
        bool compressed,
        out int actualStoredBytes)
    {
        actualStoredBytes = 0;

        if (location.PageId >= _pageFile.NextPageId)
            return false;

        var pageBuffer = new byte[_pageFile.PageSize];
        _pageFile.ReadPage(location.PageId, pageBuffer);

        var pageHeader = SlottedPageHeader.ReadFrom(pageBuffer);
        if (location.SlotIndex >= pageHeader.SlotCount)
            return false;

        var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size);
        var slot = SlotEntry.ReadFrom(pageBuffer.AsSpan(slotOffset, SlotEntry.Size));
        if ((slot.Flags & SlotFlags.Deleted) != 0)
            return false;

        var oldHasOverflow = (slot.Flags & SlotFlags.HasOverflow) != 0;
        uint oldOverflowHead = 0;
        if (oldHasOverflow)
        {
            if (slot.Length < 8)
                return false;

            oldOverflowHead = BinaryPrimitives.ReadUInt32LittleEndian(pageBuffer.AsSpan(slot.Offset + 4, 4));
        }

        if (newStoredPayload.Length <= slot.Length)
        {
            var destination = pageBuffer.AsSpan(slot.Offset, slot.Length);
            destination.Clear();
            newStoredPayload.CopyTo(destination);

            slot.Length = (ushort)newStoredPayload.Length;
            slot.Flags &= ~(SlotFlags.Compressed | SlotFlags.HasOverflow);
            if (compressed)
                slot.Flags |= SlotFlags.Compressed;

            slot.WriteTo(pageBuffer.AsSpan(slotOffset, SlotEntry.Size));
            _pageFile.WritePage(location.PageId, pageBuffer);

            if (oldOverflowHead != 0)
                FreeOverflowChainPages(oldOverflowHead);

            actualStoredBytes = newStoredPayload.Length;
            return true;
        }

        if (slot.Length < 8)
            return false;

        var primaryChunkSize = slot.Length - 8;
        if (primaryChunkSize < 0)
            return false;

        var remainder = newStoredPayload.Slice(primaryChunkSize);
        var newOverflowHead = BuildOverflowChainForMigration(remainder);

        var slotPayload = pageBuffer.AsSpan(slot.Offset, slot.Length);
        slotPayload.Clear();
        BinaryPrimitives.WriteInt32LittleEndian(slotPayload.Slice(0, 4), newStoredPayload.Length);
        BinaryPrimitives.WriteUInt32LittleEndian(slotPayload.Slice(4, 4), newOverflowHead);
        newStoredPayload.Slice(0, primaryChunkSize).CopyTo(slotPayload.Slice(8));

        slot.Flags &= ~(SlotFlags.Compressed | SlotFlags.HasOverflow);
        slot.Flags |= SlotFlags.HasOverflow;
        if (compressed)
            slot.Flags |= SlotFlags.Compressed;

        slot.WriteTo(pageBuffer.AsSpan(slotOffset, SlotEntry.Size));
        _pageFile.WritePage(location.PageId, pageBuffer);

        if (oldOverflowHead != 0)
            FreeOverflowChainPages(oldOverflowHead);

        actualStoredBytes = newStoredPayload.Length;
        return true;
    }

    private uint BuildOverflowChainForMigration(ReadOnlySpan<byte> overflowPayload)
    {
        if (overflowPayload.IsEmpty)
            return 0;

        var chunkSize = _pageFile.PageSize - SlottedPageHeader.Size;
        uint nextOverflowPageId = 0;

        var tailSize = overflowPayload.Length % chunkSize;
        var fullPages = overflowPayload.Length / chunkSize;

        if (tailSize > 0)
        {
            var tailOffset = fullPages * chunkSize;
            var tailSlice = overflowPayload.Slice(tailOffset, tailSize);
            nextOverflowPageId = AllocateOverflowPageForMigration(tailSlice, nextOverflowPageId);
        }

        for (var i = fullPages - 1; i >= 0; i--)
        {
            var chunkOffset = i * chunkSize;
            var chunk = overflowPayload.Slice(chunkOffset, chunkSize);
            nextOverflowPageId = AllocateOverflowPageForMigration(chunk, nextOverflowPageId);
        }

        return nextOverflowPageId;
    }

    private uint AllocateOverflowPageForMigration(ReadOnlySpan<byte> payloadChunk, uint nextOverflowPageId)
    {
        var pageId = _pageFile.AllocatePage();
        var buffer = new byte[_pageFile.PageSize];

        var header = new SlottedPageHeader
        {
            PageId = pageId,
            PageType = PageType.Overflow,
            SlotCount = 0,
            FreeSpaceStart = SlottedPageHeader.Size,
            FreeSpaceEnd = (ushort)_pageFile.PageSize,
            NextOverflowPage = nextOverflowPageId,
            TransactionId = 0
        };

        header.WriteTo(buffer);
        payloadChunk.CopyTo(buffer.AsSpan(SlottedPageHeader.Size));
        _pageFile.WritePage(pageId, buffer);
        return pageId;
    }

    private void FreeOverflowChainPages(uint firstOverflowPage)
    {
        var buffer = new byte[_pageFile.PageSize];
        var visited = new HashSet<uint>();
        var current = firstOverflowPage;

        while (current != 0 && current < _pageFile.NextPageId && visited.Add(current))
        {
            _pageFile.ReadPage(current, buffer);
            var header = SlottedPageHeader.ReadFrom(buffer);
            var next = header.NextOverflowPage;
            _pageFile.FreePage(current);
            current = next;
        }
    }
}
