// Copyright 2018-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/events.go in the NATS server Go source.

using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;

namespace ZB.MOM.NatsNet.Server;

/// <summary>
/// Static helpers used by the event system.
/// Mirrors various package-level helper functions in server/events.go.
/// </summary>
internal static class EventHelpers
{
    // =========================================================================
    // Base-62 alphabet used by NATS hash functions.
    // Mirrors Go package-level const in server/accounts.go.
    // =========================================================================

    private const string Digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    private const int    Base   = 62;

    // =========================================================================
    // System hash length (mirrors Go sysHashLen = 8)
    // =========================================================================

    internal const int SysHashLen = 8;

    // =========================================================================
    // Inbox constants (mirrors Go InboxPrefix / related consts)
    // =========================================================================

    internal const string InboxPrefix        = "$SYS._INBOX.";
    internal const int    InboxPrefixLen     = 12; // len("$SYS._INBOX.")
    internal const int    ReplySuffixLen     = 8;  // Gives us 62^8 unique values

    // =========================================================================
    // PubMsg pool
    // =========================================================================

    private static readonly ConcurrentBag<PubMsg> PubMsgPool = [];

    /// <summary>
    /// Factory for an internally-queued outbound publish message, using a pool
    /// to amortise allocations.
    /// Mirrors Go <c>newPubMsg</c> in server/events.go.
    /// </summary>
    internal static PubMsg NewPubMsg(
        ClientConnection? client,
        string            subject,
        string            reply,
        ServerInfo?       si,
        byte[]?           hdr,
        object?           msg,
        int               oct,
        bool              echo,
        bool              last)
    {
        if (!PubMsgPool.TryTake(out var pm))
            pm = new PubMsg();

        pm.Client  = client;
        pm.Subject = subject;
        pm.Reply   = reply;
        pm.Si      = si;
        pm.Hdr     = hdr;
        pm.Msg     = msg;
        pm.Oct     = oct;
        pm.Echo    = echo;
        pm.Last    = last;
        return pm;
    }

    /// <summary>
    /// Returns a <see cref="PubMsg"/> to the pool after all fields have been cleared.
    /// Called by <see cref="PubMsg.ReturnToPool"/>.
    /// </summary>
    internal static void ReturnPubMsg(PubMsg pm) => PubMsgPool.Add(pm);

    // =========================================================================
    // Group A: helpers
    // =========================================================================

    /// <summary>
    /// Generate a RouteStat for a statz update from a route client.
    /// Mirrors Go <c>routeStat</c> in server/events.go.
    /// Note: full in/out message counters require ClientConnection to carry those fields
    /// (added in a later batch). Until then, returns zero-valued counters.
    /// </summary>
    internal static RouteStat? RouteStat(ClientConnection? route)
    {
        if (route is null) return null;

        lock (route)
        {
            var rs = new RouteStat
            {
                Id      = route.Cid,
                Sent    = new DataStats { Msgs = 0, Bytes = 0 },
                Received = new DataStats { Msgs = 0, Bytes = 0 },
                Pending = (int)(route.OutPb),
            };
            if (route.Route != null)
                rs.Name = route.Route.RemoteName;
            return rs;
        }
    }

    /// <summary>
    /// Computes an 8-character base-62 hash of <paramref name="name"/>.
    /// Mirrors Go <c>getHash(name string) string</c> in server/events.go.
    /// </summary>
    internal static string GetHash(string name) => GetHashSize(name, SysHashLen);

    /// <summary>
    /// Computes a hash of <paramref name="size"/> characters for <paramref name="name"/>.
    /// Uses SHA-256, then maps the first <paramref name="size"/> bytes through the
    /// base-62 digit alphabet — identical to Go's <c>getHashSize</c>.
    /// Mirrors Go <c>getHashSize(name string, size int) string</c> in server/events.go.
    /// </summary>
    internal static string GetHashSize(string name, int size)
    {
        var hash   = SHA256.HashData(Encoding.UTF8.GetBytes(name));
        var result = new char[size];
        for (var i = 0; i < size; i++)
            result[i] = Digits[(int)(hash[i] % Base)];
        return new string(result);
    }

    /// <summary>
    /// Parses an Accept-Encoding header to determine which compression type
    /// the remote supports (snappy / gzip / none).
    /// Mirrors Go <c>getAcceptEncoding(hdr []byte) compressionType</c> in server/events.go.
    /// </summary>
    internal static CompressionType GetAcceptEncoding(byte[]? hdr)
    {
        if (hdr is null || hdr.Length == 0) return CompressionType.None;

        // Extract the Accept-Encoding header value.
        const string headerName = "Accept-Encoding";
        var ae = GetHeaderValue(headerName, hdr)?.ToLowerInvariant() ?? string.Empty;

        if (string.IsNullOrEmpty(ae))            return CompressionType.None;
        if (ae.Contains("snappy") || ae.Contains("s2")) return CompressionType.Snappy;
        if (ae.Contains("gzip"))                 return CompressionType.Gzip;
        return CompressionType.Unsupported;
    }

