using System.Diagnostics;
using Npgsql;

namespace NeonSprawl.Server.Tests.Game.PositionState;

/// <summary>
/// When not in CI, tries TCP to Postgres; if unreachable, runs <c>docker compose up -d</c> from the repo root.
/// Tracks whether compose was started so teardown can run <c>docker compose down</c>.
/// </summary>
/// <remarks>
/// All public entry points avoid <c>GetAwaiter().GetResult()</c> on the xUnit synchronization context;
/// <see cref="EnsurePostgresOrStartDockerBlocking"/> defers to the thread pool to prevent Rider/VS deadlocks.
/// </remarks>
internal static class PostgresDockerHelper
{
    private static readonly object StateLock = new();
    private static Task? _ensureTask;
    private static bool _composeStartedByUs;
    private static string? _repoRoot;

    /// <summary>Async path for <see cref="PostgresIntegrationHarness.InitializeAsync"/> (no deadlock).</summary>
    internal static Task EnsurePostgresOrStartDockerAsync(string? connectionString)
    {
        if (string.IsNullOrWhiteSpace(connectionString))
        {
            return Task.CompletedTask;
        }

        lock (StateLock)
        {
            _ensureTask ??= EnsureCoreAsync(connectionString.Trim());
            return _ensureTask;
        }
    }

    /// <summary>
    /// Used from <see cref="PostgresWebApplicationFactory.ConfigureWebHost"/> (sync only). Runs work on the thread pool
    /// so <c>Npgsql</c> / <c>Task</c> continuations do not post back to a blocked xUnit context.
    /// </summary>
    internal static void EnsurePostgresOrStartDockerBlocking(string? connectionString)
    {
        if (string.IsNullOrWhiteSpace(connectionString))
        {
            return;
        }

        Task.Run(() => EnsurePostgresOrStartDockerAsync(connectionString).GetAwaiter().GetResult())
            .GetAwaiter()
            .GetResult();
    }

    internal static async Task TearDownIfWeStartedAsync()
    {
        string? root;
        bool weStarted;
        lock (StateLock)
        {
            weStarted = _composeStartedByUs;
            root = _repoRoot;
            _composeStartedByUs = false;
            _repoRoot = null;
            _ensureTask = null;
        }

        if (!weStarted || string.IsNullOrEmpty(root))
        {
            return;
        }

        await RunDockerComposeAsync(root, "compose down", CancellationToken.None).ConfigureAwait(false);
    }

    private static async Task EnsureCoreAsync(string connectionString)
    {
        if (IsCiEnvironment())
        {
            await TryConnectWithTimeoutAsync(connectionString, TimeSpan.FromSeconds(30), CancellationToken.None)
                .ConfigureAwait(false);
            return;
        }

        if (await TryConnectOnceAsync(connectionString, CancellationToken.None).ConfigureAwait(false))
        {
            return;
        }

        var root = FindDockerComposeRoot();
        if (root is null)
        {
            throw new InvalidOperationException(
                "Postgres is not reachable and docker-compose.yml was not found by walking up from the test output directory.");
        }

        await RunDockerComposeAsync(root, "compose up -d", CancellationToken.None).ConfigureAwait(false);

        await TryConnectWithTimeoutAsync(connectionString, TimeSpan.FromSeconds(60), CancellationToken.None)
            .ConfigureAwait(false);

        lock (StateLock)
        {
            _composeStartedByUs = true;
            _repoRoot = root;
        }
    }

    private static bool IsCiEnvironment() =>
        string.Equals(Environment.GetEnvironmentVariable("CI"), "true", StringComparison.OrdinalIgnoreCase) ||
        string.Equals(Environment.GetEnvironmentVariable("GITHUB_ACTIONS"), "true", StringComparison.OrdinalIgnoreCase) ||
        string.Equals(Environment.GetEnvironmentVariable("GITLAB_CI"), "true", StringComparison.OrdinalIgnoreCase) ||
        string.Equals(Environment.GetEnvironmentVariable("TF_BUILD"), "True", StringComparison.OrdinalIgnoreCase);

    private static string? FindDockerComposeRoot()
    {
        var dir = new DirectoryInfo(AppContext.BaseDirectory);
        while (dir is not null)
        {
            var yml = Path.Combine(dir.FullName, "docker-compose.yml");
            if (File.Exists(yml))
            {
                try
                {
                    var text = File.ReadAllText(yml);
                    if (text.Contains("neon-sprawl-postgres", StringComparison.Ordinal) ||
                        text.Contains("postgres:", StringComparison.OrdinalIgnoreCase))
                    {
                        return dir.FullName;
                    }
                }
                catch (IOException)
                {
                    // keep walking
                }
            }

            dir = dir.Parent;
        }

        return null;
    }

    private static async Task RunDockerComposeAsync(string workingDirectory, string arguments, CancellationToken ct)
    {
        var psi = new ProcessStartInfo
        {
            FileName = "docker",
            Arguments = arguments,
            WorkingDirectory = workingDirectory,
            UseShellExecute = false,
            CreateNoWindow = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
        };

        using var proc = new Process { StartInfo = psi };
        if (!proc.Start())
        {
            throw new InvalidOperationException("Failed to start `docker` — is Docker installed and on PATH?");
        }

        await proc.WaitForExitAsync(ct).ConfigureAwait(false);
        if (proc.ExitCode != 0)
        {
            var err = await proc.StandardError.ReadToEndAsync(ct).ConfigureAwait(false);
            var @out = await proc.StandardOutput.ReadToEndAsync(ct).ConfigureAwait(false);
            throw new InvalidOperationException(
                $"`docker {arguments}` failed (exit {proc.ExitCode}). stderr: {err}. stdout: {@out}");
        }
    }

    private static async Task TryConnectWithTimeoutAsync(
        string connectionString,
        TimeSpan overall,
        CancellationToken ct)
    {
        var deadline = DateTime.UtcNow + overall;
        while (DateTime.UtcNow < deadline)
        {
            if (await TryConnectOnceAsync(connectionString, ct).ConfigureAwait(false))
            {
                return;
            }

            await Task.Delay(TimeSpan.FromMilliseconds(500), ct).ConfigureAwait(false);
        }

        throw new InvalidOperationException(
            $"Could not open a Postgres connection within {overall.TotalSeconds:0}s. Check the connection string and that the server is up.");
    }

    private static async Task<bool> TryConnectOnceAsync(string connectionString, CancellationToken ct)
    {
        try
        {
            await using var conn = new NpgsqlConnection(connectionString);
            await conn.OpenAsync(ct).ConfigureAwait(false);
            return true;
        }
        catch (NpgsqlException)
        {
            return false;
        }
    }
}
