using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace NATS.E2E.Tests.Infrastructure;

/// <summary>
/// Manages a NATS.Server.Host child process for E2E testing.
/// Launches the server on an ephemeral port and polls TCP readiness.
/// </summary>
public sealed class NatsServerProcess : IAsyncDisposable
{
    private Process? _process;
    private readonly StringBuilder _output = new();
    private readonly object _outputLock = new();
    private readonly string[]? _extraArgs;
    private readonly string? _configContent;
    private readonly bool _enableMonitoring;
    private string? _configFilePath;

    public int Port { get; }

    public int? MonitorPort { get; }

    public string Output
    {
        get
        {
            lock (_outputLock)
                return _output.ToString();
        }
    }

    public NatsServerProcess(string[]? extraArgs = null, string? configContent = null, bool enableMonitoring = false)
    {
        Port = AllocateFreePort();
        _extraArgs = extraArgs;
        _configContent = configContent;
        _enableMonitoring = enableMonitoring;

        if (_enableMonitoring)
            MonitorPort = AllocateFreePort();
    }

    /// <summary>
    /// Convenience factory for creating a server with a config file.
    /// </summary>
    public static NatsServerProcess WithConfig(string configContent, bool enableMonitoring = false)
        => new(configContent: configContent, enableMonitoring: enableMonitoring);

    public async Task StartAsync()
    {
        var hostDll = ResolveHostDll();

        // Write config file if provided
        if (_configContent is not null)
        {
            _configFilePath = Path.Combine(Path.GetTempPath(), $"nats-e2e-{Guid.NewGuid():N}.conf");
            await File.WriteAllTextAsync(_configFilePath, _configContent);
        }

        // Build argument string
        var args = new StringBuilder($"exec \"{hostDll}\" -p {Port}");
        if (_configFilePath is not null)
            args.Append($" -c \"{_configFilePath}\"");
        if (_enableMonitoring && MonitorPort.HasValue)
            args.Append($" -m {MonitorPort.Value}");
        if (_extraArgs is not null)
        {
            foreach (var arg in _extraArgs)
                args.Append($" {arg}");
        }

        var psi = new ProcessStartInfo
        {
            FileName = "dotnet",
            Arguments = args.ToString(),
            UseShellExecute = false,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            CreateNoWindow = true,
        };

        _process = new Process { StartInfo = psi, EnableRaisingEvents = true };

        _process.OutputDataReceived += (_, e) =>
        {
            if (e.Data is not null)
                lock (_outputLock) _output.AppendLine(e.Data);
        };
        _process.ErrorDataReceived += (_, e) =>
        {
            if (e.Data is not null)
                lock (_outputLock) _output.AppendLine(e.Data);
        };

        _process.Start();
        _process.BeginOutputReadLine();
        _process.BeginErrorReadLine();

        await WaitForTcpReadyAsync();
    }

    public async ValueTask DisposeAsync()
    {
        if (_process is not null)
        {
            if (!_process.HasExited)
            {
                _process.Kill(entireProcessTree: true);

                using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
                try
                {
                    await _process.WaitForExitAsync(cts.Token);
                }
                catch (OperationCanceledException)
                {
                    // Already killed the tree above; nothing more to do
                }
            }

            _process.Dispose();
            _process = null;
        }

        // Clean up temp config file
        if (_configFilePath is not null && File.Exists(_configFilePath))
        {
            File.Delete(_configFilePath);
            _configFilePath = null;
        }
    }

    private async Task WaitForTcpReadyAsync()
    {
        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));

        while (!timeout.Token.IsCancellationRequested)
        {
            try
            {
                using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                await socket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, Port), timeout.Token);
                return; // Connected — server is ready
            }
            catch (SocketException)
            {
                await Task.Delay(100, timeout.Token);
            }
        }

        throw new TimeoutException(
            $"NATS server did not become ready on port {Port} within 10s.\n\nServer output:\n{Output}");
    }

    private static string ResolveHostDll()
    {
        // Walk up from test output directory to find solution root (contains NatsDotNet.slnx)
        var dir = new DirectoryInfo(AppContext.BaseDirectory);
        while (dir is not null)
        {
            if (File.Exists(Path.Combine(dir.FullName, "NatsDotNet.slnx")))
            {
                var dll = Path.Combine(dir.FullName, "src", "NATS.Server.Host", "bin", "Debug", "net10.0", "NATS.Server.Host.dll");
                if (File.Exists(dll))
                    return dll;

                // DLL not found — build it
                var build = Process.Start(new ProcessStartInfo
                {
                    FileName = "dotnet",
                    Arguments = "build src/NATS.Server.Host/NATS.Server.Host.csproj -c Debug",
                    WorkingDirectory = dir.FullName,
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                });
                build!.WaitForExit();

                if (build.ExitCode != 0)
                    throw new InvalidOperationException(
                        $"Failed to build NATS.Server.Host:\n{build.StandardError.ReadToEnd()}");

                if (File.Exists(dll))
                    return dll;

                throw new FileNotFoundException($"Built NATS.Server.Host but DLL not found at: {dll}");
            }

            dir = dir.Parent;
        }

        throw new FileNotFoundException(
            "Could not find solution root (NatsDotNet.slnx) walking up from " + AppContext.BaseDirectory);
    }

    internal static int AllocateFreePort()
    {
        using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
        return ((IPEndPoint)socket.LocalEndPoint!).Port;
    }
}
