using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi;

namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring;

public static class ServiceCollectionExtensions
{
    /// <summary>
    /// Sentinel marker used by <see cref="AddSiteEventLogHealthMetricsBridge"/> to
    /// implement an idempotency guard. Because the reporter is registered via a
    /// factory-lambda overload of <c>AddHostedService</c>, its
    /// <see cref="Microsoft.Extensions.DependencyInjection.ServiceDescriptor.ImplementationType"/>
    /// is <see langword="null"/> — checking it would be a silent no-op. Registering
    /// this marker as a singleton and guarding on its <c>ServiceType</c> gives a
    /// reliable, allocation-free sentinel that works regardless of how the hosted
    /// service was wired.
    /// </summary>
    private sealed class SiteEventLogHealthMetricsBridgeMarker { }

    /// <summary>
    /// Sentinel marker for <see cref="AddLocalDbReplicationHealthBridge"/>'s idempotency
    /// guard — same rationale as <see cref="SiteEventLogHealthMetricsBridgeMarker"/>.
    /// </summary>
    private sealed class LocalDbReplicationHealthBridgeMarker { }

    /// <summary>
    /// Register site-side health monitoring services (metric collection + periodic reporting).
    /// Call this on site nodes only. For central, call AddCentralHealthAggregation() instead.
    /// </summary>
    /// <param name="services">The DI service collection to register into.</param>
    /// <returns>The same <paramref name="services"/> instance, for call chaining.</returns>
    public static IServiceCollection AddSiteHealthMonitoring(this IServiceCollection services)
    {
        AddOptionsValidation(services);
        services.AddSingleton<ISiteHealthCollector, SiteHealthCollector>();
        services.AddHostedService<HealthReportSender>();
        return services;
    }

    /// <summary>
    /// Register shared health monitoring services (safe for both central and site).
    /// Does not start the HealthReportSender — call AddSiteHealthMonitoring() on site nodes for that.
    /// </summary>
    /// <param name="services">The DI service collection to register into.</param>
    /// <returns>The same <paramref name="services"/> instance, for call chaining.</returns>
    public static IServiceCollection AddHealthMonitoring(this IServiceCollection services)
    {
        AddOptionsValidation(services);
        services.AddSingleton<ISiteHealthCollector, SiteHealthCollector>();
        return services;
    }

    /// <summary>
    /// Register central-side health aggregation services. Includes the
    /// <see cref="CentralHealthReportLoop"/> that generates a self-report
    /// for the central cluster so it appears on /monitoring/health.
    /// </summary>
    /// <param name="services">The DI service collection to register into.</param>
    /// <returns>The same <paramref name="services"/> instance, for call chaining.</returns>
    public static IServiceCollection AddCentralHealthAggregation(this IServiceCollection services)
    {
        AddOptionsValidation(services);
        services.AddSingleton<CentralHealthAggregator>();
        services.AddSingleton<ICentralHealthAggregator>(sp => sp.GetRequiredService<CentralHealthAggregator>());
        services.AddHostedService(sp => sp.GetRequiredService<CentralHealthAggregator>());
        services.AddHostedService<CentralHealthReportLoop>();

        // The central backlog aggregate for the Audit Log KPI *history*. Only the
        // central node runs the aggregator, so this seam is registered here; the
        // AuditLogKpiSampleSource resolves it (optional ctor param) and records the
        // real cross-site backlog instead of the append-only repository's hardwired
        // zero. Sites leave it unregistered → snapshot fallback.
        services.AddSingleton<
            Commons.Interfaces.Kpi.IAuditBacklogProvider,
            Kpi.CentralHealthAuditBacklogProvider>();

        // "KPI History & Trends": per-site Site Health KPI sample source.
        // Reads the in-memory central aggregator (a singleton) rather than a
        // repository; registered Scoped to match the recorder's per-tick scope
        // and the other sample sources (a scoped source over a singleton
        // dependency is fine — no captive dependency).
        services.TryAddEnumerable(
            ServiceDescriptor.Scoped<IKpiSampleSource, Kpi.SiteHealthKpiSampleSource>());
        return services;
    }

