// Copyright 2019-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/leafnode.go in the NATS server Go source.

using System.Text.Json.Serialization;
using System.Threading;
using ZB.MOM.NatsNet.Server.Auth;
using ZB.MOM.NatsNet.Server.Internal;

namespace ZB.MOM.NatsNet.Server;

// ============================================================================
// Session 15: Leaf Nodes
// ============================================================================

/// <summary>
/// Per-connection leaf-node state embedded in <see cref="ClientConnection"/>
/// when the connection kind is <c>Leaf</c>.
/// Mirrors Go <c>leaf</c> struct in leafnode.go.
/// </summary>
internal sealed class Leaf
{
    /// <summary>
    /// Config for solicited (outbound) leaf connections; null for accepted connections.
    /// </summary>
    public LeafNodeCfg? Remote { get; set; }

    /// <summary>
    /// True when we are the spoke side of a hub/spoke leaf pair.
    /// </summary>
    public bool IsSpoke { get; set; }

    /// <summary>
    /// Cluster name of the remote server when we are a hub and the spoke is
    /// part of a cluster.
    /// </summary>
    public string RemoteCluster { get; set; } = string.Empty;

    /// <summary>Remote server name or ID.</summary>
    public string RemoteServer  { get; set; } = string.Empty;

    /// <summary>Domain name of the remote server.</summary>
    public string RemoteDomain  { get; set; } = string.Empty;

    /// <summary>Account name of the remote server.</summary>
    public string RemoteAccName { get; set; } = string.Empty;

    /// <summary>
    /// When true, suppresses propagation of east-west interest from other leaf nodes.
    /// </summary>
    public bool Isolated { get; set; }

    /// <summary>
    /// Subject-interest suppression map shared with the remote side.
    /// Key = subject, Value = interest count (positive = subscribe, negative = unsubscribe delta).
    /// </summary>
    public Dictionary<string, int> Smap { get; set; } = new();

    /// <summary>
    /// Short-lived set of subscriptions added during <c>initLeafNodeSmapAndSendSubs</c>
    /// to detect and avoid double-counting races.
    /// </summary>
    public HashSet<Subscription>? Tsub { get; set; }

    /// <summary>Timer that clears <see cref="Tsub"/> after the initialization window.</summary>
    public Timer? Tsubt { get; set; }

    /// <summary>
    /// Selected compression mode, which may differ from the server-configured mode.
    /// </summary>
    public string Compression { get; set; } = string.Empty;

    /// <summary>
    /// Gateway-mapped reply subscription used for GW reply routing via leaf nodes.
    /// </summary>
    public Subscription? GwSub { get; set; }
}

/// <summary>
/// Runtime configuration for a remote (solicited) leaf-node connection.
/// Wraps <see cref="RemoteLeafOpts"/> with connection-attempt state and a
/// reader-writer lock for concurrent access.
/// Mirrors Go <c>leafNodeCfg</c> struct in leafnode.go.
/// Replaces the stub that was in <c>NatsServerTypes.cs</c>.
/// </summary>
public sealed class LeafNodeCfg
{
    private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion);

    // -------------------------------------------------------------------------
    // Embedded RemoteLeafOpts fields
    // -------------------------------------------------------------------------

    /// <summary>The raw remote options this cfg was constructed from.</summary>
    public RemoteLeafOpts? RemoteOpts { get; set; }

    // -------------------------------------------------------------------------
    // Runtime connection-attempt fields
    // -------------------------------------------------------------------------

    /// <summary>Resolved URLs to attempt connections to.</summary>
    public List<Uri> Urls { get; set; } = [];

    /// <summary>Currently selected URL from <see cref="Urls"/>.</summary>
    public Uri? CurUrl { get; set; }

    /// <summary>TLS server name override for SNI.</summary>
    public string TlsName { get; set; } = string.Empty;

    /// <summary>Username for authentication (resolved from credentials or options).</summary>
    public string Username { get; set; } = string.Empty;

    /// <summary>Password for authentication (resolved from credentials or options).</summary>
    public string Password { get; set; } = string.Empty;

    /// <summary>Publish/subscribe permission overrides for this connection.</summary>
    public Permissions? Perms { get; set; }

    /// <summary>
    /// Delay before the next connection attempt (e.g. during loop-detection back-off).
    /// </summary>
    public TimeSpan ConnDelay { get; set; }

    /// <summary>
    /// Timer used to trigger JetStream account migration for this leaf.
    /// </summary>
    public Timer? JsMigrateTimer { get; set; }

    // -------------------------------------------------------------------------
    // Forwarded properties from RemoteLeafOpts
    // -------------------------------------------------------------------------

    public string   LocalAccount { get => RemoteOpts?.LocalAccount ?? string.Empty; }
    public bool     NoRandomize  { get => RemoteOpts?.NoRandomize  ?? false; }
    public string   Credentials  { get => RemoteOpts?.Credentials  ?? string.Empty; }
    public bool     Disabled     { get => RemoteOpts?.Disabled     ?? false; }

    // -------------------------------------------------------------------------
    // Lock helpers
    // -------------------------------------------------------------------------

    public void AcquireReadLock()    => _lock.EnterReadLock();
    public void ReleaseReadLock()    => _lock.ExitReadLock();
    public void AcquireWriteLock()   => _lock.EnterWriteLock();
    public void ReleaseWriteLock()   => _lock.ExitWriteLock();

    public Uri? PickNextURL()
    {
        AcquireWriteLock();
        try
        {
            if (Urls.Count == 0)
            {
                CurUrl = null;
                return null;
            }

            if (CurUrl is null)
            {
                CurUrl = Urls[0];
                return CurUrl;
            }

            var index = -1;
            for (var i = 0; i < Urls.Count; i++)
            {
                if (ServerUtilities.UrlsAreEqual(Urls[i], CurUrl))
                {
                    index = i;
                    break;
                }
            }
            if (index < 0 || index + 1 >= Urls.Count)
                CurUrl = Urls[0];
            else
                CurUrl = Urls[index + 1];

            return CurUrl;
        }
        finally
        {
            ReleaseWriteLock();
        }
    }

    public Uri? GetCurrentURL()
    {
        AcquireReadLock();
        try
        {
            return CurUrl;
        }
        finally
        {
            ReleaseReadLock();
        }
    }

    public TimeSpan GetConnectDelay()
    {
        AcquireReadLock();
        try
        {
            return ConnDelay;
        }
        finally
        {
            ReleaseReadLock();
        }
    }

    public void SetConnectDelay(TimeSpan delay)
    {
        AcquireWriteLock();
        try
        {
            ConnDelay = delay < TimeSpan.Zero ? TimeSpan.Zero : delay;
        }
        finally
        {
            ReleaseWriteLock();
        }
    }

    public void CancelMigrateTimer()
    {
        AcquireWriteLock();
        try
        {
            JsMigrateTimer?.Dispose();
            JsMigrateTimer = null;
        }
        finally
        {
            ReleaseWriteLock();
        }
    }

    public void SaveTLSHostname(Uri? remoteUrl)
    {
        if (remoteUrl is null || string.IsNullOrWhiteSpace(remoteUrl.Host))
            return;

        AcquireWriteLock();
        try
        {
            TlsName = remoteUrl.Host;
        }
        finally
        {
            ReleaseWriteLock();
        }
    }

    public void SaveUserPassword(Uri? remoteUrl)
    {
        AcquireWriteLock();
        try
        {
            if (remoteUrl?.UserInfo is not { Length: > 0 } userInfo)
            {
                Username = string.Empty;
                Password = string.Empty;
                return;
            }

            var userInfoParts = userInfo.Split(':', 2, StringSplitOptions.None);
            Username = userInfoParts[0];
            Password = userInfoParts.Length > 1 ? userInfoParts[1] : string.Empty;
        }
        finally
        {
            ReleaseWriteLock();
        }
    }
}

