using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring;

/// <summary>
/// Site-side hosted service that periodically reads LocalDb replication status and pushes
/// it into <see cref="ISiteHealthCollector"/>, so the next
/// <see cref="ISiteHealthCollector.CollectReport"/> emits fresh
/// <c>LocalDbReplicationConnected</c> / <c>LocalDbOplogBacklog</c> fields.
/// </summary>
/// <remarks>
/// <para>
/// <b>Why delegates and not <c>ISyncStatus</c> directly.</b> Same reasoning as
/// <see cref="SiteEventLogFailureCountReporter"/>: HealthMonitoring does not take a
/// reference on the replication library. The Host site wiring captures the two reads as
/// lambdas at registration time; this service only moves numbers.
/// </para>
/// <para>
/// <b>Null backlog is preserved, never coerced to zero.</b> <c>ISyncStatus.OplogBacklog</c>
/// is nullable precisely so a failed poll reads as "unknown" — rendering it as 0 would
/// report a replication pair that cannot read its own oplog as perfectly healthy.
/// </para>
/// <para>
/// <b>Cadence.</b> 30 s, matching <see cref="SiteEventLogFailureCountReporter"/> and
/// <c>SiteAuditBacklogReporter</c>. Any exception during a probe is logged and swallowed;
/// the next tick retries.
/// </para>
/// </remarks>
public sealed class LocalDbReplicationStatusReporter : IHostedService, IDisposable
{
    /// <summary>Default poll cadence, matching the other site health bridges.</summary>
    internal static readonly TimeSpan DefaultRefreshInterval = TimeSpan.FromSeconds(30);

    private readonly Func<bool> _connectedProvider;
    private readonly Func<long?> _oplogBacklogProvider;
    private readonly ISiteHealthCollector _collector;
    private readonly ILogger<LocalDbReplicationStatusReporter> _logger;
    private readonly TimeSpan _refreshInterval;
    private CancellationTokenSource? _cts;
    private Task? _loop;

    /// <summary>Initializes a new instance of <see cref="LocalDbReplicationStatusReporter"/>.</summary>
    /// <param name="connectedProvider">Reads whether a peer sync session is currently running.</param>
    /// <param name="oplogBacklogProvider">Reads the unacked oplog backlog, or null when unknown.</param>
    /// <param name="collector">The site health collector receiving the snapshot.</param>
    /// <param name="logger">Logger instance.</param>
    /// <param name="refreshInterval">Poll interval override; defaults to 30 s.</param>
    public LocalDbReplicationStatusReporter(
        Func<bool> connectedProvider,
        Func<long?> oplogBacklogProvider,
        ISiteHealthCollector collector,
        ILogger<LocalDbReplicationStatusReporter> logger,
        TimeSpan? refreshInterval = null)
    {
        _connectedProvider = connectedProvider ?? throw new ArgumentNullException(nameof(connectedProvider));
        _oplogBacklogProvider = oplogBacklogProvider ?? throw new ArgumentNullException(nameof(oplogBacklogProvider));
        _collector = collector ?? throw new ArgumentNullException(nameof(collector));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _refreshInterval = refreshInterval ?? DefaultRefreshInterval;
    }

    /// <summary>Starts the polling loop, probing once immediately before entering the timed cycle.</summary>
    /// <param name="ct">Cancellation token signalling host shutdown.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public Task StartAsync(CancellationToken ct)
    {
        var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        _cts = cts;
        _loop = RunAsync(cts.Token);
        return Task.CompletedTask;
    }

    /// <summary>Stops the polling loop.</summary>
    /// <param name="ct">Cancellation token for the stop operation.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task StopAsync(CancellationToken ct)
    {
        if (_cts is null) return;

        await _cts.CancelAsync().ConfigureAwait(false);
        if (_loop is not null)
        {
            // Await the loop so the reporter is quiescent before the host disposes the
            // collector out from under it.
            try { await _loop.ConfigureAwait(false); }
            catch (OperationCanceledException) { /* expected on shutdown */ }
        }
    }

    private async Task RunAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            Probe();

            try
            {
                await Task.Delay(_refreshInterval, ct).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                return;
            }
        }
    }

    /// <summary>
    /// Reads both providers and pushes one snapshot onto the collector; never throws.
    /// Public so a test can drive a single deterministic probe instead of starting the
    /// service and waiting out the 30 s cadence.
    /// </summary>
    public void Probe()
    {
        try
        {
            _collector.SetLocalDbReplicationStatus(
                _connectedProvider(), _oplogBacklogProvider());
        }
        catch (Exception ex)
        {
            // Health reporting must never take the site node down. The previous snapshot
            // stays on the collector and the next tick retries.
            _logger.LogWarning(ex, "Failed to read LocalDb replication status for the site health report.");
        }
    }

    /// <inheritdoc />
    public void Dispose() => _cts?.Dispose();
}