    /// <summary>
    /// Site Event Logging — register the
    /// <see cref="SiteEventLogFailureCountReporter"/> hosted service that
    /// periodically reads the cumulative event-log write-failure count and
    /// pushes it into <see cref="ISiteHealthCollector"/> as a point-in-time
    /// snapshot (<c>SiteEventLogWriteFailures</c> on the site health report).
    /// </summary>
    /// <remarks>
    /// <para>
    /// Must be called AFTER <see cref="AddSiteHealthMonitoring"/> (or
    /// <see cref="AddHealthMonitoring"/>) which registers the
    /// <see cref="ISiteHealthCollector"/> the reporter depends on.
    /// </para>
    /// <para>
    /// <b>Why a Func&lt;long&gt; delegate instead of ISiteEventLogger.</b>
    /// A direct <c>HealthMonitoring → SiteEventLogging</c> reference is avoided to
    /// prevent an undesirable low-level coupling: <c>SiteEventLogging</c> is a
    /// leaf component that should not pull in higher-level infrastructure. The
    /// <see cref="Func{TResult}"/> delegate seam keeps the reference one-way and
    /// loose: the caller (Host site wiring) captures
    /// <c>ISiteEventLogger.FailedWriteCount</c> as a lambda and passes it here.
    /// Note: <c>HealthMonitoring → StoreAndForward → SiteEventLogging</c> already
    /// exists as a transitive path, so a direct reference would not introduce a
    /// cycle — the delegate is purely a coupling-avoidance measure.
    /// </para>
    /// <para>
    /// Idempotent — a <see cref="SiteEventLogHealthMetricsBridgeMarker"/> singleton
    /// is used as the sentinel. Because the reporter is registered via a factory-lambda
    /// overload of <c>AddHostedService</c>, its
    /// <see cref="Microsoft.Extensions.DependencyInjection.ServiceDescriptor.ImplementationType"/>
    /// is <see langword="null"/>; checking it would be a silent no-op and a second
    /// call would spin up a second polling timer. Guarding on the marker's
    /// <c>ServiceType</c> is always reliable regardless of how the hosted service
    /// was wired (AddHostedService has no TryAdd variant).
    /// </para>
    /// </remarks>
    /// <param name="services">The service collection to register into.</param>
    /// <param name="failedWriteCountProvider">
    /// A factory delegate that, given the root <see cref="IServiceProvider"/>,
    /// returns a <see cref="Func{TResult}"/> that reads the current cumulative
    /// event-log write-failure count. Typically:
    /// <c>sp => () => sp.GetRequiredService&lt;ISiteEventLogger&gt;().FailedWriteCount</c>.
    /// The factory is evaluated once at hosted-service resolution time; the inner
    /// <see cref="Func{TResult}"/> is called on every poll tick.
    /// </param>
    /// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
    public static IServiceCollection AddSiteEventLogHealthMetricsBridge(
        this IServiceCollection services,
        Func<IServiceProvider, Func<long>> failedWriteCountProvider)
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentNullException.ThrowIfNull(failedWriteCountProvider);

        // Idempotent guard — uses the marker type rather than ImplementationType because
        // AddHostedService(factory-lambda) sets only ImplementationFactory and leaves
        // ImplementationType null; an ImplementationType == check is a silent no-op for
        // factory-registered services. The marker singleton's ServiceType is always set.
        if (services.Any(d => d.ServiceType == typeof(SiteEventLogHealthMetricsBridgeMarker)))
        {
            return services;
        }

        services.AddSingleton<SiteEventLogHealthMetricsBridgeMarker>();
        services.AddHostedService(sp => new SiteEventLogFailureCountReporter(
            failedWriteCountProvider(sp),
            sp.GetRequiredService<ISiteHealthCollector>(),
            sp.GetRequiredService<ILogger<SiteEventLogFailureCountReporter>>()));

        return services;
    }

    /// <summary>
    /// Bridge LocalDb replication status (peer connectivity + unacked oplog backlog) onto
    /// the site health report. Must be called AFTER <c>AddSiteHealthMonitoring</c>
    /// (registers <see cref="ISiteHealthCollector"/>) and after the replication engine is
    /// registered. Idempotent via a marker sentinel.
    /// </summary>
    /// <remarks>
    /// Delegates rather than an <c>ISyncStatus</c> parameter keep HealthMonitoring free of
    /// a reference on the replication library, matching
    /// <see cref="AddSiteEventLogHealthMetricsBridge"/>. Pass the backlog through as
    /// nullable — coercing null to 0 would report an unreadable oplog as a healthy one.
    /// </remarks>
    /// <param name="services">The service collection to register into.</param>
    /// <param name="connectedProvider">Given the root provider, returns a reader for "a sync session is running".</param>
    /// <param name="oplogBacklogProvider">Given the root provider, returns a reader for the unacked backlog (null = unknown).</param>
    /// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
    public static IServiceCollection AddLocalDbReplicationHealthBridge(
        this IServiceCollection services,
        Func<IServiceProvider, Func<bool>> connectedProvider,
        Func<IServiceProvider, Func<long?>> oplogBacklogProvider)
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentNullException.ThrowIfNull(connectedProvider);
        ArgumentNullException.ThrowIfNull(oplogBacklogProvider);

        if (services.Any(d => d.ServiceType == typeof(LocalDbReplicationHealthBridgeMarker)))
        {
            return services;
        }

        services.AddSingleton<LocalDbReplicationHealthBridgeMarker>();
        services.AddHostedService(sp => new LocalDbReplicationStatusReporter(
            connectedProvider(sp),
            oplogBacklogProvider(sp),
            sp.GetRequiredService<ISiteHealthCollector>(),
            sp.GetRequiredService<ILogger<LocalDbReplicationStatusReporter>>()));

        return services;
    }

    /// <summary>
    /// Register the <see cref="HealthMonitoringOptionsValidator"/>
    /// so a misconfigured <c>ScadaBridge:HealthMonitoring</c> section (zero/negative
    /// intervals, or a <c>CentralOfflineTimeout</c> shorter than
    /// <c>OfflineTimeout</c>) is rejected with a clear, key-naming message when the
    /// hosted services resolve their options at startup — rather than crashing
    /// later inside a <see cref="PeriodicTimer"/> constructor with an opaque
    /// <see cref="ArgumentOutOfRangeException"/>. Idempotent so it is safe when
    /// more than one of the registration methods above is called.
    /// </summary>
    private static void AddOptionsValidation(IServiceCollection services)
    {
        services.TryAddEnumerable(
            ServiceDescriptor.Singleton<IValidateOptions<HealthMonitoringOptions>, HealthMonitoringOptionsValidator>());
    }
}