/// <summary>
/// CONNECT protocol payload sent by a leaf-node connection.
/// Fields map 1-to-1 with the JSON tags in Go's <c>leafConnectInfo</c>.
/// Mirrors Go <c>leafConnectInfo</c> struct in leafnode.go.
/// </summary>
internal sealed class LeafConnectInfo
{
    [JsonPropertyName("version")]        public string   Version       { get; set; } = string.Empty;
    [JsonPropertyName("nkey")]           public string   Nkey          { get; set; } = string.Empty;
    [JsonPropertyName("jwt")]            public string   Jwt           { get; set; } = string.Empty;
    [JsonPropertyName("sig")]            public string   Sig           { get; set; } = string.Empty;
    [JsonPropertyName("user")]           public string   User          { get; set; } = string.Empty;
    [JsonPropertyName("pass")]           public string   Pass          { get; set; } = string.Empty;
    [JsonPropertyName("auth_token")]     public string   Token         { get; set; } = string.Empty;
    [JsonPropertyName("server_id")]      public string   Id            { get; set; } = string.Empty;
    [JsonPropertyName("domain")]         public string   Domain        { get; set; } = string.Empty;
    [JsonPropertyName("name")]           public string   Name          { get; set; } = string.Empty;
    [JsonPropertyName("is_hub")]         public bool     Hub           { get; set; }
    [JsonPropertyName("cluster")]        public string   Cluster       { get; set; } = string.Empty;
    [JsonPropertyName("headers")]        public bool     Headers       { get; set; }
    [JsonPropertyName("jetstream")]      public bool     JetStream     { get; set; }
    [JsonPropertyName("deny_pub")]       public string[] DenyPub       { get; set; } = [];
    [JsonPropertyName("isolate")]        public bool     Isolate       { get; set; }

    /// <summary>
    /// Compression mode string.  The legacy boolean field was never used; this
    /// string field uses a different JSON tag to avoid conflicts.
    /// </summary>
    [JsonPropertyName("compress_mode")]  public string   Compression   { get; set; } = string.Empty;

    /// <summary>
    /// Used only to detect wrong-port connections (client connecting to leaf port).
    /// </summary>
    [JsonPropertyName("gateway")]        public string   Gateway       { get; set; } = string.Empty;

    /// <summary>Account name the remote is binding to on the accept side.</summary>
    [JsonPropertyName("remote_account")] public string   RemoteAccount { get; set; } = string.Empty;

    /// <summary>
    /// Protocol version sent by the soliciting side so the accepting side knows
    /// which features are supported (e.g. message tracing).
    /// </summary>
    [JsonPropertyName("protocol")]       public int      Proto         { get; set; }
}
