using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace ZB.MOM.WW.Health;

/// <summary>
/// Health check that verifies a downstream gRPC dependency is reachable over its
/// <see cref="GrpcChannel"/>.
/// </summary>
/// <remarks>
/// <para>
/// The probe is injectable via <see cref="GrpcDependencyOptions.Probe"/>; the default drives the
/// channel to a connected state with <see cref="GrpcChannel.ConnectAsync"/>. The result is
/// <see cref="HealthStatus.Healthy"/> when the probe returns <c>true</c>, and
/// <see cref="HealthStatus.Unhealthy"/> when it returns <c>false</c>, throws any exception
/// (<see cref="RpcException"/> or otherwise), or times out within
/// <see cref="GrpcDependencyOptions.Timeout"/>. External cancellation of the supplied
/// <see cref="CancellationToken"/> propagates as an <see cref="OperationCanceledException"/>.
/// </para>
/// <para>
/// Recommended registration tag: <see cref="ZbHealthTags.Ready"/> only — downstream gRPC
/// reachability is a readiness concern; the <see cref="ZbHealthTags.Active"/> tier is reserved for
/// the leader / active-node probe. The registrant applies the tag.
/// </para>
/// </remarks>
public sealed class GrpcDependencyHealthCheck : IHealthCheck
{
    private readonly GrpcChannel _channel;
    private readonly GrpcDependencyOptions _options;

    /// <summary>Initializes a new <see cref="GrpcDependencyHealthCheck"/>.</summary>
    /// <param name="channel">The gRPC channel to the downstream dependency.</param>
    /// <param name="options">
    /// Probe, dependency name, and timeout. When <c>null</c>, defaults are used (the default probe is
    /// <see cref="GrpcChannel.ConnectAsync"/> with a 5 s timeout).
    /// </param>
    public GrpcDependencyHealthCheck(GrpcChannel channel, GrpcDependencyOptions? options = null)
    {
        _channel = channel ?? throw new ArgumentNullException(nameof(channel));
        _options = options ?? new GrpcDependencyOptions();
    }

    /// <inheritdoc />
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var name = _options.DependencyName ?? "gRPC dependency";
        var probe = _options.Probe ?? DefaultProbeAsync;

        using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeoutCts.CancelAfter(_options.Timeout);

        try
        {
            var reachable = await probe(_channel, timeoutCts.Token).ConfigureAwait(false);
            return reachable
                ? HealthCheckResult.Healthy($"{name} is reachable.")
                : HealthCheckResult.Unhealthy($"{name} is unreachable.");
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled && cancellationToken.IsCancellationRequested)
        {
            throw new OperationCanceledException(cancellationToken);
        }
        catch (RpcException ex)
        {
            return HealthCheckResult.Unhealthy($"{name} probe failed: {ex.Status.StatusCode}.", ex);
        }
        catch (OperationCanceledException ex) when (timeoutCts.IsCancellationRequested)
        {
            return HealthCheckResult.Unhealthy($"{name} probe timed out after {_options.Timeout}.", ex);
        }
        catch (Exception ex)
        {
            // Catch-all to match the sibling DatabaseHealthCheck: any other probe error
            // (e.g. InvalidOperationException / HttpRequestException / SocketException from the
            // transport, or anything a custom probe throws) maps to Unhealthy rather than escaping
            // the IHealthCheck boundary. The OCE/Rpc external-cancellation handlers above run first,
            // so caller cancellation still propagates.
            return HealthCheckResult.Unhealthy($"{name} probe failed: {ex.Message}", ex);
        }
    }

    /// <summary>
    /// Default probe: connects the channel and reports reachability. Returns <c>true</c> once the
    /// channel reaches a connected state; surfaces failures as a thrown exception (handled by the caller).
    /// </summary>
    private static async Task<bool> DefaultProbeAsync(GrpcChannel channel, CancellationToken cancellationToken)
    {
        await channel.ConnectAsync(cancellationToken).ConfigureAwait(false);
        return true;
    }
}
