using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using PowderCoating.Application.Interfaces;
using PowderCoating.Infrastructure.Data;

namespace PowderCoating.Web.BackgroundServices;

/// <summary>
/// Polls every 60 seconds for appointments whose reminder window has opened and dispatches
/// an email to the linked customer plus an in-app bell notification to company staff.
///
/// Deduplication strategy: after selecting candidates the service immediately stamps
/// <c>ReminderSentAt</c> on each appointment and saves before calling the notification
/// methods. This prevents a second loop iteration from re-sending if notifications are slow
/// or the application restarts mid-batch. A 24-hour lookback window caps the query so that
/// appointments that slipped through (e.g., server downtime) are silently skipped rather
/// than sending a stale reminder.
/// </summary>
public class AppointmentReminderBackgroundService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<AppointmentReminderBackgroundService> _logger;

    private static readonly TimeSpan PollingInterval = TimeSpan.FromMinutes(1);

    /// <summary>
    /// Appointments whose scheduled start is more than this far in the past are ignored even
    /// if their reminder was never sent (server was down, etc.). We do not want to blast a
    /// customer with a "your appointment is in 30 minutes" email hours after it was due.
    /// </summary>
    private static readonly TimeSpan MaxLookback = TimeSpan.FromHours(24);

    public AppointmentReminderBackgroundService(
        IServiceScopeFactory scopeFactory,
        ILogger<AppointmentReminderBackgroundService> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    /// <summary>
    /// Long-running loop that wakes every <see cref="PollingInterval"/> (60 s) and calls
    /// <see cref="RunAsync"/>. Uses <see cref="Task.Delay"/> with the cancellation token so
    /// the service shuts down promptly when the application stops.
    /// </summary>
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("AppointmentReminderBackgroundService started.");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(PollingInterval, stoppingToken);
            }
            catch (OperationCanceledException)
            {
                break;
            }

            if (stoppingToken.IsCancellationRequested) break;

            await RunAsync(stoppingToken);
        }

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

    /// <summary>
    /// One poll iteration: find all appointments whose reminder window has opened, stamp them,
    /// then dispatch email + in-app notifications. A fresh DI scope is created per poll so that
    /// the DbContext change tracker is clean each time.
    /// </summary>
    private async Task RunAsync(CancellationToken ct)
    {
        try
        {
            using var scope = _scopeFactory.CreateScope();
            var db                   = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            var notificationService  = scope.ServiceProvider.GetRequiredService<INotificationService>();
            var inAppService         = scope.ServiceProvider.GetRequiredService<IInAppNotificationService>();

            // ScheduledStartTime is stored as server-local time (no UTC conversion on form submit),
            // so compare against DateTime.Now rather than UtcNow to avoid a 4-hour EDT offset.
            var now       = DateTime.Now;
            var lookback  = now - MaxLookback;

            // Find appointments where:
            //   - Reminder is enabled and has not been sent yet
            //   - The reminder window has opened: ScheduledStartTime - ReminderMinutesBefore <= now
            //   - The appointment hasn't been sitting unprocessed for more than MaxLookback
            //   - The appointment status is not terminal (not cancelled, completed, no-show, etc.)
            // IgnoreQueryFilters bypasses the tenant filter — no HTTP context in a background service.
            var candidates = await db.Appointments
                .IgnoreQueryFilters()
                .Include(a => a.Customer)
                .Include(a => a.AppointmentStatus)
                .Where(a =>
                    !a.IsDeleted &&
                    a.IsReminderEnabled &&
                    a.ReminderSentAt == null &&
                    a.ScheduledStartTime > lookback &&
                    EF.Functions.DateDiffMinute(now, a.ScheduledStartTime) <= a.ReminderMinutesBefore &&
                    !a.AppointmentStatus.IsTerminalStatus)
                .ToListAsync(ct);

            if (candidates.Count == 0) return;

            _logger.LogInformation(
                "AppointmentReminderBackgroundService: {Count} appointment reminder(s) to dispatch.",
                candidates.Count);

            // Stamp ReminderSentAt before sending — prevents a restart from re-sending.
            var stampedAt = now;
            foreach (var appt in candidates)
                appt.ReminderSentAt = stampedAt;

            await db.SaveChangesAsync(ct);

            // Now send notifications. Failures here don't roll back the stamp because we'd
            // rather skip one reminder than spam a customer on every restart.
            foreach (var appt in candidates)
            {
                if (ct.IsCancellationRequested) break;

                try
                {
                    // Email to linked customer (no-ops internally if customer has opted out)
                    await notificationService.NotifyAppointmentReminderAsync(appt);

                    // In-app bell notification for company staff
                    var when = appt.IsAllDay
                        ? appt.ScheduledStartTime.ToString("MMMM d, yyyy")
                        : appt.ScheduledStartTime.ToString("MMMM d, yyyy 'at' h:mm tt");

                    await inAppService.CreateAsync(
                        companyId:        appt.CompanyId,
                        title:            $"Appointment Reminder: {appt.Title}",
                        message:          $"{appt.AppointmentNumber} is scheduled for {when}.",
                        notificationType: "AppointmentReminder",
                        link:             $"/Appointments/Details/{appt.Id}",
                        customerId:       appt.CustomerId);

                    _logger.LogInformation(
                        "Reminder dispatched for appointment {AppointmentNumber} (id {Id}, company {CompanyId}).",
                        appt.AppointmentNumber, appt.Id, appt.CompanyId);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex,
                        "Failed to dispatch reminder for appointment {AppointmentId}.", appt.Id);
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "AppointmentReminderBackgroundService poll failed.");
        }
    }
}
