// Copyright 2012-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.
//
// Mirrors Go supercluster struct and createJetStreamSuperCluster* helpers from
// server/jetstream_helpers_test.go.

using ZB.MOM.NatsNet.Server;

namespace ZB.MOM.NatsNet.Server.IntegrationTests.Helpers;

/// <summary>
/// Represents a multi-cluster super-cluster connected via NATS gateways.
/// Mirrors Go <c>supercluster</c> struct from server/jetstream_helpers_test.go.
/// </summary>
internal sealed class TestSuperCluster : IDisposable
{
    // =========================================================================
    // Properties
    // =========================================================================

    /// <summary>All clusters that form this super-cluster.</summary>
    public TestCluster[] Clusters { get; }

    private bool _disposed;

    // =========================================================================
    // Constructor
    // =========================================================================

    private TestSuperCluster(TestCluster[] clusters)
    {
        Clusters = clusters;
    }

    // =========================================================================
    // Static factory
    // =========================================================================

    /// <summary>
    /// Creates a JetStream super-cluster consisting of <paramref name="numClusters"/> clusters,
    /// each with <paramref name="numPerCluster"/> servers, connected via gateways.
    /// Cluster names are C1, C2, … Cn.
    /// Mirrors Go <c>createJetStreamSuperCluster</c>.
    /// </summary>
    public static TestSuperCluster CreateJetStreamSuperCluster(int numPerCluster, int numClusters)
    {
        if (numClusters <= 1)
            throw new ArgumentException("numClusters must be > 1.", nameof(numClusters));
        if (numPerCluster < 1)
            throw new ArgumentException("numPerCluster must be >= 1.", nameof(numPerCluster));

        // Allocate gateway ports — one per server across all clusters.
        var totalServers = numClusters * numPerCluster;
        var gatewayPorts = new int[totalServers];
        for (var i = 0; i < totalServers; i++)
            gatewayPorts[i] = TestServerHelper.GetFreePort();

        // Build gateway remote-entry lines for each cluster.
        // Each cluster has numPerCluster gateway ports.
        var gwEntries = new string[numClusters];
        for (var ci = 0; ci < numClusters; ci++)
        {
            var clusterName = $"C{ci + 1}";
            var baseIndex   = ci * numPerCluster;
            var urls = string.Join(
                ",",
                Enumerable.Range(baseIndex, numPerCluster)
                          .Select(idx => $"nats-gw://127.0.0.1:{gatewayPorts[idx]}"));

            gwEntries[ci] = string.Format(
                ConfigHelper.JsGatewayEntryTemplate,
                "\n\t\t\t",
                clusterName,
                urls);
        }
        var allGwConf = string.Join(string.Empty, gwEntries);

        // Create each cluster with the super-cluster gateway wrapper.
        var clusters = new TestCluster[numClusters];

        for (var ci = 0; ci < numClusters; ci++)
        {
            var clusterName  = $"C{ci + 1}";
            var gwBaseIndex  = ci * numPerCluster;

            // Allocate cluster-route ports for this sub-cluster.
            var clusterPorts = Enumerable.Range(0, numPerCluster)
                                         .Select(_ => TestServerHelper.GetFreePort())
                                         .ToArray();
            var routeUrls = string.Join(
                ",",
                clusterPorts.Select(p => $"nats-route://127.0.0.1:{p}"));

            var servers = new NatsServer[numPerCluster];
            var opts    = new ServerOptions[numPerCluster];

            for (var si = 0; si < numPerCluster; si++)
            {
                var serverName = $"{clusterName}-S{si + 1}";
                var storeDir   = TestServerHelper.CreateTempDir($"js-sc-{clusterName}-{si + 1}-");
                var gwPort     = gatewayPorts[gwBaseIndex + si];

                // Inner cluster config (using JsClusterTemplate).
                var innerConf = string.Format(
                    ConfigHelper.JsClusterTemplate,
                    serverName,
                    storeDir,
                    clusterName,
                    clusterPorts[si],
                    routeUrls);

                // Wrap with super-cluster template (gateway section).
                var fullConf = string.Format(
                    ConfigHelper.JsSuperClusterTemplate,
                    innerConf,
                    clusterName,
                    gwPort,
                    allGwConf);

                var configFile = ConfigHelper.CreateConfigFile(fullConf);

                var serverOpts = new ServerOptions
                {
                    ServerName = serverName,
                    Host       = "127.0.0.1",
                    Port       = -1,
                    NoLog      = true,
                    NoSigs     = true,
                    JetStream  = true,
                    StoreDir   = storeDir,
                    ConfigFile = configFile,
                    Cluster = new ClusterOpts
                    {
                        Name = clusterName,
                        Host = "127.0.0.1",
                        Port = clusterPorts[si],
                    },
                    Gateway = new GatewayOpts
                    {
                        Name = clusterName,
                        Host = "127.0.0.1",
                        Port = gwPort,
                        Gateways = Enumerable.Range(0, numClusters)
                            .Where(gci => gci != ci)
                            .Select(gci =>
                            {
                                var remoteName = $"C{gci + 1}";
                                var remoteBase = gci * numPerCluster;
                                return new RemoteGatewayOpts
                                {
                                    Name = remoteName,
                                    Urls = Enumerable.Range(remoteBase, numPerCluster)
                                        .Select(idx => new Uri($"nats-gw://127.0.0.1:{gatewayPorts[idx]}"))
                                        .ToList(),
                                };
                            })
                            .ToList(),
                    },
                    Routes = clusterPorts
                        .Where((_, idx) => idx != si)
                        .Select(p => new Uri($"nats-route://127.0.0.1:{p}"))
                        .ToList(),
                };

                var (server, _) = TestServerHelper.RunServer(serverOpts);
                servers[si] = server;
                opts[si]    = serverOpts;
            }

            clusters[ci] = TestCluster.FromServers(servers, opts, clusterName);
        }

        var sc = new TestSuperCluster(clusters);
        sc.WaitOnLeader();
        return sc;
    }

