using CredentialManager.Models; using CredentialManager.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Data_Coupler.Services; /// /// Servizio background che gestisce l'esecuzione automatica dei profili schedulati /// Controlla ogni minuto se ci sono profili da eseguire secondo la schedulazione impostata /// public class ScheduledExecutionBackgroundService : BackgroundService { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(1); // Controlla ogni minuto public ScheduledExecutionBackgroundService( IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Servizio di schedulazione automatica avviato. Controllo ogni {CheckInterval} minuti.", _checkInterval.TotalMinutes); while (!stoppingToken.IsCancellationRequested) { try { await CheckAndExecuteScheduledProfiles(); } catch (Exception ex) { _logger.LogError(ex, "Errore durante il controllo delle schedulazioni"); } // Attendi il prossimo controllo await Task.Delay(_checkInterval, stoppingToken); } _logger.LogInformation("Servizio di schedulazione automatica arrestato."); } private async Task CheckAndExecuteScheduledProfiles() { using var scope = _serviceProvider.CreateScope(); var scheduleService = scope.ServiceProvider.GetRequiredService(); var executionService = scope.ServiceProvider.GetRequiredService(); var profileService = scope.ServiceProvider.GetRequiredService(); try { // Ottieni tutte le schedulazioni attive var activeSchedules = await scheduleService.GetActiveSchedulesAsync(); var currentTime = DateTime.Now; // Usa l'ora locale per il confronto con le schedulazioni _logger.LogDebug("Controllo schedulazioni: {ScheduleCount} schedulazioni attive alle {CurrentTime}", activeSchedules.Count, DateTimeHelper.FormatDateTime24H(currentTime)); foreach (var schedule in activeSchedules) { try { if (ShouldExecuteSchedule(schedule, currentTime)) { _logger.LogInformation("Esecuzione schedulata per profilo: {ProfileName} (Schedule: {ScheduleName}) - DeletionSync: {DeletionSync}", schedule.Profile?.Name ?? "N/A", schedule.Name, schedule.EnableDeletionSync); // Esegui il profilo con il flag deletion sync dalla schedulazione var result = await executionService.ExecuteProfileAsync(schedule.ProfileId, schedule.EnableDeletionSync); // Aggiorna la schedulazione await UpdateScheduleAfterExecution(scheduleService, schedule, currentTime, result.Success); // Log del risultato if (result.Success) { _logger.LogInformation("Esecuzione schedulata completata con successo per {ProfileName}. " + "Record processati: {RecordsProcessed}, Durata: {Duration}ms", schedule.Profile?.Name ?? "N/A", result.RecordsProcessed, result.Duration.TotalMilliseconds); } else { _logger.LogError("Esecuzione schedulata fallita per {ProfileName}: {ErrorMessage}", schedule.Profile?.Name ?? "N/A", result.Message); } } } catch (Exception ex) { _logger.LogError(ex, "Errore durante l'esecuzione della schedulazione {ScheduleName} per il profilo {ProfileName}", schedule.Name, schedule.Profile?.Name ?? "N/A"); // Aggiorna la schedulazione anche in caso di errore try { await UpdateScheduleAfterExecution(scheduleService, schedule, currentTime, false); } catch (Exception updateEx) { _logger.LogError(updateEx, "Errore durante l'aggiornamento della schedulazione {ScheduleName} dopo l'errore", schedule.Name); } } } } catch (Exception ex) { _logger.LogError(ex, "Errore durante il recupero delle schedulazioni attive"); } } /// /// Determina se una schedulazione deve essere eseguita in base all'orario corrente /// private bool ShouldExecuteSchedule(ProfileSchedule schedule, DateTime currentTime) { if (!schedule.IsEnabled) { return false; } // Controllo per evitare esecuzioni multiple nella stessa finestra temporale (tolleranza di 1 minuto) if (schedule.LastExecutionTime.HasValue) { var timeSinceLastExecution = currentTime - schedule.LastExecutionTime.Value; if (timeSinceLastExecution.TotalMinutes < 1) { return false; } } return schedule.ScheduleType.ToLower() switch { "once" => ShouldExecuteOnceSchedule(schedule, currentTime), "daily" => ShouldExecuteDailySchedule(schedule, currentTime), "weekly" => ShouldExecuteWeeklySchedule(schedule, currentTime), "monthly" => ShouldExecuteMonthlySchedule(schedule, currentTime), _ => false }; } private bool ShouldExecuteOnceSchedule(ProfileSchedule schedule, DateTime currentTime) { if (!schedule.ScheduledDateTime.HasValue) { _logger.LogWarning("Schedulazione 'once' senza data/ora specificata: {ScheduleName}", schedule.Name); return false; } var scheduledTime = schedule.ScheduledDateTime.Value; // Esegui se l'orario programmato è passato e non è mai stato eseguito return currentTime >= scheduledTime && !schedule.LastExecutionTime.HasValue; } private bool ShouldExecuteDailySchedule(ProfileSchedule schedule, DateTime currentTime) { if (string.IsNullOrEmpty(schedule.DailyTime)) { _logger.LogWarning("Schedulazione 'daily' senza orario specificato: {ScheduleName}", schedule.Name); return false; } if (!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime)) { _logger.LogWarning("Formato orario non valido per schedulazione daily {ScheduleName}: {DailyTime} (formato richiesto: HH:mm)", schedule.Name, schedule.DailyTime); return false; } var currentTimeOfDay = currentTime.TimeOfDay; var scheduledTimeOfDay = scheduledTime; // Verifica se siamo nell'orario giusto (con tolleranza di ±1 minuto) var timeDifference = Math.Abs((currentTimeOfDay - scheduledTimeOfDay).TotalMinutes); if (timeDifference > 1) { return false; } // Se non è mai stato eseguito, esegui if (!schedule.LastExecutionTime.HasValue) { return true; } // Se è stato eseguito, verifica che sia passato almeno un giorno var lastExecutionDate = schedule.LastExecutionTime.Value.Date; var currentDate = currentTime.Date; return currentDate > lastExecutionDate; } private bool ShouldExecuteWeeklySchedule(ProfileSchedule schedule, DateTime currentTime) { if (!schedule.DayOfWeek.HasValue || string.IsNullOrEmpty(schedule.DailyTime)) { _logger.LogWarning("Schedulazione 'weekly' senza giorno della settimana o orario specificato: {ScheduleName}", schedule.Name); return false; } if (!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime)) { _logger.LogWarning("Formato orario non valido per schedulazione weekly {ScheduleName}: {DailyTime} (formato richiesto: HH:mm)", schedule.Name, schedule.DailyTime); return false; } // Verifica il giorno della settimana (0=Domenica, 1=Lunedì, etc.) var currentDayOfWeek = (int)currentTime.DayOfWeek; if (currentDayOfWeek != schedule.DayOfWeek.Value) { return false; } // Verifica l'orario (con tolleranza di ±1 minuto) var currentTimeOfDay = currentTime.TimeOfDay; var timeDifference = Math.Abs((currentTimeOfDay - scheduledTime).TotalMinutes); if (timeDifference > 1) { return false; } // Se non è mai stato eseguito, esegui if (!schedule.LastExecutionTime.HasValue) { return true; } // Se è stato eseguito, verifica che sia passata almeno una settimana var daysSinceLastExecution = (currentTime.Date - schedule.LastExecutionTime.Value.Date).TotalDays; return daysSinceLastExecution >= 7; } private bool ShouldExecuteMonthlySchedule(ProfileSchedule schedule, DateTime currentTime) { if (!schedule.DayOfMonth.HasValue || string.IsNullOrEmpty(schedule.DailyTime)) { _logger.LogWarning("Schedulazione 'monthly' senza giorno del mese o orario specificato: {ScheduleName}", schedule.Name); return false; } if (!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime)) { _logger.LogWarning("Formato orario non valido per schedulazione monthly {ScheduleName}: {DailyTime} (formato richiesto: HH:mm)", schedule.Name, schedule.DailyTime); return false; } // Verifica il giorno del mese var currentDayOfMonth = currentTime.Day; var scheduledDayOfMonth = schedule.DayOfMonth.Value; // Gestione per mesi con meno giorni (es. 31 in febbraio diventa ultimo giorno del mese) var daysInCurrentMonth = DateTime.DaysInMonth(currentTime.Year, currentTime.Month); var effectiveScheduledDay = Math.Min(scheduledDayOfMonth, daysInCurrentMonth); if (currentDayOfMonth != effectiveScheduledDay) { return false; } // Verifica l'orario (con tolleranza di ±1 minuto) var currentTimeOfDay = currentTime.TimeOfDay; var timeDifference = Math.Abs((currentTimeOfDay - scheduledTime).TotalMinutes); if (timeDifference > 1) { return false; } // Se non è mai stato eseguito, esegui if (!schedule.LastExecutionTime.HasValue) { return true; } // Se è stato eseguito, verifica che sia passato almeno un mese var lastExecutionDate = schedule.LastExecutionTime.Value; var monthsSinceLastExecution = ((currentTime.Year - lastExecutionDate.Year) * 12) + currentTime.Month - lastExecutionDate.Month; return monthsSinceLastExecution >= 1; } /// /// Aggiorna la schedulazione dopo l'esecuzione /// private async Task UpdateScheduleAfterExecution(IProfileScheduleService scheduleService, ProfileSchedule schedule, DateTime executionTime, bool wasSuccessful) { try { // Aggiorna i dati della schedulazione schedule.LastExecutionTime = executionTime; schedule.ExecutionCount++; // Calcola il prossimo tempo di esecuzione schedule.NextExecutionTime = CalculateNextExecutionTime(schedule, executionTime); // Per schedulazioni "once", disabilita dopo l'esecuzione if (schedule.ScheduleType.ToLower() == "once") { schedule.IsEnabled = false; _logger.LogInformation("Schedulazione 'once' {ScheduleName} disabilitata dopo l'esecuzione", schedule.Name); } await scheduleService.UpdateScheduleAsync(schedule); _logger.LogDebug("Schedulazione {ScheduleName} aggiornata. Prossima esecuzione: {NextExecution}", schedule.Name, schedule.NextExecutionTime.HasValue ? DateTimeHelper.FormatDateTime24H(schedule.NextExecutionTime.Value) : "N/A"); } catch (Exception ex) { _logger.LogError(ex, "Errore durante l'aggiornamento della schedulazione {ScheduleName}", schedule.Name); } } /// /// Calcola il prossimo tempo di esecuzione per una schedulazione /// private DateTime? CalculateNextExecutionTime(ProfileSchedule schedule, DateTime lastExecution) { if (!schedule.IsEnabled) { return null; } return schedule.ScheduleType.ToLower() switch { "once" => null, // Schedulazione singola non ha prossima esecuzione "daily" => CalculateNextDailyExecution(schedule, lastExecution), "weekly" => CalculateNextWeeklyExecution(schedule, lastExecution), "monthly" => CalculateNextMonthlyExecution(schedule, lastExecution), _ => null }; } private DateTime? CalculateNextDailyExecution(ProfileSchedule schedule, DateTime lastExecution) { if (string.IsNullOrEmpty(schedule.DailyTime) || !DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime)) { return null; } var nextDate = lastExecution.Date.AddDays(1); return nextDate.Add(scheduledTime); } private DateTime? CalculateNextWeeklyExecution(ProfileSchedule schedule, DateTime lastExecution) { if (!schedule.DayOfWeek.HasValue || string.IsNullOrEmpty(schedule.DailyTime) || !DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime)) { return null; } var nextDate = lastExecution.Date.AddDays(7); return nextDate.Add(scheduledTime); } private DateTime? CalculateNextMonthlyExecution(ProfileSchedule schedule, DateTime lastExecution) { if (!schedule.DayOfMonth.HasValue || string.IsNullOrEmpty(schedule.DailyTime) || !DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime)) { return null; } var nextMonth = lastExecution.AddMonths(1); var daysInNextMonth = DateTime.DaysInMonth(nextMonth.Year, nextMonth.Month); var effectiveDay = Math.Min(schedule.DayOfMonth.Value, daysInNextMonth); var nextDate = new DateTime(nextMonth.Year, nextMonth.Month, effectiveDay); return nextDate.Add(scheduledTime); } }