// Copyright 2012-2025 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/util.go in the NATS server Go source.

using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;

namespace ZB.MOM.NatsNet.Server.Internal;

/// <summary>
/// General-purpose server utility methods.
/// Mirrors server/util.go.
/// </summary>
public static class ServerUtilities
{
    // Semver validation regex — mirrors semVerRe in const.go.
    private static readonly Regex SemVerRe = new(
        @"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$",
        RegexOptions.Compiled);

    // -------------------------------------------------------------------------
    // Version helpers
    // -------------------------------------------------------------------------

    /// <summary>
    /// Parses a semver string into major/minor/patch components.
    /// Returns an error if the string is not a valid semver.
    /// Mirrors <c>versionComponents</c>.
    /// </summary>
    public static (int major, int minor, int patch, Exception? err) VersionComponents(string version)
    {
        var m = SemVerRe.Match(version);
        if (!m.Success)
            return (0, 0, 0, new InvalidOperationException("invalid semver"));

        if (!int.TryParse(m.Groups[1].Value, out var major) ||
            !int.TryParse(m.Groups[2].Value, out var minor) ||
            !int.TryParse(m.Groups[3].Value, out var patch))
            return (-1, -1, -1, new InvalidOperationException("invalid semver component"));

        return (major, minor, patch, null);
    }

    /// <summary>
    /// Returns (true, nil) if <paramref name="version"/> is at least major.minor.patch.
    /// Mirrors <c>versionAtLeastCheckError</c>.
    /// </summary>
    public static (bool ok, Exception? err) VersionAtLeastCheckError(
        string version, int emajor, int eminor, int epatch)
    {
        var (major, minor, patch, err) = VersionComponents(version);
        if (err != null) return (false, err);

        if (major > emajor) return (true, null);
        if (major == emajor && minor > eminor) return (true, null);
        if (major == emajor && minor == eminor && patch >= epatch) return (true, null);
        return (false, null);
    }

    /// <summary>
    /// Returns true if <paramref name="version"/> is at least major.minor.patch.
    /// Mirrors <c>versionAtLeast</c>.
    /// </summary>
    public static bool VersionAtLeast(string version, int emajor, int eminor, int epatch)
    {
        var (ok, _) = VersionAtLeastCheckError(version, emajor, eminor, epatch);
        return ok;
    }

    // -------------------------------------------------------------------------
    // Integer parsing helpers (used for NATS protocol parsing)
    // -------------------------------------------------------------------------

    /// <summary>
    /// Parses a decimal positive integer from ASCII bytes.
    /// Returns -1 on error or if the input contains non-digit characters.
    /// Mirrors <c>parseSize</c>.
    /// </summary>
    public static int ParseSize(ReadOnlySpan<byte> d)
    {
        const int MaxParseSizeLen = 9; // 999M

        if (d.IsEmpty || d.Length > MaxParseSizeLen) return -1;

        var n = 0;
        foreach (var dec in d)
        {
            if (dec < '0' || dec > '9') return -1;
            n = n * 10 + (dec - '0');
        }
        return n;
    }

    /// <summary>
    /// Parses a decimal positive int64 from ASCII bytes.
    /// Returns -1 on error.
    /// Mirrors <c>parseInt64</c>.
    /// </summary>
    public static long ParseInt64(ReadOnlySpan<byte> d)
    {
        if (d.IsEmpty) return -1;

        long n = 0;
        foreach (var dec in d)
        {
            if (dec < '0' || dec > '9') return -1;
            n = n * 10 + (dec - '0');
        }
        return n;
    }

    // -------------------------------------------------------------------------
    // Duration / network helpers
    // -------------------------------------------------------------------------

    /// <summary>
    /// Converts float64 seconds to a <see cref="TimeSpan"/>.
    /// Mirrors <c>secondsToDuration</c>.
    /// </summary>
    public static TimeSpan SecondsToDuration(double seconds) =>
        TimeSpan.FromSeconds(seconds);

