using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;

/// <summary>
/// Caches one <see cref="SiteStreamGrpcClient"/> per site identifier.
/// The DebugStreamBridgeActor uses this factory to obtain (or create) a
/// gRPC client for a given site before opening a streaming subscription.
/// </summary>
public class SiteStreamGrpcClientFactory : IAsyncDisposable, IDisposable
{
    private readonly ConcurrentDictionary<string, SiteStreamGrpcClient> _clients = new();
    private readonly ILoggerFactory _loggerFactory;
    private readonly CommunicationOptions _options;

    /// <summary>
    /// Test/default constructor — uses default <see cref="CommunicationOptions"/>.
    /// </summary>
    /// <param name="loggerFactory">Logger factory passed to created clients.</param>
    public SiteStreamGrpcClientFactory(ILoggerFactory loggerFactory)
        : this(loggerFactory, Options.Create(new CommunicationOptions()))
    {
    }

    /// <summary>
    /// DI constructor — flows <see cref="CommunicationOptions"/> into every created
    /// <see cref="SiteStreamGrpcClient"/> so the configured gRPC keepalive settings
    /// are applied rather than hard-coded defaults.
    /// </summary>
    /// <param name="loggerFactory">Logger factory passed to created clients.</param>
    /// <param name="options">Communication options applied to each created client.</param>
    public SiteStreamGrpcClientFactory(ILoggerFactory loggerFactory, IOptions<CommunicationOptions> options)
    {
        _loggerFactory = loggerFactory;
        _options = options.Value;
    }

    /// <summary>
    /// Returns the cached client for the site, or creates a new one. If a client is
    /// already cached but bound to a *different* <paramref name="grpcEndpoint"/> — the
    /// NodeA→NodeB failover flip, or a site whose gRPC address was edited — the stale
    /// client is disposed and replaced with one bound to the requested endpoint.
    /// Communication-012/013: keying purely by site identifier and ignoring the
    /// endpoint on a cache hit defeated debug-stream node failover and meant a
    /// corrected gRPC address never took effect without a central restart.
    /// </summary>
    /// <param name="siteIdentifier">Unique site identifier used as the cache key.</param>
    /// <param name="grpcEndpoint">gRPC endpoint the returned client must be bound to.</param>
    /// <returns>The cached or newly-created client bound to <paramref name="grpcEndpoint"/>.</returns>
    public virtual SiteStreamGrpcClient GetOrCreate(string siteIdentifier, string grpcEndpoint)
    {
        // Fast path: a client is cached and already bound to the requested endpoint.
        if (_clients.TryGetValue(siteIdentifier, out var existing) &&
            string.Equals(existing.Endpoint, grpcEndpoint, StringComparison.Ordinal))
        {
            return existing;
        }

        // Either no client is cached, or the cached one is bound to a different
        // endpoint. AddOrUpdate atomically installs a client for the requested
        // endpoint; the prior (stale) client, if any, is disposed afterwards.
        SiteStreamGrpcClient? stale = null;
        var client = _clients.AddOrUpdate(
            siteIdentifier,
            _ => CreateClient(grpcEndpoint),
            (_, current) =>
            {
                if (string.Equals(current.Endpoint, grpcEndpoint, StringComparison.Ordinal))
                    return current;
                stale = current;
                return CreateClient(grpcEndpoint);
            });

        stale?.Dispose();
        return client;
    }

    /// <summary>
    /// Creates a single <see cref="SiteStreamGrpcClient"/>. Overridable so tests
    /// can substitute a tracking client while still exercising the factory's real
    /// caching and disposal machinery.
    /// </summary>
    /// <param name="grpcEndpoint">gRPC endpoint the new client will connect to.</param>
    /// <returns>A new <see cref="SiteStreamGrpcClient"/> connected to <paramref name="grpcEndpoint"/>.</returns>
    protected virtual SiteStreamGrpcClient CreateClient(string grpcEndpoint)
    {
        var logger = _loggerFactory.CreateLogger<SiteStreamGrpcClient>();
        return new SiteStreamGrpcClient(grpcEndpoint, logger, _options);
    }

    /// <summary>
    /// Removes and disposes the client for the given site. Site *address changes* are
    /// now handled transparently by <see cref="GetOrCreate"/> (it disposes and recreates
    /// a client whose endpoint no longer matches). This method remains the disposal
    /// path for full site *removal* — call it when a site record is deleted so its
    /// cached gRPC client does not linger for the life of the process.
    /// </summary>
    /// <param name="siteIdentifier">Unique site identifier whose client should be removed.</param>
    /// <returns>A task that completes when the cached client has been removed and disposed.</returns>
    public async Task RemoveSiteAsync(string siteIdentifier)
    {
        if (_clients.TryRemove(siteIdentifier, out var client))
        {
            await client.DisposeAsync();
        }
    }

    /// <summary>
    /// Asynchronously disposes all cached clients and clears the cache.
    /// </summary>
    /// <returns>A value task that completes when all clients have been disposed.</returns>
    public async ValueTask DisposeAsync()
    {
        foreach (var client in _clients.Values)
        {
            await client.DisposeAsync();
        }
        _clients.Clear();
    }

    /// <summary>
    /// Synchronous disposal. Communication-007: this used to block on
    /// <c>DisposeAsync().AsTask().GetAwaiter().GetResult()</c> (sync-over-async,
    /// a stall/deadlock risk during host shutdown). Each
    /// <see cref="SiteStreamGrpcClient"/> releases all of its resources
    /// synchronously, so we dispose them directly with no async path.
    /// </summary>
    public void Dispose()
    {
        foreach (var client in _clients.Values)
        {
            client.Dispose();
        }
        _clients.Clear();
    }
}
