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

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

/// <summary>
/// Site-side hosted service that runs the SQLite retention purge on a timer
/// (S1/U2). Each tick calls
/// <see cref="ISiteAuditQueue.PurgeExpiredAsync"/> with a cutoff of
/// <c>UtcNow - <see cref="SiteAuditRetentionOptions.ResolvedRetentionDays"/></c>,
/// deleting Forwarded/Reconciled rows past the retention window and reclaiming the
/// freed pages. Without it the ephemeral site store grows unbounded until the next
/// deployment recreates it.
/// </summary>
/// <remarks>
/// <para>
/// <b>Cadence.</b> First tick after <see cref="SiteAuditRetentionOptions.InitialDelay"/>
/// (default 5 min — short so a node that recycles daily still purges shortly after
/// start), then every <see cref="SiteAuditRetentionOptions.ResolvedPurgeInterval"/>
/// (default 24 h). Mirrors the loop shape of <see cref="SiteAuditBacklogReporter"/>.
/// </para>
/// <para>
/// <b>Failure containment.</b> Each purge is wrapped in a try/catch so a transient
/// SQLite error never tears down the hosted service — the next tick retries.
/// </para>
/// </remarks>
public sealed class SiteAuditRetentionService : IHostedService, IDisposable
{
    private readonly ISiteAuditQueue _queue;
    private readonly SiteAuditRetentionOptions _options;
    private readonly ILogger<SiteAuditRetentionService> _logger;
    private CancellationTokenSource? _cts;
    private Task? _loop;

    /// <summary>Initializes a new instance of <see cref="SiteAuditRetentionService"/>.</summary>
    /// <param name="queue">The site audit queue whose retention purge is driven.</param>
    /// <param name="options">Retention cadence + window options.</param>
    /// <param name="logger">Logger instance.</param>
    public SiteAuditRetentionService(
        ISiteAuditQueue queue,
        IOptions<SiteAuditRetentionOptions> options,
        ILogger<SiteAuditRetentionService> logger)
    {
        _queue = queue ?? throw new ArgumentNullException(nameof(queue));
        _options = (options ?? throw new ArgumentNullException(nameof(options))).Value;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <summary>Starts the background purge loop.</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 so both StopAsync and the host shutdown token abort the loop.
        var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        _cts = cts;

        // Read Token on the caller's thread, not inside the lambda: the lambda runs
        // whenever the thread pool gets to it, so a Dispose landing first would make
        // the deferred _cts.Token read throw and fault the loop task the host awaits.
        var token = cts.Token;
        _loop = Task.Run(() => RunLoopAsync(token), CancellationToken.None);
        return Task.CompletedTask;
    }

    private async Task RunLoopAsync(CancellationToken ct)
    {
        try
        {
            // InitialDelay before the first purge — short so a daily-recycled node
            // still purges soon after start rather than waiting a full interval it may
            // never reach.
            try
            {
                await Task.Delay(_options.InitialDelay, ct).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                return;
            }

            await SafePurgeAsync(ct).ConfigureAwait(false);

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

                await SafePurgeAsync(ct).ConfigureAwait(false);
            }
        }
        catch (OperationCanceledException)
        {
            // Shutdown landed mid-purge: SafePurgeAsync rethrows OCE by design so the purge
            // aborts promptly, but the loop task must complete CLEANLY — StopAsync hands
            // _loop straight to the host, and a canceled task there is shutdown-log noise
            // (arch-review 04 round 2, R7). Cancellation here IS the clean exit.
        }
    }

    private async Task SafePurgeAsync(CancellationToken ct)
    {
        try
        {
            var cutoff = DateTime.UtcNow.AddDays(-_options.ResolvedRetentionDays);
            var purged = await _queue.PurgeExpiredAsync(cutoff, ct).ConfigureAwait(false);
            if (purged > 0)
            {
                _logger.LogInformation(
                    "Site audit retention purge removed {Count} row(s) older than {Cutoff:o}.",
                    purged, cutoff);
            }
        }
        catch (OperationCanceledException)
        {
            // Shutdown — let the outer loop exit cleanly.
            throw;
        }
        catch (Exception ex)
        {
            // Catch-all is deliberate: a transient SQLite fault must not tear down
            // the hosted service; the next tick retries.
            _logger.LogWarning(ex, "SiteAuditRetentionService purge failed; next tick will retry.");
        }
    }

    /// <summary>Signals the purge 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)
    {
        try
        {
            _cts?.Cancel();
        }
        catch (ObjectDisposedException)
        {
            // Stop-after-Dispose is a legal ordering; Dispose already cancelled the
            // loop. Letting this escape would abort the host's shutdown sequence.
        }

        return _loop ?? Task.CompletedTask;
    }

    /// <summary>Releases the internal <see cref="CancellationTokenSource"/>.</summary>
    public void Dispose()
    {
        // Cancel before disposing so the loop is always signalled even when the host
        // disposes the container without having driven StopAsync first, and so the
        // loop's pending Task.Delay(interval, token) sees an already-cancelled token
        // rather than registering against a dead source.
        try
        {
            _cts?.Cancel();
        }
        catch (ObjectDisposedException)
        {
            // Already disposed — Dispose is idempotent.
        }

        _cts?.Dispose();
    }
}
