using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Health;
using ZB.MOM.WW.Health.Akka;
using ZB.MOM.WW.Health.EntityFrameworkCore;
using ZB.MOM.WW.LocalDb.Replication;
using ZB.MOM.WW.OtOpcUa.Cluster;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Host.Configuration;

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

public static class HealthEndpoints
{
    /// <summary>
    /// Registers the shared ZB.MOM.WW health probes. Tier semantics preserved: configdb + akka on
    /// ready+active; cluster-primary on active only. The configdb probe is admin-only (per-cluster mesh
    /// Phase 4): a driver-only node holds no ConfigDb, so it is registered iff <paramref name="hasAdmin"/>.
    /// </summary>
    /// <param name="services">The service collection to register the health checks on.</param>
    /// <param name="hasAdmin">
    /// Whether this node carries the <c>admin</c> role. When <c>false</c> (a driver-only node) the
    /// <c>configdb</c> probe is skipped — the node registers no <see cref="OtOpcUaConfigDbContext"/>
    /// factory, so probing it would throw when the readiness endpoint resolves the factory. Defaults to
    /// <c>true</c> so admin/fused callers (including the test harnesses) keep the probe unchanged.
    /// </param>
    /// <returns>The same service collection, for chaining.</returns>
    public static IServiceCollection AddOtOpcUaHealth(this IServiceCollection services, bool hasAdmin = true)
    {
        var checks = services.AddHealthChecks();

        // configdb probe is admin-only (per-cluster mesh Phase 4). A driver-only node gets its config
        // from central via ConfigSource:Mode=FetchAndCache and never registers the ConfigDb factory, so
        // the type-activated DatabaseHealthCheck would throw resolving it. Admin + fused nodes: unchanged.
        if (hasAdmin)
        {
            checks.AddTypeActivatedCheck<DatabaseHealthCheck<OtOpcUaConfigDbContext>>(
                "configdb",
                failureStatus: null,
                tags: new[] { ZbHealthTags.Ready, ZbHealthTags.Active },
                args: new DatabaseHealthCheckOptions<OtOpcUaConfigDbContext>
                {
                    ProbeQuery = static (db, ct) => db.Deployments.AsNoTracking().Take(1).ToListAsync(ct),
                });
        }

        checks
            .AddTypeActivatedCheck<AkkaClusterHealthCheck>(
                "akka",
                failureStatus: null,
                tags: new[] { ZbHealthTags.Ready, ZbHealthTags.Active },
                args: AkkaClusterStatusPolicy.OtOpcUaCompat)
            // "Is this node in charge of its mesh?" — Healthy on the one node that owns the active
            // work for its Cluster, Unhealthy (503) on its partner, so Traefik pins the AdminUI to the
            // node actually hosting the cluster singletons.
            //
            // Role preference admin-then-driver: a fused central node answers for `admin` (the role
            // the singletons and the AdminUI are pinned to), while a driver-only site node answers for
            // `driver` — which is exactly RedundancyStateActor.SelectDriverPrimary, the election behind
            // IsDriverPrimary and the OPC UA ServiceLevel 250/240 split. Both now route through the
            // shared ClusterActiveNode, so the tier and the ServiceLevel cannot disagree.
            //
            // Scoping is per-mesh for free: after per-cluster mesh Phase 6 a node's ClusterState
            // contains only its own application Cluster, so "oldest Up member" is already "oldest Up
            // member of this Cluster" — exactly one node answers 200 per Cluster, by construction.
            //
            // Before Health 0.3.0 this was ActiveNodeHealthCheck(role: "admin"), which answered a
            // different question and got it wrong twice: it returned Healthy for any node lacking the
            // admin role, so every driver-only site node called itself active, and it selected by
            // RoleLeader (lowest address) rather than by age, which is not where Akka places
            // singletons. See lmxopcua#494.
            .AddTypeActivatedCheck<ActiveNodeHealthCheck>(
                "cluster-primary",
                failureStatus: null,
                tags: new[] { ZbHealthTags.Active },
                args: new ActiveNodeHealthCheckOptions
                {
                    RolePreference = new[] { RoleParser.Admin, RoleParser.Driver },
                })
            // Registered on every node regardless of role (unlike the admin-only configdb probe above):
            // the check itself resolves ISyncStatus optionally and reports Healthy when LocalDb is absent
            // (admin-only graphs) or replication is default-OFF, so a plain node is never degraded by it.
            // A factory registration reads ISyncStatus + options + the sync port from the container.
            .Add(new HealthCheckRegistration(
                "localdb-replication",
                sp => new LocalDbReplicationHealthCheck(
                    sp.GetService<ISyncStatus>(),
                    sp.GetRequiredService<IOptions<ReplicationOptions>>(),
                    LocalDbRegistration.SyncListenPort(sp.GetRequiredService<IConfiguration>())),
                failureStatus: null,
                tags: new[] { ZbHealthTags.Active }));
        return services;
    }

    /// <summary>Maps the OtOpcUa health check endpoints to the route builder.</summary>
    /// <param name="app">The endpoint route builder.</param>
    /// <returns>The same endpoint route builder, for chaining.</returns>
    public static IEndpointRouteBuilder MapOtOpcUaHealth(this IEndpointRouteBuilder app)
    {
        app.MapZbHealth();
        return app;
    }
}
