using Akka.Actor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;

namespace ZB.MOM.WW.ScadaBridge.Host.Health;

/// <summary>
/// Readiness check that verifies every <b>required central cluster
/// singleton</b> is reachable from this node, satisfying the "required cluster
/// singletons running (if applicable)" clause of REQ-HOST-4a. Register it
/// <see cref="ZB.MOM.WW.Health.ZbHealthTags.Ready"/>-tagged in the Central-role
/// <c>AddHealthChecks()</c> chain only, so it is naturally role-scoped (site nodes
/// never register it).
/// </summary>
/// <remarks>
/// <para>
/// <b>Probe strategy.</b> Each central singleton has a local
/// <c>ClusterSingletonProxy</c> actor (created unconditionally in
/// <c>AkkaHostedService.RegisterCentralActors</c>). The proxy actor exists locally
/// as soon as it is created, so merely resolving its path proves nothing about the
/// singleton itself. Instead we <see cref="ActorRefImplicitSenderExtensions.Ask{T}(ICanTell, object, TimeSpan?)"/>
/// the proxy an <see cref="Identify"/> with a short bounded per-singleton timeout and
/// expect an <see cref="ActorIdentity"/> whose <see cref="ActorIdentity.Subject"/> is
/// non-null. The proxy buffers and forwards to the live singleton, so a non-null
/// Subject within the timeout means the singleton is running and reachable; a null
/// Subject or a timeout means it is unreachable. Probes run concurrently
/// (<see cref="Task.WhenAll(System.Collections.Generic.IEnumerable{Task})"/>) so the
/// whole check stays cheap and readiness polling stays fast.
/// </para>
/// <para>
/// <b>Required-always vs if-applicable.</b> All five central singleton proxies are
/// created unconditionally on a central node (there is no feature/config gate around
/// any of them), so all five are treated as required-always here. If a future
/// singleton is created behind a feature flag, it should NOT be added to
/// <see cref="RequiredSingletonProxyNames"/> — "if applicable" means skip when its
/// feature is off.
/// </para>
/// <para>
/// <b>Failover flakiness.</b> During a brief singleton handover the singleton may be
/// momentarily unreachable through the proxy. The bounded per-singleton timeout maps
/// that to Unhealthy (we never throw and never retry — retries would make the probe
/// slow). Readiness flapping briefly during a failover is acceptable and correct: a
/// node mid-handover is legitimately not fully ready. We deliberately accept that
/// tradeoff rather than masking it with retries.
/// </para>
/// <para>
/// <b>No leadership requirement.</b> The proxy reaches the singleton from either node
/// (active or standby), so a ready standby still reports Healthy here — readiness must
/// NOT require cluster leadership (that is the Active tier's job).
/// </para>
/// <para>
/// The <see cref="ActorSystem"/> is resolved lazily from DI per probe, mirroring
/// <c>AkkaClusterHealthCheck</c>; if it is not yet available (startup race) the check
/// returns Unhealthy rather than throwing.
/// </para>
/// </remarks>
public sealed class RequiredSingletonsHealthCheck : IHealthCheck
{
    /// <summary>
    /// Local actor names (under <c>/user</c>) of the <c>ClusterSingletonProxy</c>
    /// actors for the singletons that must always be running on a central node.
    /// Matches the unconditional proxy registrations in
    /// <c>AkkaHostedService.RegisterCentralActors</c>.
    /// </summary>
    public static readonly IReadOnlyList<string> RequiredSingletonProxyNames = new[]
    {
        "notification-outbox-proxy",
        "audit-log-ingest-proxy",
        "site-call-audit-proxy",
        "audit-log-purge-proxy",
        "site-audit-reconciliation-proxy",
    };

    // Short, bounded per-singleton timeout. Kept small so readiness polling stays
    // fast; a singleton in mid-handover that does not answer within this window is
    // (correctly) treated as momentarily unreachable. Do NOT add retries here.
    private static readonly TimeSpan ProbeTimeout = TimeSpan.FromSeconds(2);

    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<RequiredSingletonsHealthCheck> _logger;

