namespace NATS.Server.Gateways;

/// <summary>
/// Maps reply subjects to gateway-prefixed forms and restores them.
/// The gateway reply format is <c>_GR_.{clusterId}.{hash}.{originalReply}</c>.
/// A legacy format <c>_GR_.{clusterId}.{originalReply}</c> (no hash) is also supported
/// for backward compatibility.
/// Go reference: gateway.go:2000-2100, gateway.go:340-380.
/// </summary>
public static class ReplyMapper
{
    public const string GatewayReplyPrefix = "_GR_.";
    public const string OldGatewayReplyPrefix = "$GR.";
    public const int GatewayReplyPrefixLen = 5;
    public const int OldGatewayReplyPrefixLen = 4;
    public const int GatewayHashLen = 6;
    public const int OldGatewayHashLen = 4;

    /// <summary>
    /// Checks whether the subject starts with either gateway reply prefix:
    /// <c>_GR_.</c> (current) or <c>$GR.</c> (legacy).
    /// </summary>
    public static bool HasGatewayReplyPrefix(string? subject)
        => IsGatewayRoutedSubject(subject, out _);

    /// <summary>
    /// Returns true when the subject is gateway-routed and indicates if the legacy
    /// old prefix (<c>$GR.</c>) was used.
    /// Go reference: isGWRoutedSubjectAndIsOldPrefix.
    /// </summary>
    public static bool IsGatewayRoutedSubject(string? subject, out bool isOldPrefix)
    {
        isOldPrefix = false;
        if (string.IsNullOrWhiteSpace(subject))
            return false;

        if (subject.StartsWith(GatewayReplyPrefix, StringComparison.Ordinal))
            return true;

        if (subject.StartsWith(OldGatewayReplyPrefix, StringComparison.Ordinal))
        {
            isOldPrefix = true;
            return true;
        }

        return false;
    }

    /// <summary>
    /// Computes a deterministic FNV-1a hash of the reply subject.
    /// Go reference: gateway.go uses SHA-256 truncated to base-62; we use FNV-1a for speed
    /// while maintaining determinism and good distribution.
    /// </summary>
    public static long ComputeReplyHash(string replyTo)
    {
        // FNV-1a 64-bit
        const ulong fnvOffsetBasis = 14695981039346656037UL;
        const ulong fnvPrime = 1099511628211UL;

        var hash = fnvOffsetBasis;
        foreach (var c in replyTo)
        {
            hash ^= (byte)c;
            hash *= fnvPrime;
        }

        // Return as non-negative long
        return (long)(hash & 0x7FFFFFFFFFFFFFFF);
    }

    /// <summary>
    /// Computes the short (6-char) gateway hash used in modern gateway reply routing.
    /// Go reference: getGWHash.
    /// </summary>
    public static string ComputeGatewayHash(string gatewayName)
    {
        var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(gatewayName));
        return Convert.ToHexString(digest.AsSpan(0, 3)).ToLowerInvariant();
    }

    /// <summary>
    /// Computes the short (4-char) legacy gateway hash used with old prefixes.
    /// Go reference: getOldHash.
    /// </summary>
    public static string ComputeOldGatewayHash(string gatewayName)
    {
        var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(gatewayName));
        return Convert.ToHexString(digest.AsSpan(0, 2)).ToLowerInvariant();
    }

    /// <summary>
    /// Converts a reply subject to gateway form with an explicit hash segment.
    /// Format: <c>_GR_.{clusterId}.{hash}.{originalReply}</c>.
    /// </summary>
    public static string? ToGatewayReply(string? replyTo, string localClusterId, long hash)
    {
        if (string.IsNullOrWhiteSpace(replyTo))
            return replyTo;

        return $"{GatewayReplyPrefix}{localClusterId}.{hash}.{replyTo}";
    }

    /// <summary>
    /// Converts a reply subject to gateway form, automatically computing the hash.
    /// Format: <c>_GR_.{clusterId}.{hash}.{originalReply}</c>.
    /// </summary>
    public static string? ToGatewayReply(string? replyTo, string localClusterId)
    {
        if (string.IsNullOrWhiteSpace(replyTo))
            return replyTo;

        var hash = ComputeReplyHash(replyTo);
        return ToGatewayReply(replyTo, localClusterId, hash);
    }

    /// <summary>
    /// Restores the original reply subject from a gateway-prefixed reply.
    /// Handles both new format (<c>_GR_.{clusterId}.{hash}.{originalReply}</c>) and
    /// legacy format (<c>_GR_.{clusterId}.{originalReply}</c>).
    /// Nested prefixes are unwrapped iteratively.
    /// </summary>
    public static bool TryRestoreGatewayReply(string? gatewayReply, out string restoredReply)
    {
        restoredReply = string.Empty;

        if (!IsGatewayRoutedSubject(gatewayReply, out _))
            return false;

        var current = gatewayReply!;
        while (IsGatewayRoutedSubject(current, out var isOldPrefix))
        {
            var prefixLen = isOldPrefix ? OldGatewayReplyPrefixLen : GatewayReplyPrefixLen;
            var afterPrefix = current[prefixLen..];

            // Find the first dot (end of clusterId)
            var firstDot = afterPrefix.IndexOf('.');
            if (firstDot < 0 || firstDot == afterPrefix.Length - 1)
                return false;

            var afterCluster = afterPrefix[(firstDot + 1)..];

            // Check if the next segment is a numeric hash
            var secondDot = afterCluster.IndexOf('.');
            if (secondDot > 0 && secondDot < afterCluster.Length - 1 && IsNumericSegment(afterCluster.AsSpan()[..secondDot]))
            {
                // New format: skip hash segment too
                current = afterCluster[(secondDot + 1)..];
            }
            else
            {
                // Legacy format: no hash, the rest is the original reply
                current = afterCluster;
            }
        }

        restoredReply = current;
        return true;
    }

    /// <summary>
    /// Extracts the cluster ID from a gateway reply subject.
    /// The cluster ID is the first segment after the <c>_GR_.</c> prefix.
    /// </summary>
    public static bool TryExtractClusterId(string? gatewayReply, out string clusterId)
    {
        clusterId = string.Empty;

        if (!IsGatewayRoutedSubject(gatewayReply, out var isOldPrefix))
            return false;

        var afterPrefix = gatewayReply![(isOldPrefix ? OldGatewayReplyPrefixLen : GatewayReplyPrefixLen)..];
        var dot = afterPrefix.IndexOf('.');
        if (dot <= 0)
            return false;

        clusterId = afterPrefix[..dot];
        return true;
    }

    /// <summary>
    /// Extracts the hash from a gateway reply subject (new format only).
    /// Returns false if the reply uses the legacy format without a hash.
    /// </summary>
    public static bool TryExtractHash(string? gatewayReply, out long hash)
    {
        hash = 0;

        if (!IsGatewayRoutedSubject(gatewayReply, out var isOldPrefix))
            return false;

        var afterPrefix = gatewayReply![(isOldPrefix ? OldGatewayReplyPrefixLen : GatewayReplyPrefixLen)..];

        // Skip clusterId
        var firstDot = afterPrefix.IndexOf('.');
        if (firstDot <= 0 || firstDot == afterPrefix.Length - 1)
            return false;

        var afterCluster = afterPrefix[(firstDot + 1)..];

        // Try to parse hash segment
        var secondDot = afterCluster.IndexOf('.');
        if (secondDot <= 0)
            return false;

        var hashSegment = afterCluster[..secondDot];
        return long.TryParse(hashSegment, out hash);
    }

    private static bool IsNumericSegment(ReadOnlySpan<char> segment)
    {
        if (segment.IsEmpty)
            return false;

        foreach (var c in segment)
        {
            if (c is not (>= '0' and <= '9'))
                return false;
        }

        return true;
    }
}

