using Microsoft.EntityFrameworkCore;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Infrastructure.Data;

namespace PowderCoating.Web.BackgroundServices;

/// <summary>
/// Runs once daily and purges StripeWebhookEvents older than the StripeWebhookRetentionDays platform setting (default 90).
/// Processed events are safe to purge; unprocessed ones are kept regardless of age
/// so nothing is silently dropped.
/// </summary>
public class StripeWebhookRetentionBackgroundService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<StripeWebhookRetentionBackgroundService> _logger;

    private static readonly TimeOnly RunTime = new(3, 0); // 3:00 AM — stagger after AuditLog (2 AM)
    private const int BatchSize = 500;

    /// <summary>
    /// Initializes the service with the required scope factory and logger.
    /// <see cref="IServiceScopeFactory"/> is required because <see cref="BackgroundService"/> is a singleton
    /// and scoped EF DbContext instances cannot be injected directly into it.
    /// </summary>
    public StripeWebhookRetentionBackgroundService(
        IServiceScopeFactory scopeFactory,
        ILogger<StripeWebhookRetentionBackgroundService> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    /// <summary>
    /// Long-running loop that wakes once per day at <see cref="RunTime"/> (3:00 AM local) to purge
    /// old webhook event records. Scheduled at 3 AM — one hour after the audit log retention job —
    /// to stagger database-intensive maintenance tasks and reduce peak disk I/O.
    /// </summary>
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("StripeWebhookRetentionBackgroundService started.");

        while (!stoppingToken.IsCancellationRequested)
        {
            var delay = GetDelayUntilNextRun();
            _logger.LogDebug("Stripe webhook retention sleeping {Delay} until next run.", delay);

            await Task.Delay(delay, stoppingToken);

            if (stoppingToken.IsCancellationRequested) break;

            await RunRetentionAsync(stoppingToken);
        }
    }

    /// <summary>
    /// Reads the <c>StripeWebhookRetentionDays</c> platform setting (default 90) and hard-deletes
    /// processed <c>StripeWebhookEvent</c> rows older than the cutoff date.
    /// Unprocessed events (where <c>ProcessedAt</c> is null) are always preserved regardless of age
    /// so that no webhook is silently dropped — a stuck event will surface in monitoring rather than
    /// disappear. Deletions are batched with a 200 ms pause between iterations to minimize lock pressure.
    /// </summary>
    private async Task RunRetentionAsync(CancellationToken ct)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        var platformSettings = scope.ServiceProvider.GetRequiredService<IPlatformSettingsService>();

        var raw = await platformSettings.GetAsync(PlatformSettingKeys.StripeWebhookRetentionDays);
        var retentionDays = int.TryParse(raw, out var d) ? d : 90;
        var cutoff = DateTime.UtcNow.AddDays(-retentionDays);

        _logger.LogInformation(
            "Stripe webhook retention: purging processed events older than {Cutoff:yyyy-MM-dd} (retention={Days}d)",
            cutoff, retentionDays);

        try
        {

            var totalDeleted = 0;
            int deleted;
            do
            {
                // Only purge events that have been processed — never drop unprocessed ones
                deleted = await db.StripeWebhookEvents
                    .Where(e => e.ReceivedAt < cutoff && e.ProcessedAt != null)
                    .OrderBy(e => e.ReceivedAt)
                    .Take(BatchSize)
                    .ExecuteDeleteAsync(ct);

                totalDeleted += deleted;

                if (deleted > 0)
                    await Task.Delay(200, ct);

            } while (deleted == BatchSize && !ct.IsCancellationRequested);

            if (totalDeleted > 0)
                _logger.LogInformation("Stripe webhook retention: deleted {Count} old events.", totalDeleted);
            else
                _logger.LogDebug("Stripe webhook retention: nothing to purge.");
        }
        catch (OperationCanceledException) { /* shutting down */ }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Stripe webhook retention job failed.");
        }
    }

    /// <summary>
    /// Calculates how long to sleep before the next scheduled run at <see cref="RunTime"/> (3:00 AM local).
    /// Uses local server time so the window aligns with the server's overnight low-traffic period.
    /// Returns the delay to tomorrow's 3 AM if that time has already passed today.
    /// </summary>
    private static TimeSpan GetDelayUntilNextRun()
    {
        var now = DateTime.Now;
        var next = DateTime.Today.Add(RunTime.ToTimeSpan());
        if (next <= now)
            next = next.AddDays(1);
        return next - now;
    }
}
