// Go reference: server/events.go:2081-2090 — compressionType, snappyCompression,
// and events.go:578-598 — internalSendLoop compression via s2.WriterSnappyCompat().

using System.IO.Compression;
using IronSnappy;

namespace NATS.Server.Events;

/// <summary>
/// Compression encodings supported for event API responses.
/// Go reference: events.go compressionType constants.
/// </summary>
public enum EventCompressionType : sbyte
{
    None = 0,
    Gzip = 1,
    Snappy = 2,
    Unsupported = 3,
}

/// <summary>
/// Provides S2 (Snappy-compatible) compression for system event payloads.
/// Maps to Go's compressionType / snappyCompression handling in events.go:2082-2098
/// and the compression branch in the internalSendLoop (events.go:578-598).
/// </summary>
public static class EventCompressor
{
    public const string AcceptEncodingHeader = "Accept-Encoding";
    public const string ContentEncodingHeader = "Content-Encoding";

    // Default threshold: only compress payloads larger than this many bytes.
    // Compressing tiny payloads wastes CPU and may produce larger output.
    private const int DefaultThresholdBytes = 256;

    private static long _totalCompressed;
    private static long _totalUncompressed;
    private static long _bytesSaved;

    /// <summary>
    /// Total number of compress operations performed (payloads that met the
    /// threshold and were actually compressed). Thread-safe via <see cref="Interlocked"/>.
    /// </summary>
    public static long TotalCompressed => Interlocked.Read(ref _totalCompressed);

    /// <summary>
    /// Total number of payloads that were left uncompressed (below threshold or
    /// <see cref="CompressIfBeneficial"/> returned the original). Thread-safe.
    /// </summary>
    public static long TotalUncompressed => Interlocked.Read(ref _totalUncompressed);

    /// <summary>
    /// Cumulative bytes saved across all compression operations.
    /// Computed as (original size − compressed size) for each call to
    /// <see cref="CompressIfBeneficial"/> that chose to compress. Thread-safe.
    /// </summary>
    public static long BytesSaved => Interlocked.Read(ref _bytesSaved);

    /// <summary>
    /// Resets all statistics counters to zero. Useful in test isolation.
    /// </summary>
    public static void ResetStats()
    {
        Interlocked.Exchange(ref _totalCompressed, 0);
        Interlocked.Exchange(ref _totalUncompressed, 0);
        Interlocked.Exchange(ref _bytesSaved, 0);
    }

    /// <summary>
    /// Compresses <paramref name="payload"/> using the Snappy block format via IronSnappy.
    /// Go reference: events.go:591 — <c>s2.WriterSnappyCompat()</c> writes Snappy-compatible frames.
    /// </summary>
    /// <param name="payload">Raw bytes to compress.</param>
    /// <returns>Compressed bytes. Returns an empty array for empty input.</returns>
    public static byte[] Compress(ReadOnlySpan<byte> payload)
        => Compress(payload, EventCompressionType.Snappy);

    /// <summary>
    /// Compresses <paramref name="payload"/> using the requested <paramref name="compression"/>.
    /// </summary>
    public static byte[] Compress(ReadOnlySpan<byte> payload, EventCompressionType compression)
    {
        if (payload.IsEmpty)
            return [];

        return compression switch
        {
            EventCompressionType.None => payload.ToArray(),
            EventCompressionType.Gzip => CompressGzip(payload),
            EventCompressionType.Snappy => Snappy.Encode(payload),
            _ => throw new InvalidOperationException($"Unsupported compression type: {compression}."),
        };
    }

    /// <summary>
    /// Decompresses a Snappy-compressed <paramref name="compressed"/> payload.
    /// Go reference: events.go:590 — <c>s2.NewReader</c> / Snappy-compat decode.
    /// </summary>
    /// <param name="compressed">Snappy-compressed bytes to decode.</param>
    /// <returns>Decompressed bytes. Returns an empty array for empty input.</returns>
    /// <exception cref="Exception">Propagated from IronSnappy if data is corrupt.</exception>
    public static byte[] Decompress(ReadOnlySpan<byte> compressed)
        => Decompress(compressed, EventCompressionType.Snappy);

    /// <summary>
    /// Decompresses <paramref name="compressed"/> using the selected <paramref name="compression"/>.
    /// </summary>
    public static byte[] Decompress(ReadOnlySpan<byte> compressed, EventCompressionType compression)
    {
        if (compressed.IsEmpty)
            return [];

        return compression switch
        {
            EventCompressionType.None => compressed.ToArray(),
            EventCompressionType.Gzip => DecompressGzip(compressed),
            EventCompressionType.Snappy => Snappy.Decode(compressed),
            _ => throw new InvalidOperationException($"Unsupported compression type: {compression}."),
        };
    }

