using System.Net.Http.Json;
using System.Text.Json.Serialization;
using NATS.Client.Core;

namespace NATS.E2E.Cluster.Tests.Infrastructure;

public sealed class GatewayPairFixture : IAsyncLifetime
{
    private readonly NatsServerProcess?[] _servers = new NatsServerProcess?[2];
    private readonly int[] _clientPorts = new int[2];
    private readonly int[] _gatewayPorts = new int[2];
    private readonly int[] _monitorPorts = new int[2];
    private readonly string[] _configs = new string[2];

    public async Task InitializeAsync()
    {
        // Pre-allocate all ports upfront so they can be reused across kill/restart
        for (var i = 0; i < 2; i++)
        {
            _clientPorts[i] = NatsServerProcess.AllocateFreePort();
            _gatewayPorts[i] = NatsServerProcess.AllocateFreePort();
            _monitorPorts[i] = NatsServerProcess.AllocateFreePort();
        }

        _configs[0] = $$"""
            server_name: gw-a
            gateway {
                name: cluster-a
                listen: 127.0.0.1:{{_gatewayPorts[0]}}
                gateways: [
                    { name: cluster-b, url: nats://127.0.0.1:{{_gatewayPorts[1]}} }
                ]
            }
            """;

        _configs[1] = $$"""
            server_name: gw-b
            gateway {
                name: cluster-b
                listen: 127.0.0.1:{{_gatewayPorts[1]}}
                gateways: [
                    { name: cluster-a, url: nats://127.0.0.1:{{_gatewayPorts[0]}} }
                ]
            }
            """;

        for (var i = 0; i < 2; i++)
        {
            _servers[i] = new NatsServerProcess(
                _clientPorts[i],
                configContent: _configs[i],
                enableMonitoring: true,
                monitorPort: _monitorPorts[i]);
        }

        await Task.WhenAll(_servers.Select(s => s!.StartAsync()));

        await WaitForGatewayConnectionAsync();
    }

    /// <summary>
    /// Kills the server at the given index (0=A, 1=B) by disposing it.
    /// </summary>
    public async Task KillNode(int index)
    {
        if (_servers[index] is { } server)
        {
            await server.DisposeAsync();
            _servers[index] = null;
        }
    }

    /// <summary>
    /// Restarts the server at the given index on its originally allocated ports.
    /// </summary>
    public async Task RestartNode(int index)
    {
        // Dispose any existing process first (idempotent)
        if (_servers[index] is not null)
            await KillNode(index);

        _servers[index] = new NatsServerProcess(
            _clientPorts[index],
            configContent: _configs[index],
            enableMonitoring: true,
            monitorPort: _monitorPorts[index]);

        await _servers[index]!.StartAsync();
    }

    /// <summary>
    /// Polls /gatewayz on alive nodes until NumGateways >= 1 for each.
    /// Times out after 30 seconds.
    /// </summary>
    public async Task WaitForGatewayConnectionAsync()
    {
        using var http = new HttpClient();
        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
        using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(200));

        while (await timer.WaitForNextTickAsync(timeout.Token).ConfigureAwait(false))
        {
            var allConnected = true;

            for (var i = 0; i < 2; i++)
            {
                // Skip killed nodes
                if (_servers[i] is null)
                    continue;

                try
                {
                    var gatewayz = await http.GetFromJsonAsync<Gatewayz>(
                        $"http://127.0.0.1:{_monitorPorts[i]}/gatewayz",
                        timeout.Token);

                    if (gatewayz?.NumGateways < 1)
                    {
                        allConnected = false;
                        break;
                    }
                }
                catch (HttpRequestException)
                {
                    // Monitor not yet ready — retry on next tick
                    allConnected = false;
                    break;
                }
            }

            if (allConnected)
                return;
        }

        throw new TimeoutException("Gateways did not connect to each other within 30s.");
    }

    /// <summary>
    /// Creates a NatsConnection pointed at server A (index 0).
    /// </summary>
    public NatsConnection CreateClientA()
        => new(new NatsOpts { Url = $"nats://127.0.0.1:{_clientPorts[0]}" });

    /// <summary>
    /// Creates a NatsConnection pointed at server B (index 1).
    /// </summary>
    public NatsConnection CreateClientB()
        => new(new NatsOpts { Url = $"nats://127.0.0.1:{_clientPorts[1]}" });

    public async Task DisposeAsync()
    {
        var disposeTasks = _servers
            .Where(s => s is not null)
            .Select(s => s!.DisposeAsync().AsTask());

        await Task.WhenAll(disposeTasks);
    }

    private sealed class Gatewayz
    {
        [JsonPropertyName("num_gateways")]
        public int NumGateways { get; init; }
    }
}

[CollectionDefinition("E2E-GatewayPair")]
public class GatewayPairCollection : ICollectionFixture<GatewayPairFixture>;
