using System.Buffers;
using System.IO.Compression;
using System.Runtime.CompilerServices;
using System.Text;

namespace AyCode.Core.Compression;

/// <summary>
/// GZip compression/decompression helper for SignalR message transport.
/// Matches BrotliHelper API to make switching effortless.
/// </summary>
public static class GzipHelper
{
    private const int DefaultBufferSize = 4096;
    private const int MaxStackAllocSize = 1024;

    #region Compression

    /// <summary>
    /// Compresses a string using GZip compression with pooled buffers.
    /// </summary>
    public static byte[] Compress(string text, CompressionLevel compressionLevel = CompressionLevel.Optimal)
    {
        if (string.IsNullOrEmpty(text))
            return [];

        var maxByteCount = Encoding.UTF8.GetMaxByteCount(text.Length);

        if (maxByteCount <= MaxStackAllocSize)
        {
            Span<byte> utf8Bytes = stackalloc byte[maxByteCount];
            var actualLength = Encoding.UTF8.GetBytes(text.AsSpan(), utf8Bytes);
            return CompressSpan(utf8Bytes[..actualLength], compressionLevel);
        }

        var rentedBuffer = ArrayPool<byte>.Shared.Rent(maxByteCount);
        try
        {
            var actualLength = Encoding.UTF8.GetBytes(text.AsSpan(), rentedBuffer);
            return CompressSpan(rentedBuffer.AsSpan(0, actualLength), compressionLevel);
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(rentedBuffer);
        }
    }

    /// <summary>
    /// Compresses a byte array using GZip compression.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static byte[] Compress(byte[] data, CompressionLevel compressionLevel = CompressionLevel.Optimal)
        => data == null || data.Length == 0 ? [] : CompressSpan(data.AsSpan(), compressionLevel);

    /// <summary>
    /// Compresses a ReadOnlySpan using GZip compression with pooled output buffer.
    /// </summary>
    public static byte[] CompressSpan(ReadOnlySpan<byte> data, CompressionLevel compressionLevel = CompressionLevel.Optimal)
    {
        if (data.IsEmpty)
            return [];

        using var outputStream = new MemoryStream();
        using (var gzipStream = new GZipStream(outputStream, compressionLevel, leaveOpen: true))
        {
            gzipStream.Write(data);
        }

        return outputStream.ToArray();
    }

    #endregion

    #region Decompression

    /// <summary>
    /// Decompresses GZip-compressed data to a string.
    /// </summary>
    public static string DecompressToString(byte[] compressedData)
    {
        if (compressedData == null || compressedData.Length == 0)
            return string.Empty;

        var decompressedBytes = Decompress(compressedData);
        return Encoding.UTF8.GetString(decompressedBytes);
    }

    /// <summary>
    /// Decompresses GZip-compressed data to a byte array.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static byte[] Decompress(byte[] compressedData)
    {
        if (compressedData == null || compressedData.Length == 0)
            return [];

        return DecompressCore(compressedData);
    }

    /// <summary>
    /// Decompresses GZip-compressed data from a ReadOnlySpan.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static byte[] DecompressSpan(ReadOnlySpan<byte> compressedData)
    {
        if (compressedData.IsEmpty)
            return [];

        return DecompressCore(compressedData.ToArray());
    }

    private static byte[] DecompressCore(byte[] compressedData)
    {
        using var inputStream = new MemoryStream(compressedData, writable: false);
        using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress);
        using var outputStream = new MemoryStream();

        gzipStream.CopyTo(outputStream);
        return outputStream.ToArray();
    }

    /// <summary>
    /// Decompresses to a rented buffer. Caller must return the buffer to ArrayPool.
    /// Returns the actual decompressed length.
    /// </summary>
    public static (byte[] Buffer, int Length) DecompressToRentedBuffer(ReadOnlySpan<byte> compressedData)
    {
        if (compressedData.IsEmpty)
            return ([], 0);

        var estimatedSize = Math.Max(compressedData.Length * 4, DefaultBufferSize);
        var outputBuffer = ArrayPool<byte>.Shared.Rent(estimatedSize);

        using var inputStream = new MemoryStream(compressedData.ToArray(), writable: false);
        using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress);

        var totalRead = 0;
        int bytesRead;

        while ((bytesRead = gzipStream.Read(outputBuffer.AsSpan(totalRead))) > 0)
        {
            totalRead += bytesRead;

            if (totalRead >= outputBuffer.Length - DefaultBufferSize)
            {
                var newBuffer = ArrayPool<byte>.Shared.Rent(outputBuffer.Length * 2);
                outputBuffer.AsSpan(0, totalRead).CopyTo(newBuffer);
                ArrayPool<byte>.Shared.Return(outputBuffer);
                outputBuffer = newBuffer;
            }
        }

        return (outputBuffer, totalRead);
    }

    #endregion

    #region Utility

    /// <summary>
    /// Checks if the data appears to be GZip compressed.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsGzipCompressed(byte[] data)
    {
        if (data == null || data.Length < 2)
            return false;

        // 0x1F, 0x8B are the fixed gzip header bytes
        if (data[0] != 0x1F || data[1] != 0x8B)
            return false;

        try
        {
            using var inputStream = new MemoryStream(data, 0, Math.Min(data.Length, 64), writable: false);
            using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress);
            Span<byte> buffer = stackalloc byte[1];
            return gzipStream.Read(buffer) >= 0;
        }
        catch
        {
            return false;
        }
    }

    #endregion
}