    /// <summary>
    /// Splits "host:port" into components, using <paramref name="defaultPort"/>
    /// when no port (or port 0 / -1) is present.
    /// Mirrors <c>parseHostPort</c>.
    /// </summary>
    public static (string host, int port, Exception? err) ParseHostPort(string hostPort, int defaultPort)
    {
        if (string.IsNullOrEmpty(hostPort))
            return ("", -1, new InvalidOperationException("no hostport specified"));

        // Try splitting; if port is missing, append the default and retry.
        string host, sPort;
        try
        {
            var ep = ParseEndpoint(hostPort);
            host = ep.host;
            sPort = ep.port;
        }
        catch
        {
            try
            {
                var ep = ParseEndpoint($"{hostPort}:{defaultPort}");
                host = ep.host;
                sPort = ep.port;
            }
            catch (Exception ex)
            {
                return ("", -1, ex);
            }
        }

        if (!int.TryParse(sPort.Trim(), out var port))
            return ("", -1, new InvalidOperationException($"invalid port: {sPort}"));

        if (port == 0 || port == -1)
            port = defaultPort;

        return (host.Trim(), port, null);
    }

    private static (string host, string port) ParseEndpoint(string hostPort)
    {
        // net.SplitHostPort equivalent — handles IPv6 [::1]:port
        if (hostPort.StartsWith('['))
        {
            var closeIdx = hostPort.IndexOf(']');
            if (closeIdx < 0 || closeIdx + 1 >= hostPort.Length || hostPort[closeIdx + 1] != ':')
                throw new InvalidOperationException($"missing port in address {hostPort}");
            return (hostPort[1..closeIdx], hostPort[(closeIdx + 2)..]);
        }

        var lastColon = hostPort.LastIndexOf(':');
        if (lastColon < 0)
            throw new InvalidOperationException($"missing port in address {hostPort}");

        var host = hostPort[..lastColon];
        var port = hostPort[(lastColon + 1)..];

        // Reject bare IPv6 addresses (multiple colons without brackets).
        if (host.Contains(':'))
            throw new InvalidOperationException($"too many colons in address {hostPort}");

        return (host, port);
    }

    /// <summary>
    /// Returns true if two <see cref="Uri"/> instances represent the same URL.
    /// Mirrors <c>urlsAreEqual</c>.
    /// </summary>
    public static bool UrlsAreEqual(Uri? u1, Uri? u2) =>
        u1 == u2 || (u1 != null && u2 != null && u1.ToString() == u2.ToString());

    // -------------------------------------------------------------------------
    // Comma formatting
    // -------------------------------------------------------------------------

    /// <summary>
    /// Formats an int64 with comma thousands separators.
    /// Mirrors <c>comma</c> in util.go.
    /// </summary>
    public static string Comma(long v)
    {
        if (v == long.MinValue) return "-9,223,372,036,854,775,808";

        var sign = "";
        if (v < 0) { sign = "-"; v = -v; }

        var parts = new string[7];
        var j = parts.Length - 1;

        while (v > 999)
        {
            var part = (v % 1000).ToString();
            parts[j--] = part.Length switch { 2 => "0" + part, 1 => "00" + part, _ => part };
            v /= 1000;
        }
        parts[j] = v.ToString();

        return sign + string.Join(",", parts.Skip(j));
    }

    // -------------------------------------------------------------------------
    // TCP helpers
    // -------------------------------------------------------------------------

    /// <summary>
    /// Creates a TCP listener with keepalives disabled (NATS server default).
    /// Mirrors <c>natsListen</c>.
    /// </summary>
    public static System.Net.Sockets.TcpListener NatsListen(string address, int port)
    {
        // .NET TcpListener does not set keepalive by default; the socket can be
        // further configured after creation if needed.
        var listener = new System.Net.Sockets.TcpListener(IPAddress.Parse(address), port);
        return listener;
    }

    /// <summary>
    /// Opens a TCP connection with the given timeout and keepalives disabled.
    /// Mirrors <c>natsDialTimeout</c>.
    /// </summary>
    public static async Task<System.Net.Sockets.TcpClient> NatsDialTimeoutAsync(
        string host, int port, TimeSpan timeout)
    {
        var client = new System.Net.Sockets.TcpClient();
        // Disable keepalive to match Go 1.12 behavior.
        client.Client.SetSocketOption(
            System.Net.Sockets.SocketOptionLevel.Socket,
            System.Net.Sockets.SocketOptionName.KeepAlive,
            false);

        using var cts = new CancellationTokenSource(timeout);
        await client.ConnectAsync(host, port, cts.Token);
        return client;
    }