    /// <summary>
    /// Returns <see langword="true"/> when <paramref name="payloadSize"/> exceeds
    /// <paramref name="thresholdBytes"/>.  Mirrors the Go pattern where compression
    /// is only applied once a minimum payload size is reached (events.go:578).
    /// </summary>
    /// <param name="payloadSize">Number of bytes in the raw payload.</param>
    /// <param name="thresholdBytes">Minimum size to trigger compression (default 256).</param>
    public static bool ShouldCompress(int payloadSize, int thresholdBytes = DefaultThresholdBytes)
        => payloadSize > thresholdBytes;

    /// <summary>
    /// Compresses <paramref name="payload"/> if it exceeds <paramref name="thresholdBytes"/>,
    /// updating the shared statistics counters via <see cref="Interlocked"/>.
    /// Returns the original span as a new array when the payload is too small.
    /// </summary>
    /// <param name="payload">Raw bytes to (maybe) compress.</param>
    /// <param name="thresholdBytes">Minimum size to trigger compression (default 256).</param>
    /// <returns>
    /// A tuple of (<c>Data</c>, <c>Compressed</c>):
    /// <list type="bullet">
    ///   <item><c>Compressed = true</c>  — <c>Data</c> is Snappy-compressed.</item>
    ///   <item><c>Compressed = false</c> — <c>Data</c> is the original bytes unchanged.</item>
    /// </list>
    /// </returns>
    public static (byte[] Data, bool Compressed) CompressIfBeneficial(
        ReadOnlySpan<byte> payload,
        int thresholdBytes = DefaultThresholdBytes)
        => CompressIfBeneficial(payload, EventCompressionType.Snappy, thresholdBytes);

    /// <summary>
    /// Compresses using <paramref name="compression"/> when payload size exceeds threshold.
    /// </summary>
    public static (byte[] Data, bool Compressed) CompressIfBeneficial(
        ReadOnlySpan<byte> payload,
        EventCompressionType compression,
        int thresholdBytes = DefaultThresholdBytes)
    {
        if (!ShouldCompress(payload.Length, thresholdBytes))
        {
            Interlocked.Increment(ref _totalUncompressed);
            return (payload.ToArray(), false);
        }

        var compressed = Compress(payload, compression);
        Interlocked.Increment(ref _totalCompressed);
        var saved = payload.Length - compressed.Length;
        if (saved > 0)
            Interlocked.Add(ref _bytesSaved, saved);

        return (compressed, true);
    }

    /// <summary>
    /// Returns the compression ratio as <c>compressedSize / originalSize</c>.
    /// A value less than 1.0 indicates the data shrank; exactly 0.0 means the
    /// original was empty. Lower is better.
    /// </summary>
    /// <param name="originalSize">Size of the uncompressed payload in bytes.</param>
    /// <param name="compressedSize">Size of the compressed output in bytes.</param>
    public static double GetCompressionRatio(int originalSize, int compressedSize)
    {
        if (originalSize == 0)
            return 0.0;

        return (double)compressedSize / originalSize;
    }

    /// <summary>
    /// Parses an HTTP Accept-Encoding value into a supported compression type.
    /// Go reference: events.go getAcceptEncoding().
    /// </summary>
    public static EventCompressionType GetAcceptEncoding(string? acceptEncoding)
    {
        if (string.IsNullOrWhiteSpace(acceptEncoding))
            return EventCompressionType.None;

        var value = acceptEncoding.ToLowerInvariant();
        if (value.Contains("snappy", StringComparison.Ordinal)
            || value.Contains("s2", StringComparison.Ordinal))
        {
            return EventCompressionType.Snappy;
        }

        if (value.Contains("gzip", StringComparison.Ordinal))
            return EventCompressionType.Gzip;

        return EventCompressionType.Unsupported;
    }

    private static byte[] CompressGzip(ReadOnlySpan<byte> payload)
    {
        using var output = new MemoryStream();
        using (var gzip = new GZipStream(output, CompressionLevel.Fastest, leaveOpen: true))
        {
            gzip.Write(payload);
        }
        return output.ToArray();
    }

    private static byte[] DecompressGzip(ReadOnlySpan<byte> compressed)
    {
        using var input = new MemoryStream(compressed.ToArray());
        using var gzip = new GZipStream(input, CompressionMode.Decompress);
        using var output = new MemoryStream();
        gzip.CopyTo(output);
        return output.ToArray();
    }
}