    /// <summary>
    /// Generate the tracking subject for remote latency from a response subject.
    /// Mirrors Go <c>remoteLatencySubjectForResponse</c> in server/events.go.
    /// </summary>
    internal static string RemoteLatencySubjectForResponse(ReadOnlySpan<byte> subject)
    {
        if (!Account.IsTrackedReply(subject)) return string.Empty;

        // Split on '.' and take second-to-last token.
        var str  = Encoding.ASCII.GetString(subject);
        var toks = str.Split('.');
        if (toks.Length < 2) return string.Empty;

        return string.Format(SystemSubjects.RemoteLatencyEventSubj, toks[^2]);
    }

    /// <summary>
    /// Sum subscription counts from a sublist result for clients and hub leaf nodes.
    /// Mirrors Go <c>totalSubs</c> in server/events.go.
    /// </summary>
    internal static int TotalSubs(SubscriptionIndexResult? rr, byte[]? queueGroup)
    {
        if (rr is null) return 0;

        var count = 0;

        bool Matches(Subscription sub)
        {
            if (queueGroup != null && !sub.Queue.AsSpan().SequenceEqual(queueGroup))
                return false;
            return sub.Client?.Kind == ClientKind.Client || (sub.Client?.IsHubLeafNode() ?? false);
        }

        if (queueGroup is null)
        {
            foreach (var sub in rr.PSubs)
                if (Matches(sub)) count++;
        }

        foreach (var qsubs in rr.QSubs)
            foreach (var sub in qsubs)
                if (Matches(sub)) count++;

        return count;
    }

    /// <summary>
    /// Returns the account name for a client (or "N/A" if none).
    /// Mirrors Go <c>accForClient(c *client) string</c> in server/events.go.
    /// </summary>
    internal static string AccForClient(ClientConnection c)
    {
        var acc = c._account as Account;
        return acc?.Name ?? "N/A";
    }

    /// <summary>
    /// Returns the issuer key for a client (empty if none).
    /// Mirrors Go <c>issuerForClient(c *client) string</c> in server/events.go.
    /// </summary>
    internal static string IssuerForClient(ClientConnection c)
    {
        if (c.Perms is null) return string.Empty;
        // Stub: full user/signing-key lookup requires auth integration.
        return string.Empty;
    }

    /// <summary>
    /// Safely stops and nulls a timer reference.
    /// Mirrors Go <c>clearTimer(tp **time.Timer)</c> in server/events.go.
    /// </summary>
    internal static void ClearTimer(ref System.Threading.Timer? timer)
    {
        var t = timer;
        timer = null;
        t?.Dispose();
    }

    // =========================================================================
    // Header parsing helper
    // =========================================================================

    /// <summary>
    /// Extracts the value of a named header from a NATS wire-format header block.
    /// </summary>
    private static string? GetHeaderValue(string headerName, byte[] hdr)
    {
        var text   = Encoding.ASCII.GetString(hdr);
        var search = headerName + ":";
        var idx    = text.IndexOf(search, StringComparison.OrdinalIgnoreCase);
        if (idx < 0) return null;

        var start = idx + search.Length;
        var end   = text.IndexOf('\r', start);
        if (end < 0) end = text.Length;

        return text[start..end].Trim();
    }

    /// <summary>
    /// Sends a JSON response for an account claims update request.
    /// Mirrors Go <c>respondToUpdate</c> in server/events.go.
    /// </summary>
    internal static void RespondToUpdate(NatsServer server, string reply, string pubKey, string message, Exception? err)
    {
        if (string.IsNullOrEmpty(reply)) return;
        var response = new ServerApiResponse();
        if (err != null)
        {
            response.Error = new ServerApiError { Code = 500, Description = err.Message };
        }
        response.Data = new { account = pubKey, message, error = err?.Message };
        server.SendInternalResponse(reply, response);
    }
}

// =========================================================================
// CompressionType — mirrors Go compressionType enum in events.go
// =========================================================================

/// <summary>
/// Compression type for internal message publishing.
/// Mirrors Go <c>compressionType</c> in server/events.go.
/// </summary>
internal enum CompressionType : int
{
    None        = 0,
    Gzip        = 1,
    Snappy      = 2,
    Unsupported = 3,
}