    /// <summary>
    /// Parity wrapper for Go <c>natsDialTimeout</c>.
    /// Accepts a network label (tcp/tcp4/tcp6) and host:port address.
    /// </summary>
    public static Task<System.Net.Sockets.TcpClient> NatsDialTimeout(
        string network, string address, TimeSpan timeout)
    {
        if (!string.Equals(network, "tcp", StringComparison.OrdinalIgnoreCase) &&
            !string.Equals(network, "tcp4", StringComparison.OrdinalIgnoreCase) &&
            !string.Equals(network, "tcp6", StringComparison.OrdinalIgnoreCase))
            throw new NotSupportedException($"unsupported network: {network}");

        var (host, port, err) = ParseHostPort(address, defaultPort: 0);
        if (err != null || port <= 0)
            throw new InvalidOperationException($"invalid dial address: {address}", err);

        return NatsDialTimeoutAsync(host, port, timeout);
    }

    // -------------------------------------------------------------------------
    // URL redaction
    // -------------------------------------------------------------------------

    /// <summary>
    /// Returns a copy of <paramref name="unredacted"/> where any URL that
    /// contains a password has its password replaced with "xxxxx".
    /// Mirrors <c>redactURLList</c>.
    /// </summary>
    public static Uri[] RedactUrlList(Uri[] unredacted)
    {
        var r = new Uri[unredacted.Length];
        var needCopy = false;

        for (var i = 0; i < unredacted.Length; i++)
        {
            var u = unredacted[i];
            if (u?.UserInfo?.Contains(':') == true)
            {
                needCopy = true;
                var ui = u.UserInfo;
                var colon = ui.IndexOf(':');
                var username = ui[..colon];
                var b = new UriBuilder(u) { Password = "xxxxx", UserName = username };
                r[i] = b.Uri;
            }
            else
            {
                r[i] = u!;
            }
        }

        return needCopy ? r : unredacted;
    }

    /// <summary>
    /// Returns the URL string with the password component redacted ("xxxxx").
    /// Returns the original string if no password is present or it cannot be parsed.
    /// Mirrors <c>redactURLString</c>.
    /// </summary>
    public static string RedactUrlString(string raw)
    {
        if (!raw.Contains('@')) return raw;
        if (!Uri.TryCreate(raw, UriKind.Absolute, out var u)) return raw;

        if (!u.UserInfo.Contains(':')) return raw;

        var colon = u.UserInfo.IndexOf(':');
        var username = u.UserInfo[..colon];
        var b = new UriBuilder(u) { Password = "xxxxx", UserName = username };
        var result = b.Uri.ToString();
        // UriBuilder adds a trailing slash for authority-only URLs; strip it if the input had none.
        if (!raw.EndsWith('/') && result.EndsWith('/'))
            result = result[..^1];
        return result;
    }

    /// <summary>
    /// Returns the Host part of each URL in the list.
    /// Mirrors <c>getURLsAsString</c>.
    /// </summary>
    public static string[] GetUrlsAsString(Uri[] urls)
    {
        var result = new string[urls.Length];
        for (var i = 0; i < urls.Length; i++)
            result[i] = urls[i].Authority; // host:port
        return result;
    }

    // -------------------------------------------------------------------------
    // RefCountedUrlSet wrappers (Go parity mapping)
    // -------------------------------------------------------------------------

    /// <summary>
    /// Parity wrapper for <see cref="RefCountedUrlSet.AddUrl"/>.
    /// Mirrors <c>refCountedUrlSet.addUrl</c>.
    /// </summary>
    public static bool AddUrl(RefCountedUrlSet urlSet, string urlStr)
    {
        ArgumentNullException.ThrowIfNull(urlSet);
        return urlSet.AddUrl(urlStr);
    }

    /// <summary>
    /// Parity wrapper for <see cref="RefCountedUrlSet.RemoveUrl"/>.
    /// Mirrors <c>refCountedUrlSet.removeUrl</c>.
    /// </summary>
    public static bool RemoveUrl(RefCountedUrlSet urlSet, string urlStr)
    {
        ArgumentNullException.ThrowIfNull(urlSet);
        return urlSet.RemoveUrl(urlStr);
    }

    /// <summary>
    /// Parity wrapper for <see cref="RefCountedUrlSet.GetAsStringSlice"/>.
    /// Mirrors <c>refCountedUrlSet.getAsStringSlice</c>.
    /// </summary>
    public static string[] GetAsStringSlice(RefCountedUrlSet urlSet)
    {
        ArgumentNullException.ThrowIfNull(urlSet);
        return urlSet.GetAsStringSlice();
    }

