using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
using Polly;
using System.Net.Http;
using System.Net.Security;
using System.Runtime.CompilerServices;
using System.Security.Cryptography.X509Certificates;

namespace ZB.MOM.WW.MxGateway.Client;

/// <summary>
/// Provides the .NET client entry point for the public Galaxy Repository gRPC API.
/// All RPCs are read-only metadata calls that share the gateway's API-key auth
/// interceptor and require the <c>metadata:read</c> scope server-side.
/// </summary>
public sealed class GalaxyRepositoryClient : IAsyncDisposable
{
    private const int DiscoverHierarchyPageSize = 5000;
    private const int BrowseChildrenPageSize = 500;

    private readonly GrpcChannel? _channel;
    private readonly IGalaxyRepositoryClientTransport _transport;
    private readonly ResiliencePipeline _safeUnaryRetryPipeline;
    private bool _disposed;

    /// <summary>
    /// Initializes a Galaxy Repository client with custom transport and options.
    /// </summary>
    /// <param name="options">Client options.</param>
    /// <param name="transport">The underlying gRPC transport.</param>
    internal GalaxyRepositoryClient(
        MxGatewayClientOptions options,
        IGalaxyRepositoryClientTransport transport)
    {
        ArgumentNullException.ThrowIfNull(options);
        options.Validate();

        Options = options;
        _transport = transport ?? throw new ArgumentNullException(nameof(transport));
        _safeUnaryRetryPipeline = MxGatewayClientRetryPolicy.Create(
            options.Retry,
            options.LoggerFactory?.CreateLogger<GalaxyRepositoryClient>());
        _channel = null;
    }

    private GalaxyRepositoryClient(
        GrpcChannel channel,
        IGalaxyRepositoryClientTransport transport)
    {
        _channel = channel;
        _transport = transport;
        Options = transport.Options;
        _safeUnaryRetryPipeline = MxGatewayClientRetryPolicy.Create(
            Options.Retry,
            Options.LoggerFactory?.CreateLogger<GalaxyRepositoryClient>());
    }

    /// <summary>
    /// Client options used to configure timeouts, authentication, and retry policy.
    /// </summary>
    public MxGatewayClientOptions Options { get; }

    /// <summary>
    /// The underlying generated gRPC client for advanced operations.
    /// </summary>
    public GalaxyRepository.GalaxyRepositoryClient RawClient =>
        _transport.RawClient
        ?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");

    /// <summary>
    /// Creates a Galaxy Repository client with the given options, establishing a new gRPC channel.
    /// </summary>
    /// <param name="options">Client options.</param>
    /// <returns>A new client instance.</returns>
    public static GalaxyRepositoryClient Create(MxGatewayClientOptions options)
    {
        ArgumentNullException.ThrowIfNull(options);
        options.Validate();

        HttpMessageHandler handler = CreateHttpHandler(options);
        var channel = GrpcChannel.ForAddress(
            options.Endpoint,
            new GrpcChannelOptions
            {
                HttpHandler = handler,
                LoggerFactory = options.LoggerFactory,
                MaxReceiveMessageSize = options.MaxGrpcMessageBytes,
                MaxSendMessageSize = options.MaxGrpcMessageBytes,
            });

        return new GalaxyRepositoryClient(
            channel,
            new GrpcGalaxyRepositoryClientTransport(
                options,
                new GalaxyRepository.GalaxyRepositoryClient(channel)));
    }

    /// <summary>
    /// Probes the Galaxy Repository database connection. Returns true when the
    /// gateway can reach the configured ZB SQL Server.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>True if connection is successful, false otherwise.</returns>
    public async Task<bool> TestConnectionAsync(CancellationToken cancellationToken = default)
    {
        TestConnectionReply reply = await TestConnectionRawAsync(
                new TestConnectionRequest(),
                cancellationToken)
            .ConfigureAwait(false);

        return reply.Ok;
    }

    /// <summary>
    /// Probes the Galaxy Repository database connection without result wrapping.
    /// </summary>
    /// <param name="request">The test connection request.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<TestConnectionReply> TestConnectionRawAsync(
        TestConnectionRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        ThrowIfDisposed();