    // =========================================================================
    // Accessors
    // =========================================================================

    /// <summary>
    /// Finds the JetStream meta-leader across all clusters.
    /// Returns null if no leader is elected.
    /// Mirrors Go <c>sc.leader()</c>.
    /// </summary>
    public NatsServer? Leader()
    {
        foreach (var c in Clusters)
        {
            var l = c.Leader();
            if (l != null) return l;
        }
        return null;
    }

    /// <summary>
    /// Returns a random running server from a random cluster.
    /// Mirrors Go <c>sc.randomServer()</c>.
    /// </summary>
    public NatsServer RandomServer()
    {
        var cluster = Clusters[Random.Shared.Next(Clusters.Length)];
        return cluster.RandomServer();
    }

    /// <summary>
    /// Searches all clusters for a server with the given name.
    /// Mirrors Go <c>sc.serverByName</c>.
    /// </summary>
    public NatsServer? ServerByName(string name)
    {
        foreach (var c in Clusters)
        {
            var s = c.ServerByName(name);
            if (s != null) return s;
        }
        return null;
    }

    /// <summary>
    /// Returns the <see cref="TestCluster"/> with the given cluster name (e.g. "C1").
    /// Mirrors Go <c>sc.clusterForName</c>.
    /// </summary>
    public TestCluster? ClusterForName(string name)
    {
        foreach (var c in Clusters)
        {
            if (c.Name == name) return c;
        }
        return null;
    }

    // =========================================================================
    // Wait helpers
    // =========================================================================

    /// <summary>
    /// Waits until a JetStream meta-leader is elected across all clusters.
    /// Mirrors Go <c>sc.waitOnLeader()</c>.
    /// </summary>
    public void WaitOnLeader()
    {
        CheckHelper.CheckFor(TimeSpan.FromSeconds(30), TimeSpan.FromMilliseconds(100), () =>
        {
            if (Leader() == null)
                return new Exception("SuperCluster: no JetStream meta-leader elected yet.");
            return null;
        });
    }

    /// <summary>
    /// Waits until the named stream has an elected leader across all clusters.
    /// Mirrors Go <c>sc.waitOnStreamLeader</c>.
    /// </summary>
    public void WaitOnStreamLeader(string account, string stream)
    {
        CheckHelper.CheckFor(TimeSpan.FromSeconds(30), TimeSpan.FromMilliseconds(100), () =>
        {
            foreach (var c in Clusters)
            {
                if (c.StreamLeader(account, stream) != null) return null;
            }
            return new Exception(
                $"SuperCluster: no leader for stream '{stream}' in account '{account}'.");
        });
    }

    // =========================================================================
    // Lifecycle
    // =========================================================================

    /// <inheritdoc/>
    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;

        foreach (var c in Clusters)
        {
            try { c.Dispose(); } catch { /* best effort */ }
        }
    }
}
