using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using Polly;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace ZB.MOM.WW.MxGateway.Client;

/// <summary>
/// Provides the .NET client entry point for the public MXAccess Gateway gRPC API.
/// </summary>
public sealed class MxGatewayClient : IAsyncDisposable
{
    private readonly GrpcChannel _channel;
    private readonly IMxGatewayClientTransport _transport;
    private readonly ResiliencePipeline _safeUnaryRetryPipeline;
    private bool _disposed;

    /// <summary>
    /// Initializes a new instance of the <see cref="MxGatewayClient"/> with given options and transport.
    /// </summary>
    /// <param name="options">Client configuration options.</param>
    /// <param name="transport">Transport implementation for gateway communication.</param>
    internal MxGatewayClient(
        MxGatewayClientOptions options,
        IMxGatewayClientTransport transport)
    {
        ArgumentNullException.ThrowIfNull(options);
        options.Validate();

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

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

    /// <summary>
    /// Gets the client configuration options.
    /// </summary>
    public MxGatewayClientOptions Options { get; }

    /// <summary>
    /// Gets the underlying generated gRPC client.
    /// </summary>
    public MxAccessGateway.MxAccessGatewayClient RawClient =>
        _transport.RawClient
        ?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");

    /// <summary>
    /// Creates a new gateway client with the given options.
    /// </summary>
    /// <param name="options">Client configuration options.</param>
    /// <returns>A new gateway client instance.</returns>
    public static MxGatewayClient 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 MxGatewayClient(
            channel,
            new GrpcMxGatewayClientTransport(
                options,
                new MxAccessGateway.MxAccessGatewayClient(channel)));
    }

    /// <summary>
    /// Opens a new gateway session.
    /// </summary>
    /// <param name="request">Session open request; defaults to empty request if null.</param>
    /// <param name="cancellationToken">Cancellation token for the operation.</param>
    /// <returns>A wrapped gateway session.</returns>
    public async Task<MxGatewaySession> OpenSessionAsync(
        OpenSessionRequest? request = null,
        CancellationToken cancellationToken = default)
    {
        OpenSessionReply reply = await OpenSessionRawAsync(
                request ?? new OpenSessionRequest(),
                cancellationToken)
            .ConfigureAwait(false);

        return new MxGatewaySession(this, reply);
    }

    /// <summary>
    /// Opens a new gateway session and returns the raw protobuf reply.
    /// </summary>
    /// <param name="request">Session open request.</param>
    /// <param name="cancellationToken">Cancellation token for the operation.</param>
    /// <returns>The raw gateway session open reply.</returns>
    public Task<OpenSessionReply> OpenSessionRawAsync(
        OpenSessionRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        ThrowIfDisposed();

        return _transport.OpenSessionAsync(request, CreateCallOptions(cancellationToken));
    }