/// <summary>
/// LRU cache with TTL expiration for gateway reply subject mappings.
/// Caches the result of MapToGateway/MapFromGateway to avoid repeated parsing.
/// Go reference: gateway.go — reply mapping cache.
/// </summary>
public sealed class ReplyMapCache
{
    private readonly int _capacity;
    private readonly int _ttlMs;
    private readonly Dictionary<string, LinkedListNode<CacheEntry>> _map;
    private readonly LinkedList<CacheEntry> _list = new();
    private readonly Lock _lock = new();
    private long _hits;
    private long _misses;

    public ReplyMapCache(int capacity = 4096, int ttlMs = 60_000)
    {
        _capacity = capacity;
        _ttlMs = ttlMs;
        _map = new Dictionary<string, LinkedListNode<CacheEntry>>(capacity, StringComparer.Ordinal);
    }

    public long Hits => Interlocked.Read(ref _hits);
    public long Misses => Interlocked.Read(ref _misses);
    public int Count { get { lock (_lock) return _map.Count; } }

    public bool TryGet(string key, out string? value)
    {
        lock (_lock)
        {
            if (_map.TryGetValue(key, out var node))
            {
                // Check TTL
                if ((DateTime.UtcNow - node.Value.CreatedUtc).TotalMilliseconds > _ttlMs)
                {
                    // Expired
                    _list.Remove(node);
                    _map.Remove(key);
                    value = null;
                    Interlocked.Increment(ref _misses);
                    return false;
                }

                value = node.Value.Value;
                _list.Remove(node);
                _list.AddFirst(node);
                Interlocked.Increment(ref _hits);
                return true;
            }
        }
        value = null;
        Interlocked.Increment(ref _misses);
        return false;
    }

    public void Set(string key, string value)
    {
        lock (_lock)
        {
            if (_map.TryGetValue(key, out var existing))
            {
                _list.Remove(existing);
                existing.Value = new CacheEntry(key, value, DateTime.UtcNow);
                _list.AddFirst(existing);
                return;
            }

            if (_map.Count >= _capacity)
            {
                var last = _list.Last!;
                _map.Remove(last.Value.Key);
                _list.RemoveLast();
            }

            var entry = new CacheEntry(key, value, DateTime.UtcNow);
            var node = new LinkedListNode<CacheEntry>(entry);
            _list.AddFirst(node);
            _map[key] = node;
        }
    }

    public void Clear()
    {
        lock (_lock)
        {
            _map.Clear();
            _list.Clear();
        }
    }

    /// <summary>Removes expired entries.</summary>
    public int PurgeExpired()
    {
        lock (_lock)
        {
            var now = DateTime.UtcNow;
            var purged = 0;
            var node = _list.Last;
            while (node != null)
            {
                var prev = node.Previous;
                if ((now - node.Value.CreatedUtc).TotalMilliseconds > _ttlMs)
                {
                    _map.Remove(node.Value.Key);
                    _list.Remove(node);
                    purged++;
                }
                node = prev;
            }
            return purged;
        }
    }

    private sealed record CacheEntry(string Key, string Value, DateTime CreatedUtc);
}
