using Microsoft.Extensions.Logging; using PARR.Core.Services.MatchingStatusService; using PARR.Domain.Cache.Models; using PARR.Domain.Entities.Base.History; using PARR.Domain.Enums; using PARR.TemplateMatcher.Exceptions; using PARR.TemplateMatcher.Services.GroupedSync; using PARR.TemplateMatcher.Services.Implementations.GroupedSync; using PARR.TemplateMatcher.Services.Interfaces; using System.Diagnostics; namespace PARR.TemplateMatcher.Services.Implementations; internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { private readonly IEnumerable _readStages; private readonly IEnumerable _writeStages; private readonly IMatchingStatusService _matchingStatusService; private readonly ILogger _logger; public GroupedTemplateSynchronizer( IEnumerable readStages, IEnumerable writeStages, IMatchingStatusService matchingStatusService, ILogger logger ) { _readStages = readStages; _writeStages = writeStages; _matchingStatusService = matchingStatusService; _logger = logger; } public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator, bool dryRun = false, CancellationToken ct = default) { _logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId} (DryRun={DryRun})", jobGroupId, dryRun); var existingStatus = await _matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); if (existingStatus.DetailsJobGroups?.Count > 0) { _logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId); return; } await SetStatusAsync(jobGroupId, dryRun ? "DryRun: Начало анализа" : "Начало синхронизации"); var totalSw = Stopwatch.StartNew(); try { var context = new GroupedSyncContext { JobGroupId = jobGroupId, Initiator = initiator, DryRun = dryRun }; // Read-этапы выполняются всегда foreach (var stage in _readStages) { var stageSw = Stopwatch.StartNew(); await stage.ExecuteAsync(context); stageSw.Stop(); _logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс", context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds); } // Подробный отчёт по результатам read-этапов PrintDryRunReport(context); // Write-этапы пропускаем при DryRun if (!dryRun) { foreach (var stage in _writeStages) { var stageSw = Stopwatch.StartNew(); await stage.ExecuteAsync(context); stageSw.Stop(); _logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс", context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds); } await SetStatusAsync(jobGroupId, "Синхронизация завершена успешно"); await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); } else { _logger.LogInformation("[DryRun] Write-этапы пропущены. Изменения в БД и MQ не выполнены."); await SetStatusAsync(jobGroupId, "DryRun: Анализ завершён без изменений"); await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); } totalSw.Stop(); _logger.LogInformation("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | ИТОГО: {TotalMs} мс", context.JobGroupName, jobGroupId, totalSw.ElapsedMilliseconds); } catch (SyncEarlyExitException ex) { totalSw.Stop(); _logger.LogInformation("JobGroup {JobGroupId}: {Reason} ({ElapsedMs} мс)", jobGroupId, ex.Reason, totalSw.ElapsedMilliseconds); await SetStatusAsync(jobGroupId, ex.Reason); await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); } catch (Exception ex) { totalSw.Stop(); _logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс", jobGroupId, totalSw.ElapsedMilliseconds); await SetStatusAsync(jobGroupId, $"Ошибка: {ex.Message}"); throw; } } private void PrintDryRunReport(GroupedSyncContext context) { _logger.LogInformation("========== [DryRun] ОТЧЁТ по JobGroup '{JobGroupName}' ({JobGroupId}) ==========", context.JobGroupName, context.JobGroupId); _logger.LogInformation("[DryRun] Отфильтровано юнитов: {Count}", context.FilteredUnits.Count); _logger.LogInformation("[DryRun] Разрешено конфликтов (potentialUnits): {Count}", context.ReverseMapping.Count); _logger.LogInformation("[DryRun] Сформировано групп: {Count}", context.TemplateGroups.Count); int totalSubGroups = 0; int totalUnitsInTemplates = 0; foreach (var group in context.TemplateGroups.OrderBy(g => g.PotentialUnitId)) { _logger.LogInformation( "[DryRun] PotentialUnit {Unit}: {SubGroupCount} подгрупп", context.FormatUnit(group.PotentialUnitId), group.SubGroups.Count); foreach (var subGroup in group.SubGroups.OrderBy(s => s.GlobalIndex)) { _logger.LogInformation( "[DryRun] Index={Index}, группа='{Group}', экземпляров: {Count}", subGroup.GlobalIndex, subGroup.InnerGroupName, subGroup.Entries.Count); foreach (var entry in subGroup.Entries) { _logger.LogDebug( "[DryRun] - {Unit} (ValueId: {ValueId})", context.FormatUnit(entry.UnitId), entry.UnitFieldValueId); } totalSubGroups++; totalUnitsInTemplates += subGroup.Entries.Count; } } _logger.LogInformation("[DryRun] ИТОГО: {GroupCount} групп, {SubGroupCount} подгрупп (=шаблонов), {UnitCount} экземпляров в шаблонах", context.TemplateGroups.Count, totalSubGroups, totalUnitsInTemplates); _logger.LogInformation("========== [DryRun] КОНЕЦ ОТЧЁТА =========="); } public Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator, bool dryRun = false, CancellationToken ct = default) { _logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId); return Task.CompletedTask; } public Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator, CancellationToken ct = default) { _logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId); return Task.CompletedTask; } private async Task SetStatusAsync(Guid jobGroupId, string comment) { var status = new MatchingStatusItemDto { DateStart = DateTimeOffset.UtcNow, Action = TemplateMatcherActionEnum.Sync, Comment = comment }; await _matchingStatusService.SetMatchingStatusAsync( jobGroupId, SyncTaskEntityTypeEnum.JobGroup, new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) }, TimeSpan.FromMinutes(30)); } }