    /// <summary>
    /// Closes an open gateway session.
    /// </summary>
    /// <param name="request">Session close request.</param>
    /// <param name="cancellationToken">Cancellation token for the operation.</param>
    /// <returns>The session close reply.</returns>
    public Task<CloseSessionReply> CloseSessionRawAsync(
        CloseSessionRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        ThrowIfDisposed();

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

    /// <summary>
    /// Invokes an MXAccess command on the open session.
    /// </summary>
    /// <param name="request">The command request.</param>
    /// <param name="cancellationToken">Cancellation token for the operation.</param>
    /// <returns>The command reply.</returns>
    public Task<MxCommandReply> InvokeAsync(
        MxCommandRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        ThrowIfDisposed();

        if (MxGatewayClientRetryPolicy.IsRetryableCommand(request.Command?.Kind ?? MxCommandKind.Unspecified))
        {
            return ExecuteSafeUnaryAsync(
                token => _transport.InvokeAsync(request, CreateCallOptions(token)),
                cancellationToken);
        }

        return _transport.InvokeAsync(request, CreateCallOptions(cancellationToken));
    }

    /// <summary>
    /// Streams events from the gateway session.
    /// </summary>
    /// <param name="request">The stream events request.</param>
    /// <param name="cancellationToken">Cancellation token for the operation.</param>
    /// <returns>An async enumerable of events.</returns>
    public IAsyncEnumerable<MxEvent> StreamEventsAsync(
        StreamEventsRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        ThrowIfDisposed();

        return _transport.StreamEventsAsync(request, CreateStreamCallOptions(cancellationToken));
    }

    /// <summary>
    /// Acknowledges an active MXAccess alarm condition through the gateway. The
    /// gateway authenticates the request against the API key's <c>invoke:alarm-ack</c>
    /// scope and forwards the acknowledge to the worker's MXAccess session;
    /// the resulting <see cref="MxStatusProxy"/> is returned in the reply.
    /// </summary>
    /// <param name="request">The acknowledge request — alarm reference, comment, operator user.</param>
    /// <param name="cancellationToken">Cancellation token for the operation.</param>
    /// <returns>The acknowledge reply with protocol + native MxStatus.</returns>
    public Task<AcknowledgeAlarmReply> AcknowledgeAlarmAsync(
        AcknowledgeAlarmRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        ThrowIfDisposed();

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

    /// <summary>
    /// Streams a snapshot of all alarms currently Active or ActiveAcked — the gateway's
    /// ConditionRefresh equivalent. Used after reconnect to seed the local Part 9 state
    /// machine, or to reconcile alarms that may have been missed during a transport
    /// blip. Optionally scoped by alarm-reference prefix
    /// (<see cref="QueryActiveAlarmsRequest.AlarmFilterPrefix"/>) so a partial refresh
    /// can target an equipment sub-tree.
    /// </summary>
    /// <param name="request">The query request, optionally scoped by alarm-reference prefix.</param>
    /// <param name="cancellationToken">Cancellation token for the stream.</param>
    /// <returns>An async enumerable of active-alarm snapshots.</returns>
    public IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync(
        QueryActiveAlarmsRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        ThrowIfDisposed();

        return _transport.QueryActiveAlarmsAsync(request, CreateStreamCallOptions(cancellationToken));
    }

    /// <summary>
    /// Attaches to the gateway's central alarm feed. The stream opens with one
    /// <see cref="AlarmFeedMessage"/> per currently-active alarm (the
    /// ConditionRefresh snapshot), then a single <c>snapshot_complete</c>, then a
    /// <c>transition</c> for every subsequent raise / acknowledge / clear. Served
    /// by the gateway's always-on alarm monitor — no worker session is opened, so
    /// any number of clients may attach. Optionally scoped by alarm-reference
    /// prefix (<see cref="StreamAlarmsRequest.AlarmFilterPrefix"/>).
    /// </summary>
    /// <param name="request">The stream request, optionally scoped by alarm-reference prefix.</param>
    /// <param name="cancellationToken">Cancellation token for the stream.</param>
    /// <returns>An async enumerable of alarm feed messages.</returns>
    public IAsyncEnumerable<AlarmFeedMessage> StreamAlarmsAsync(
        StreamAlarmsRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        ThrowIfDisposed();

        return _transport.StreamAlarmsAsync(request, CreateStreamCallOptions(cancellationToken));
    }

    /// <summary>
    /// Disposes the client and releases all 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 default timeout and authorization.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token for the call.</param>
    /// <returns>Configured call options.</returns>
    internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
    {
        return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
    }

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

    /// <summary>
    /// Creates gRPC call options with specified timeout and authorization.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token for the call.</param>
    /// <param name="timeout">Optional timeout duration; null means no timeout.</param>
    /// <returns>Configured 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 internally so tests can inspect it.
    /// </summary>
    /// <param name="options">The client options that configure connect timeout and TLS behavior.</param>
    /// <returns>A configured <see cref="SocketsHttpHandler"/>.</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);
    }
}