    // -------------------------------------------------------------------------
    // INFO helpers
    // -------------------------------------------------------------------------

    /// <summary>
    /// Serialises <paramref name="info"/> into an INFO line (<c>INFO {...}\r\n</c>).
    /// Mirrors <c>generateInfoJSON</c>.
    /// </summary>
    public static byte[] GenerateInfoJSON(global::ZB.MOM.NatsNet.Server.ServerInfo info)
    {
        var json = JsonSerializer.Serialize(info);
        return Encoding.UTF8.GetBytes($"INFO {json}\r\n");
    }

    // -------------------------------------------------------------------------
    // Copy helpers
    // -------------------------------------------------------------------------

    /// <summary>
    /// Returns a copy of <paramref name="src"/>, or null if src is empty.
    /// Mirrors <c>copyBytes</c>.
    /// </summary>
    public static byte[]? CopyBytes(byte[]? src)
    {
        if (src == null || src.Length == 0) return null;
        var dst = new byte[src.Length];
        src.CopyTo(dst, 0);
        return dst;
    }

    /// <summary>
    /// Returns a copy of <paramref name="src"/>, or null if src is null.
    /// Mirrors <c>copyStrings</c>.
    /// </summary>
    public static string[]? CopyStrings(string[]? src)
    {
        if (src == null) return null;
        var dst = new string[src.Length];
        src.CopyTo(dst, 0);
        return dst;
    }

    // -------------------------------------------------------------------------
    // Parallel task queue
    // -------------------------------------------------------------------------

    /// <summary>
    /// Creates a bounded channel onto which tasks can be posted for parallel
    /// execution across a pool of dedicated threads. Close the returned channel
    /// to signal workers to stop (after queued items complete).
    /// Mirrors <c>parallelTaskQueue</c>.
    /// </summary>
    public static System.Threading.Channels.ChannelWriter<Action> CreateParallelTaskQueue(int maxParallelism = 0)
    {
        var mp = maxParallelism <= 0 ? Environment.ProcessorCount : Math.Max(Environment.ProcessorCount, maxParallelism);
        var channel = System.Threading.Channels.Channel.CreateBounded<Action>(mp);

        for (var i = 0; i < mp; i++)
        {
            Task.Run(async () =>
            {
                await foreach (var fn in channel.Reader.ReadAllAsync())
                    fn();
            });
        }

        return channel.Writer;
    }

    /// <summary>
    /// Parity wrapper for <see cref="CreateParallelTaskQueue"/>.
    /// Mirrors <c>parallelTaskQueue</c>.
    /// </summary>
    public static System.Threading.Channels.ChannelWriter<Action> ParallelTaskQueue(int maxParallelism = 0) =>
        CreateParallelTaskQueue(maxParallelism);
}

// -------------------------------------------------------------------------
// RefCountedUrlSet (mirrors refCountedUrlSet map[string]int in util.go)
// -------------------------------------------------------------------------

/// <summary>
/// A reference-counted set of URL strings used for gossip URL management.
/// Mirrors <c>refCountedUrlSet</c> in server/util.go.
/// </summary>
public sealed class RefCountedUrlSet
{
    private readonly Dictionary<string, int> _map = new();

    /// <summary>
    /// Adds <paramref name="urlStr"/>. Returns true if it was added for the first time.
    /// Mirrors <c>refCountedUrlSet.addUrl</c>.
    /// </summary>
    public bool AddUrl(string urlStr)
    {
        _map.TryGetValue(urlStr, out var count);
        _map[urlStr] = count + 1;
        return count == 0;
    }

    /// <summary>
    /// Decrements the reference count for <paramref name="urlStr"/>.
    /// Returns true if this was the last reference (entry removed).
    /// Mirrors <c>refCountedUrlSet.removeUrl</c>.
    /// </summary>
    public bool RemoveUrl(string urlStr)
    {
        if (!_map.TryGetValue(urlStr, out var count)) return false;
        if (count == 1) { _map.Remove(urlStr); return true; }
        _map[urlStr] = count - 1;
        return false;
    }

    /// <summary>
    /// Returns the unique URL strings currently in the set.
    /// Mirrors <c>refCountedUrlSet.getAsStringSlice</c>.
    /// </summary>
    public string[] GetAsStringSlice() => [.. _map.Keys];
}
