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

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;

/// <summary>
///     Background service that periodically evicts idle browse sessions from the
///     <see cref="BrowseSessionRegistry"/>. Each tick takes a snapshot of the registry,
///     removes any session that has been idle longer than <see cref="IdleTtl"/>, then
///     disposes the evicted instance OUTSIDE the dictionary — so concurrent expand
///     calls racing eviction fail cleanly via <see cref="BrowseSessionNotFoundException"/>.
/// </summary>
public sealed class BrowseSessionReaper(
    BrowseSessionRegistry registry,
    ILogger<BrowseSessionReaper> logger) : BackgroundService
{
    /// <summary>How long a session may be untouched before it becomes eligible for eviction.</summary>
    public static readonly TimeSpan IdleTtl = TimeSpan.FromMinutes(2);

    /// <summary>How often the reaper checks the registry.</summary>
    public static readonly TimeSpan TickInterval = TimeSpan.FromSeconds(30);

    /// <inheritdoc />
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TickInterval);
        while (!stoppingToken.IsCancellationRequested &&
               await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false))
        {
            try { await ReapOnceAsync(stoppingToken).ConfigureAwait(false); }
            catch (Exception ex)
            {
                logger.LogWarning(ex, "Browse-session reaper iteration failed; will retry next tick.");
            }
        }
        await DrainAllAsync().ConfigureAwait(false);
    }

    /// <summary>Evicts every session whose <see cref="Commons.Browsing.IBrowseSession.LastUsedUtc"/>
    /// is older than <see cref="IdleTtl"/>. Internal so tests can drive a tick directly.</summary>
    /// <param name="ct">Cancellation token for the reap pass.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    internal async Task ReapOnceAsync(CancellationToken ct)
    {
        var now = DateTime.UtcNow;
        foreach (var (token, session) in registry.Snapshot())
        {
            if (now - session.LastUsedUtc < IdleTtl) continue;
            if (!registry.TryRemove(token, out var taken)) continue;
            try { await taken.DisposeAsync().ConfigureAwait(false); }
            catch (Exception ex)
            {
                logger.LogDebug(ex,
                    "Best-effort dispose of idle-evicted browse session {Token} failed.", token);
            }
            logger.LogDebug("Browse session {Token} closed reason=idle-ttl", token);
        }
    }

    private async Task DrainAllAsync()
    {
        foreach (var (token, session) in registry.Snapshot())
        {
            if (!registry.TryRemove(token, out var taken)) continue;
            try { await taken.DisposeAsync().ConfigureAwait(false); } catch { }
            logger.LogDebug("Browse session {Token} closed reason=shutdown", token);
        }
    }
}
