using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;

namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;

/// <summary>
/// Audit Log (#23) M6 Bundle E (T6) — site-side hosted service that
/// periodically pulls a backlog snapshot from <see cref="ISiteAuditQueue"/>
/// and pushes it into <see cref="ISiteHealthCollector"/> so the next
/// <see cref="ISiteHealthCollector.CollectReport"/> emits a fresh
/// <c>SiteAuditBacklog</c> field on the site health report.
/// </summary>
/// <remarks>
/// <para>
/// <b>Why a hosted service, not the report sender.</b> Querying SQLite for the
/// backlog requires the queue's write lock; doing it inline in
/// <see cref="ISiteHealthCollector.CollectReport"/> would couple the collector
/// to <see cref="ISiteAuditQueue"/> and turn an in-memory snapshot read into
/// a synchronous I/O call on the report path. The hosted-service pattern keeps
/// the report path pure and the SQL probe off the report timing budget.
/// </para>
/// <para>
/// <b>Cadence.</b> 30 s by default — coarse enough to amortise the SQL probe
/// across many reports, fine enough that the central dashboard never lags by
/// more than one health-report interval. Tunable via
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Site.SqliteAuditWriterOptions"/> in a follow-up
/// if ops needs a different cadence; for M6 we hard-code the value because the
/// brief calls it out explicitly.
/// </para>
/// <para>
/// <b>Failure containment.</b> The probe call is wrapped in a try/catch so a
/// transient SQLite error never tears down the hosted service — the next tick
/// retries. Mirrors <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Central.AuditLogPartitionMaintenanceService"/>'s
/// "exception logged, not propagated" contract.
/// </para>
/// </remarks>
public sealed class SiteAuditBacklogReporter : IHostedService, IDisposable
{
    /// <summary>
    /// Default poll cadence. Half a typical 60 s health-report interval keeps
    /// the snapshot fresh without spinning the SQL probe more often than
    /// necessary.
    /// </summary>
    internal static readonly TimeSpan DefaultRefreshInterval = TimeSpan.FromSeconds(30);

    private readonly ISiteAuditQueue _queue;
    private readonly ISiteHealthCollector _collector;
    private readonly ILogger<SiteAuditBacklogReporter> _logger;
    private readonly TimeSpan _refreshInterval;
    private CancellationTokenSource? _cts;
    private Task? _loop;

    /// <summary>Initializes a new instance of <see cref="SiteAuditBacklogReporter"/>.</summary>
    /// <param name="queue">The site audit queue used to probe the backlog count.</param>
    /// <param name="collector">The site health collector that receives the backlog snapshot.</param>
    /// <param name="logger">Logger instance.</param>
    /// <param name="refreshInterval">Poll interval override; defaults to <see cref="DefaultRefreshInterval"/> (30 s).</param>
    public SiteAuditBacklogReporter(
        ISiteAuditQueue queue,
        ISiteHealthCollector collector,
        ILogger<SiteAuditBacklogReporter> logger,
        TimeSpan? refreshInterval = null)
    {
        _queue = queue ?? throw new ArgumentNullException(nameof(queue));
        _collector = collector ?? throw new ArgumentNullException(nameof(collector));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _refreshInterval = refreshInterval ?? DefaultRefreshInterval;
    }

    /// <summary>Starts the background polling loop, running an immediate first probe 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)
    {
        // Linked CTS lets StopAsync's cancellation AND the host's shutdown
        // token both terminate the loop; either side firing aborts the
        // pending Task.Delay.
        _cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        _loop = Task.Run(() => RunLoopAsync(_cts.Token));
        return Task.CompletedTask;
    }

    private async Task RunLoopAsync(CancellationToken ct)
    {
        // First tick runs immediately so the very first health report after
        // process start carries a real backlog snapshot — without this the
        // dashboard would show null for the first 30 s after a deploy.
        await SafeProbeAsync(ct).ConfigureAwait(false);

        while (!ct.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(_refreshInterval, ct).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                break;
            }

            await SafeProbeAsync(ct).ConfigureAwait(false);
        }
    }

    private async Task SafeProbeAsync(CancellationToken ct)
    {
        try
        {
            var snapshot = await _queue.GetBacklogStatsAsync(ct).ConfigureAwait(false);
            _collector.UpdateSiteAuditBacklog(snapshot);
        }
        catch (OperationCanceledException)
        {
            // Shutdown — let the outer loop exit cleanly.
            throw;
        }
        catch (Exception ex)
        {
            // Catch-all is deliberate: the hosted service must survive every
            // class of probe failure (transient SQLite lock contention, disk
            // I/O hiccup, …) so the next tick gets a chance.
            _logger.LogWarning(ex, "SiteAuditBacklogReporter probe failed; next tick will retry.");
        }
    }

    /// <summary>Signals the polling loop to stop and waits for it to complete.</summary>
    /// <param name="ct">Cancellation token (not used; the internal CTS governs shutdown).</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public Task StopAsync(CancellationToken ct)
    {
        _cts?.Cancel();
        return _loop ?? Task.CompletedTask;
    }

    /// <summary>Releases the internal <see cref="CancellationTokenSource"/> used to stop the polling loop.</summary>
    public void Dispose()
    {
        _cts?.Dispose();
    }
}