    /// <summary>Initializes a new <see cref="RequiredSingletonsHealthCheck"/>.</summary>
    /// <param name="serviceProvider">
    /// Application service provider; the <see cref="ActorSystem"/> is resolved lazily so the
    /// check is startup-safe (Unhealthy, never throwing, if Akka is not yet up).
    /// </param>
    /// <param name="logger">Logger for diagnostic detail on unreachable singletons.</param>
    public RequiredSingletonsHealthCheck(
        IServiceProvider serviceProvider,
        ILogger<RequiredSingletonsHealthCheck> logger)
    {
        _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <summary>Evaluates whether all required cluster singletons are running and reports the aggregate health.</summary>
    /// <param name="context">The health-check context supplied by the health-check middleware.</param>
    /// <param name="cancellationToken">Token used to cancel the health evaluation.</param>
    /// <returns>A task that resolves to a healthy result when every required singleton is present, otherwise an unhealthy result describing what is missing.</returns>
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        // CheckHealthAsync must NEVER throw — catch everything and map to Unhealthy
        // with a descriptive message. An escaping exception would be recorded as
        // Unhealthy anyway, but a thrown exception loses the descriptive message.
        try
        {
            var system = _serviceProvider.GetService<ActorSystem>();
            if (system is null)
                return HealthCheckResult.Unhealthy("ActorSystem not yet available.");

            // Probe each required singleton concurrently so the whole check is bounded
            // by ~ProbeTimeout, not the sum of the per-singleton timeouts.
            var probes = RequiredSingletonProxyNames
                .Select(name => ProbeAsync(system, name, cancellationToken))
                .ToArray();

            var results = await Task.WhenAll(probes).ConfigureAwait(false);

            var unreachable = results
                .Where(r => !r.Reachable)
                .Select(r => r.Name)
                .ToList();

            if (unreachable.Count == 0)
                return HealthCheckResult.Healthy(
                    $"All {RequiredSingletonProxyNames.Count} required cluster singletons are reachable.");

            var joined = string.Join(", ", unreachable);
            _logger.LogWarning(
                "Readiness degraded: required cluster singleton(s) unreachable: {Unreachable}",
                joined);
            return HealthCheckResult.Unhealthy(
                $"Required cluster singleton(s) unreachable: {joined}.");
        }
        catch (Exception ex)
        {
            // Defensive: any unexpected failure (including OperationCanceledException
            // on shutdown) degrades readiness rather than escaping the check.
            return HealthCheckResult.Unhealthy(
                "Failed to probe required cluster singletons.", ex);
        }
    }

    /// <summary>
    /// Asks the named local proxy an <see cref="Identify"/> with a bounded timeout.
    /// Reachable iff a non-null <see cref="ActorIdentity.Subject"/> comes back in time.
    /// A null Subject (path not present) or a timeout/exception → not reachable. This
    /// method itself never throws.
    /// </summary>
    private async Task<(string Name, bool Reachable)> ProbeAsync(
        ActorSystem system,
        string proxyName,
        CancellationToken cancellationToken)
    {
        try
        {
            // ActorSelection so a missing path resolves an ActorIdentity with a null
            // Subject (rather than throwing) within the bounded timeout.
            var selection = system.ActorSelection($"/user/{proxyName}");

            var identity = await selection
                .Ask<ActorIdentity>(new Identify(proxyName), ProbeTimeout, cancellationToken)
                .ConfigureAwait(false);

            return (proxyName, identity.Subject is not null);
        }
        catch (Exception)
        {
            // Timeout / cancellation / any failure → momentarily unreachable. Bounded,
            // no retry — readiness may briefly flap during a singleton handover, which
            // is the correct signal for a node mid-handover.
            return (proxyName, false);
        }
    }
}