        return ExecuteSafeUnaryAsync(
            token => _transport.TestConnectionAsync(request, CreateCallOptions(token)),
            cancellationToken);
    }

    /// <summary>
    /// Returns the timestamp of the most recent Galaxy deployment, or
    /// <see langword="null"/> when no deployment has been recorded.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The deployment timestamp, or null if not recorded.</returns>
    public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken cancellationToken = default)
    {
        GetLastDeployTimeReply reply = await GetLastDeployTimeRawAsync(
                new GetLastDeployTimeRequest(),
                cancellationToken)
            .ConfigureAwait(false);

        if (!reply.Present || reply.TimeOfLastDeploy is null)
        {
            return null;
        }

        return reply.TimeOfLastDeploy.ToDateTime();
    }

    /// <summary>
    /// Returns the most recent Galaxy deployment timestamp without result wrapping.
    /// </summary>
    /// <param name="request">The last deploy-time request.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<GetLastDeployTimeReply> GetLastDeployTimeRawAsync(
        GetLastDeployTimeRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        ThrowIfDisposed();

        return ExecuteSafeUnaryAsync(
            token => _transport.GetLastDeployTimeAsync(request, CreateCallOptions(token)),
            cancellationToken);
    }

    /// <summary>
    /// Enumerates the deployed Galaxy object hierarchy. Each <see cref="GalaxyObject"/>
    /// includes its dynamic attributes so callers can determine which tag references
    /// they may subscribe to via the MxAccessGateway service.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The collection of Galaxy objects in the hierarchy.</returns>
    public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(CancellationToken cancellationToken = default)
    {
        return await DiscoverHierarchyAsync(new DiscoverHierarchyOptions(), cancellationToken).ConfigureAwait(false);
    }

    /// <summary>Discovers the Galaxy object hierarchy.</summary>
    /// <param name="options">Client configuration options.</param>
    /// <param name="cancellationToken">Token to observe for cancellation.</param>
    /// <returns>The collection of Galaxy objects in the hierarchy.</returns>
    public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(
        DiscoverHierarchyOptions options,
        CancellationToken cancellationToken = default)
    {
        List<GalaxyObject> objects = [];
        HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
        string pageToken = string.Empty;
        do
        {
            DiscoverHierarchyRequest request = CreateDiscoverHierarchyRequest(options);
            request.PageSize = DiscoverHierarchyPageSize;
            request.PageToken = pageToken;
            DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync(
                    request,
                    cancellationToken)
                .ConfigureAwait(false);

            objects.AddRange(reply.Objects);
            pageToken = reply.NextPageToken;
            if (!string.IsNullOrWhiteSpace(pageToken)
                && !seenPageTokens.Add(pageToken))
            {
                throw new MxGatewayException(
                    $"Galaxy DiscoverHierarchy returned a repeated page token '{pageToken}'.");
            }
        }
        while (!string.IsNullOrWhiteSpace(pageToken));

        return objects;
    }

    private static DiscoverHierarchyRequest CreateDiscoverHierarchyRequest(DiscoverHierarchyOptions options)
    {
        ArgumentNullException.ThrowIfNull(options);

        DiscoverHierarchyRequest request = new()
        {
            AlarmBearingOnly = options.AlarmBearingOnly,
            HistorizedOnly = options.HistorizedOnly,
        };

        if (options.RootGobjectId.HasValue)
        {
            request.RootGobjectId = options.RootGobjectId.Value;
        }
        else if (!string.IsNullOrWhiteSpace(options.RootTagName))
        {
            request.RootTagName = options.RootTagName;
        }
        else if (!string.IsNullOrWhiteSpace(options.RootContainedPath))
        {
            request.RootContainedPath = options.RootContainedPath;
        }

        if (options.MaxDepth.HasValue)
        {
            request.MaxDepth = options.MaxDepth.Value;
        }

        request.CategoryIds.Add(options.CategoryIds);
        request.TemplateChainContains.Add(options.TemplateChainContains);
        if (!string.IsNullOrWhiteSpace(options.TagNameGlob))
        {
            request.TagNameGlob = options.TagNameGlob;
        }

        if (options.IncludeAttributes.HasValue)
        {
            request.IncludeAttributes = options.IncludeAttributes.Value;
        }

        return request;
    }

    /// <summary>
    /// Enumerates the Galaxy object hierarchy without result wrapping.
    /// </summary>
    /// <param name="request">The discover-hierarchy request.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<DiscoverHierarchyReply> DiscoverHierarchyRawAsync(
        DiscoverHierarchyRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        ThrowIfDisposed();

        return ExecuteSafeUnaryAsync(
            token => _transport.DiscoverHierarchyAsync(request, CreateCallOptions(token)),
            cancellationToken);
    }

    /// <summary>Returns root-level browse nodes (objects with no parent).</summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The list of root <see cref="LazyBrowseNode"/> instances.</returns>
    public Task<IReadOnlyList<LazyBrowseNode>> BrowseAsync(CancellationToken cancellationToken = default)
        => BrowseAsync(null, cancellationToken);

    /// <summary>Returns root-level browse nodes filtered by the given options.</summary>
    /// <param name="options">Browse filter options. Null applies no filter.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The list of root <see cref="LazyBrowseNode"/> instances.</returns>
    public async Task<IReadOnlyList<LazyBrowseNode>> BrowseAsync(
        BrowseChildrenOptions? options,
        CancellationToken cancellationToken = default)
    {
        BrowseChildrenOptions effective = options ?? new BrowseChildrenOptions();
        List<LazyBrowseNode> roots = [];
        string pageToken = string.Empty;
        HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
        do
        {
            BrowseChildrenRequest request = BuildBrowseChildrenRequest(effective);
            request.PageToken = pageToken;
            BrowseChildrenReply reply = await BrowseChildrenRawAsync(request, cancellationToken).ConfigureAwait(false);

            for (int i = 0; i < reply.Children.Count; i++)
            {
                bool hint = i < reply.ChildHasChildren.Count && reply.ChildHasChildren[i];
                roots.Add(new LazyBrowseNode(this, reply.Children[i], hint, effective));
            }

            pageToken = reply.NextPageToken;
            if (!string.IsNullOrWhiteSpace(pageToken) && !seenPageTokens.Add(pageToken))
            {
                throw new MxGatewayException(
                    $"Galaxy BrowseChildren returned a repeated page token '{pageToken}'.");
            }
        }
        while (!string.IsNullOrWhiteSpace(pageToken));

        return roots;
    }

    /// <summary>Issues a raw BrowseChildren RPC without result wrapping.</summary>
    /// <param name="request">The browse-children request.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The raw server reply.</returns>
    public Task<BrowseChildrenReply> BrowseChildrenRawAsync(
        BrowseChildrenRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        ThrowIfDisposed();

        return ExecuteSafeUnaryAsync(
            token => _transport.BrowseChildrenAsync(request, CreateCallOptions(token)),
            cancellationToken);
    }

    /// <summary>Builds a BrowseChildren request from the caller-supplied filter options.</summary>
    /// <param name="options">The browse-children filter options.</param>
    /// <returns>The populated request.</returns>
    internal static BrowseChildrenRequest BuildBrowseChildrenRequest(BrowseChildrenOptions options)
    {
        ArgumentNullException.ThrowIfNull(options);

        BrowseChildrenRequest request = new()
        {
            PageSize = BrowseChildrenPageSize,
            AlarmBearingOnly = options.AlarmBearingOnly,
            HistorizedOnly = options.HistorizedOnly,
        };
        request.CategoryIds.Add(options.CategoryIds);
        request.TemplateChainContains.Add(options.TemplateChainContains);
        if (!string.IsNullOrWhiteSpace(options.TagNameGlob))
        {
            request.TagNameGlob = options.TagNameGlob;
        }

        if (options.IncludeAttributes.HasValue)
        {
            request.IncludeAttributes = options.IncludeAttributes.Value;
        }

        return request;
    }

    /// <summary>
    /// Subscribes to Galaxy deploy events. The server emits a bootstrap event with the
    /// current state on subscribe so callers can prime their cache, then emits one event
    /// per new <c>time_of_last_deploy</c>. Pass <paramref name="lastSeenDeployTime"/> to
    /// suppress the bootstrap when the caller already holds the current deploy time.
    /// </summary>
    /// <remarks>
    /// Streaming RPCs are not wrapped by the unary safe-read retry pipeline. If the
    /// stream is interrupted the caller must reopen it; the server does not guarantee
    /// at-least-once delivery beyond the per-subscriber buffer (gaps in
    /// <see cref="DeployEvent.Sequence"/> indicate dropped events).
    /// </remarks>
    /// <param name="lastSeenDeployTime">Optional timestamp to suppress the bootstrap event.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>An async enumerable of deploy events.</returns>
    public IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
        DateTimeOffset? lastSeenDeployTime = null,
        CancellationToken cancellationToken = default)
    {
        ThrowIfDisposed();

        WatchDeployEventsRequest request = new();
        if (lastSeenDeployTime is { } seen)
        {
            request.LastSeenDeployTime = Timestamp.FromDateTimeOffset(seen);
        }

        return WatchDeployEventsRawAsync(request, cancellationToken);
    }

    /// <summary>
    /// Subscribes to Galaxy deploy events without result wrapping.
    /// </summary>
    /// <param name="request">The watch-deploy-events request.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>An async enumerable of raw deploy events.</returns>
    public IAsyncEnumerable<DeployEvent> WatchDeployEventsRawAsync(
        WatchDeployEventsRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        ThrowIfDisposed();

        return WatchDeployEventsCoreAsync(request, cancellationToken);
    }

    private async IAsyncEnumerable<DeployEvent> WatchDeployEventsCoreAsync(
        WatchDeployEventsRequest request,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await foreach (DeployEvent deployEvent in _transport
            .WatchDeployEventsAsync(request, CreateStreamCallOptions(cancellationToken))
            .WithCancellation(cancellationToken)
            .ConfigureAwait(false))
        {
            yield return deployEvent;
        }
    }

    /// <summary>
    /// Closes the gRPC channel and releases resources.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public ValueTask DisposeAsync()
    {
        if (_disposed)
        {
            return ValueTask.CompletedTask;
        }

        _disposed = true;
        _channel?.Dispose();
        return ValueTask.CompletedTask;
    }

    /// <summary>
    /// Creates gRPC call options with the client's default timeout and API-key authorization.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The call options.</returns>
    internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
    {
        return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
    }

    /// <summary>
    /// Creates gRPC call options for streaming RPCs with the stream timeout and API-key authorization.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The stream call options.</returns>
    internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken)
    {
        return CreateCallOptions(cancellationToken, Options.StreamTimeout);
    }

    /// <summary>
    /// Creates gRPC call options with the specified timeout and API-key authorization.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <param name="timeout">Optional timeout duration.</param>
    /// <returns>The call options.</returns>
    internal CallOptions CreateCallOptions(
        CancellationToken cancellationToken,
        TimeSpan? timeout)
    {
        Metadata headers = new()
        {
            { "authorization", $"Bearer {Options.ApiKey}" },
        };

        return new CallOptions(
            headers,
            timeout is null ? null : DateTime.UtcNow.Add(timeout.Value),
            cancellationToken);
    }

    private async Task<T> ExecuteSafeUnaryAsync<T>(
        Func<CancellationToken, Task<T>> call,
        CancellationToken cancellationToken)
    {
        using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeout.CancelAfter(Options.DefaultCallTimeout);

        return await _safeUnaryRetryPipeline.ExecuteAsync(
                async token => await call(token).ConfigureAwait(false),
                timeout.Token)
            .ConfigureAwait(false);
    }

    private static HttpMessageHandler CreateHttpHandler(MxGatewayClientOptions options) =>
        CreateHttpHandlerForTests(options);

    /// <summary>Creates the <see cref="SocketsHttpHandler"/> used for gRPC calls, exposed for test configuration.</summary>
    /// <param name="options">The client options supplying TLS and timeout settings.</param>
    /// <returns>The configured HTTP handler.</returns>
    internal static SocketsHttpHandler CreateHttpHandlerForTests(MxGatewayClientOptions options)
    {
        SocketsHttpHandler handler = new()
        {
            ConnectTimeout = options.ConnectTimeout,
        };

        if (options.UseTls)
        {
            handler.SslOptions = new SslClientAuthenticationOptions();
            if (!string.IsNullOrWhiteSpace(options.ServerNameOverride))
            {
                handler.SslOptions.TargetHost = options.ServerNameOverride;
            }

            if (!string.IsNullOrWhiteSpace(options.CaCertificatePath))
            {
                X509Certificate2 trustedRoot = X509CertificateLoader.LoadCertificateFromFile(options.CaCertificatePath);
                handler.SslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, errors) =>
                {
                    if ((errors & System.Net.Security.SslPolicyErrors.RemoteCertificateNameMismatch) != 0)
                    {
                        return false;
                    }

                    if (certificate is null)
                    {
                        return false;
                    }

                    using X509Chain customChain = new();
                    customChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
                    customChain.ChainPolicy.CustomTrustStore.Add(trustedRoot);
                    customChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
                    customChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
                    X509Certificate2 certificateToValidate = certificate as X509Certificate2
                        ?? X509CertificateLoader.LoadCertificate(certificate.Export(X509ContentType.Cert));
                    return customChain.Build(certificateToValidate);
                };
            }
            else if (!options.RequireCertificateValidation)
            {
                handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true;
            }
        }

        return handler;
    }

    private void ThrowIfDisposed()
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
    }
}
