using JdeScoping.Core.Models; using JdeScoping.Core.Models.Enums; using JdeScoping.Core.Models.Infrastructure; using JdeScoping.DataSync.Options; using JdeScoping.DataSync.Contracts; using JdeScoping.DataSync.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace JdeScoping.DataSync.Services; /// /// Checks schedules and determines which sync tasks need to be executed. /// public class ScheduleChecker : IScheduleChecker { private readonly IDataUpdateRepository _repository; private readonly IOptions _options; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// public ScheduleChecker( IDataUpdateRepository repository, IOptions options, ILogger logger) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _options = options ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// public async Task> GetPendingTasksAsync(CancellationToken cancellationToken = default) { var lastUpdates = await _repository.GetLastDataUpdatesAsync(cancellationToken); var tasks = new List(); var now = DateTime.UtcNow; foreach (var config in _options.Value.DataSources.Where(c => c.IsEnabled)) { var task = CheckConfigSchedule(config, lastUpdates, now); if (task != null) { tasks.Add(task); } } if (tasks.Count > 0) { _logger.LogInformation( "Found {Count} pending sync tasks: {Tasks}", tasks.Count, string.Join(", ", tasks.Select(t => $"{t.TableName}({t.UpdateType})"))); } else { _logger.LogDebug("No pending sync tasks found"); } return tasks; } /// /// Checks a single data source config and returns a task if sync is needed. /// Priority order: Mass > Daily > Hourly /// private DataUpdateTask? CheckConfigSchedule( DataSourceConfig config, Dictionary lastUpdates, DateTime now) { // Get last updates for each type var massKey = GetUpdateKey(config.TableName, UpdateTypes.Mass); var dailyKey = GetUpdateKey(config.TableName, UpdateTypes.Daily); var hourlyKey = GetUpdateKey(config.TableName, UpdateTypes.Hourly); lastUpdates.TryGetValue(massKey, out var lastMass); lastUpdates.TryGetValue(dailyKey, out var lastDaily); lastUpdates.TryGetValue(hourlyKey, out var lastHourly); // Check Mass first (highest priority) if (config.MassConfig.Enabled && NeedsMassSync(config, lastMass, now)) { _logger.LogDebug( "Mass sync needed for {Table}: last={LastSync}, interval={Interval}m", config.TableName, lastMass?.EndDt.ToString("o") ?? "never", config.MassConfig.IntervalMinutes); return CreateTask(config, UpdateTypes.Mass, null); } // Check Daily if (config.DailyConfig.Enabled && NeedsDailySync(config, lastDaily, lastMass, now)) { var minimumDt = CalculateMinimumDt(lastDaily, config.DailyConfig.IntervalMinutes); _logger.LogDebug( "Daily sync needed for {Table}: last={LastSync}, interval={Interval}m, minDT={MinDT}", config.TableName, lastDaily?.EndDt.ToString("o") ?? "never", config.DailyConfig.IntervalMinutes, minimumDt?.ToString("o") ?? "null"); return CreateTask(config, UpdateTypes.Daily, minimumDt); } // Check Hourly (uses Daily's last timestamp for MinimumDT calculation, per legacy behavior) if (config.HourlyConfig.Enabled && NeedsHourlySync(config, lastHourly, lastDaily, lastMass, now)) { // Use daily update timestamp for lookback, not hourly var minimumDt = CalculateMinimumDt(lastDaily, config.DailyConfig.IntervalMinutes); _logger.LogDebug( "Hourly sync needed for {Table}: last={LastSync}, interval={Interval}m, minDT={MinDT}", config.TableName, lastHourly?.EndDt.ToString("o") ?? "never", config.HourlyConfig.IntervalMinutes, minimumDt?.ToString("o") ?? "null"); return CreateTask(config, UpdateTypes.Hourly, minimumDt); } return null; } /// /// Determines if a mass sync is needed. /// private bool NeedsMassSync(DataSourceConfig config, DataUpdate? lastMass, DateTime now) { // Never synced before - need mass sync if (lastMass == null) { return true; } // Check if successful last mass sync was within interval if (!lastMass.WasSuccessful) { // Last sync failed - try again return true; } var nextSyncDue = lastMass.EndDt.AddMinutes(config.MassConfig.IntervalMinutes); return now > nextSyncDue; } /// /// Determines if a daily sync is needed. /// private bool NeedsDailySync(DataSourceConfig config, DataUpdate? lastDaily, DataUpdate? lastMass, DateTime now) { // If no mass sync ever happened, we need mass first if (lastMass == null) { return false; } // Never done daily sync if (lastDaily == null) { return true; } // Check if successful last daily sync was within interval if (!lastDaily.WasSuccessful) { return true; } var nextSyncDue = lastDaily.EndDt.AddMinutes(config.DailyConfig.IntervalMinutes); return now > nextSyncDue; } /// /// Determines if an hourly sync is needed. /// private bool NeedsHourlySync( DataSourceConfig config, DataUpdate? lastHourly, DataUpdate? lastDaily, DataUpdate? lastMass, DateTime now) { // If no mass sync ever happened, we need mass first if (lastMass == null) { return false; } // Never done hourly sync if (lastHourly == null) { return true; } // Check if successful last hourly sync was within interval if (!lastHourly.WasSuccessful) { return true; } var nextSyncDue = lastHourly.EndDt.AddMinutes(config.HourlyConfig.IntervalMinutes); return now > nextSyncDue; } /// /// Calculates the MinimumDT for incremental updates using lookback multiplier. /// private DateTime? CalculateMinimumDt(DataUpdate? lastUpdate, int intervalMinutes) { if (lastUpdate == null) { return null; } var lookbackMinutes = _options.Value.LookbackMultiplier * intervalMinutes; return lastUpdate.EndDt.AddMinutes(-lookbackMinutes); } /// /// Creates a data update task. /// private static DataUpdateTask CreateTask(DataSourceConfig config, UpdateTypes updateType, DateTime? minimumDt) { return new DataUpdateTask { TableName = config.TableName, SourceSystem = config.SourceSystem, SourceData = config.SourceData, UpdateType = updateType, MinimumDt = minimumDt, Config = config }; } /// /// Gets the dictionary key for looking up last updates. /// private static string GetUpdateKey(string tableName, UpdateTypes updateType) { return $"{tableName}_{(int)updateType}"; } }