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

namespace PowderCoating.Web.BackgroundServices;

/// <summary>
/// Runs once per day and sends an in-app notification to any company that has not yet completed
/// the setup wizard. The reminder schedule is: first nudge on day 3, then every 7 days after that
/// (days 3, 10, 17, 24, …). Deduplication is done by querying the most recent
/// <c>SetupWizardReminder</c> notification in <c>InAppNotifications</c> — no extra DB columns
/// are needed.
/// </summary>
public class SetupWizardReminderBackgroundService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<SetupWizardReminderBackgroundService> _logger;

    // First reminder fires when the company is at least this many days old.
    private const int FirstReminderAfterDays = 3;

    // Subsequent reminders fire this many days after the previous one.
    private const int RepeatIntervalDays = 7;

    // Run at 9:00 AM server local time each day — same cadence as other daily services.
    private static readonly TimeOnly RunTime = new(9, 0);

    // Matches the string stored in InAppNotification.NotificationType for wizard reminders.
    private const string ReminderNotificationType = "SetupWizardReminder";

    /// <summary>
    /// Initializes the service with a scope factory (required because BackgroundService is a
    /// singleton and both DbContext and IInAppNotificationService are scoped) and a logger.
    /// </summary>
    public SetupWizardReminderBackgroundService(
        IServiceScopeFactory scopeFactory,
        ILogger<SetupWizardReminderBackgroundService> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    /// <summary>
    /// Long-running loop that sleeps until 9:00 AM each day then delegates to
    /// <see cref="RunAsync"/>. Uses the cancellation token so the service stops promptly on
    /// application shutdown rather than waiting for the next wake window.
    /// </summary>
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("SetupWizardReminderBackgroundService started.");

        while (!stoppingToken.IsCancellationRequested)
        {
            var delay = GetDelayUntilNextRun();
            _logger.LogDebug("Setup wizard reminder service sleeping for {Delay} until next run.", delay);

            try
            {
                await Task.Delay(delay, stoppingToken);
            }
            catch (OperationCanceledException)
            {
                break;
            }

            if (stoppingToken.IsCancellationRequested) break;

            await RunAsync(stoppingToken);
        }

        _logger.LogInformation("SetupWizardReminderBackgroundService stopped.");
    }

    /// <summary>
    /// Opens a fresh DI scope, loads all companies with an incomplete setup wizard, and sends
    /// a reminder to each one that is due for a nudge today. A new scope per run keeps the
    /// DbContext change tracker clean across daily executions. Top-level exceptions are caught
    /// here so that a single bad run does not kill the service loop.
    /// </summary>
    private async Task RunAsync(CancellationToken ct)
    {
        _logger.LogInformation("Running daily setup-wizard reminder check.");

        try
        {
            using var scope = _scopeFactory.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            var notificationService = scope.ServiceProvider.GetRequiredService<IInAppNotificationService>();

            var today = DateTime.UtcNow.Date;

            // Load all incomplete companies. IgnoreQueryFilters is required — there is no HTTP
            // context in a background service so the tenant filter would resolve to null and
            // return zero rows.
            var incompletePrefs = await db.CompanyPreferences
                .IgnoreQueryFilters()
                .AsNoTracking()
                .Where(p => !p.IsDeleted && !p.SetupWizardCompleted)
                .Select(p => new { p.CompanyId, p.CreatedAt })
                .ToListAsync(ct);

            if (incompletePrefs.Count == 0)
            {
                _logger.LogDebug("All companies have completed the setup wizard — nothing to do.");
                return;
            }

            // Pull the most recent SetupWizardReminder notification per company in one query so
            // we don't do N round-trips.
            var companyIds = incompletePrefs.Select(p => p.CompanyId).ToList();

            var lastReminderByCompany = await db.InAppNotifications
                .IgnoreQueryFilters()
                .AsNoTracking()
                .Where(n =>
                    !n.IsDeleted &&
                    n.NotificationType == ReminderNotificationType &&
                    companyIds.Contains(n.CompanyId))
                .GroupBy(n => n.CompanyId)
                .Select(g => new
                {
                    CompanyId = g.Key,
                    LastSentAt = g.Max(n => n.CreatedAt),
                    Count = g.Count()
                })
                .ToDictionaryAsync(x => x.CompanyId, ct);

            foreach (var prefs in incompletePrefs)
            {
                if (ct.IsCancellationRequested) break;

                var companyAgedays = (today - prefs.CreatedAt.Date).Days;

                if (!lastReminderByCompany.TryGetValue(prefs.CompanyId, out var lastReminder))
                {
                    // No reminder sent yet — fire the first one once the company is old enough.
                    if (companyAgedays < FirstReminderAfterDays) continue;

                    await SendReminderAsync(notificationService, prefs.CompanyId, reminderNumber: 1, ct);
                }
                else
                {
                    // A reminder was sent before — repeat if the interval has elapsed.
                    var daysSinceLast = (today - lastReminder.LastSentAt.Date).Days;
                    if (daysSinceLast < RepeatIntervalDays) continue;

                    await SendReminderAsync(notificationService, prefs.CompanyId,
                        reminderNumber: lastReminder.Count + 1, ct);
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Setup wizard reminder run failed.");
        }
    }

    /// <summary>
    /// Sends a single setup-wizard reminder notification for the given company. The title and
    /// message escalate slightly across reminder numbers: the first nudge is friendly; later ones
    /// are more direct. The link takes the user straight to Step 1 of the wizard.
    /// </summary>
    private static async Task SendReminderAsync(
        IInAppNotificationService notificationService,
        int companyId,
        int reminderNumber,
        CancellationToken ct)
    {
        var (title, message) = reminderNumber switch
        {
            1 => (
                "Complete your setup wizard",
                "Welcome! Your setup wizard is still in progress. Taking a few minutes to finish it will unlock accurate pricing, oven scheduling, and more."),
            2 => (
                "Your setup isn't finished yet",
                "You're leaving features on the table. Complete the setup wizard to configure your operating costs, equipment, and pricing tiers."),
            _ => (
                "Reminder: setup wizard incomplete",
                "Your account setup is still incomplete. Finish the setup wizard so your team can get the most out of Powder Coating Logix.")
        };

        await notificationService.CreateAsync(
            companyId,
            title,
            message,
            ReminderNotificationType,
            link: "/SetupWizard");
    }

    /// <summary>
    /// Calculates the delay until the next 9:00 AM local run. If that time has already passed
    /// today the next run is scheduled for tomorrow — same logic used by other daily services.
    /// </summary>
    private static TimeSpan GetDelayUntilNextRun()
    {
        var now = DateTime.Now;
        var nextRun = now.Date.Add(RunTime.ToTimeSpan());
        if (nextRun <= now)
            nextRun = nextRun.AddDays(1);
        return nextRun - now;
    }
}
