using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces.Job; using PARR.Core.Services.MatchingStatusService; using PARR.Core.Services.UnitFilterService; using PARR.Domain.Cache.Models; using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Job; using PARR.Domain.Enums; using PARR.TemplateMatcher.Services.Interfaces; namespace PARR.TemplateMatcher.Services.Implementations; internal class GroupedTemplateSynchronizer : ITemplateSynchronizer { private readonly ILogger logger; private readonly IJobGroupRepository jobGroupService; private readonly IUnitFilterService unitFilterService; private readonly IGroupedTemplateUnitFilter groupedTemplateUnitFilter; private readonly IUnitInTemplateConflictMapper unitInTemplateConflictMapper; private readonly IGroupedTemplateBuilder groupedTemplateBuilder; private readonly IGroupedTemplateProcessor groupedTemplateProcessor; private readonly ITemplateRepository templateService; private readonly ITemplateDeactivator templateDeactivator; private readonly IMatchingStatusService matchingStatusService; public GroupedTemplateSynchronizer( ILogger logger, IJobGroupRepository jobGroupService, IUnitFilterService unitFilterService, IGroupedTemplateUnitFilter groupedTemplateUnitFilter, IUnitInTemplateConflictMapper unitInTemplateConflictMapper, IGroupedTemplateBuilder groupedTemplateBuilder, IGroupedTemplateProcessor groupedTemplateProcessor, ITemplateRepository templateService, ITemplateDeactivator templateDeactivator, IMatchingStatusService matchingStatusService) { this.logger = logger; this.jobGroupService = jobGroupService; this.unitFilterService = unitFilterService; this.groupedTemplateUnitFilter = groupedTemplateUnitFilter; this.unitInTemplateConflictMapper = unitInTemplateConflictMapper; this.groupedTemplateBuilder = groupedTemplateBuilder; this.groupedTemplateProcessor = groupedTemplateProcessor; this.templateService = templateService; this.templateDeactivator = templateDeactivator; this.matchingStatusService = matchingStatusService; } public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId); } public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) { logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId); // === Проверка: уже запущена? === var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); if (existingStatus.DetailsJobGroups?.Any() == true) { logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId); return; } // === Устанавливаем статус "в процессе" === var initialStatus = new MatchingStatusItemDto { DateStart = DateTimeOffset.UtcNow, Action = TemplateMatcherActionEnum.Sync, Comment = "Начало синхронизации" }; await matchingStatusService.SetMatchingStatusAsync( jobGroupId, SyncTaskEntityTypeEnum.JobGroup, new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) }, TimeSpan.FromMinutes(35) ); try { // 1. Загрузка JobGroup и связанных Job'ов var jobGroup = await jobGroupService.Get() .AsNoTracking() .AsSingleQuery() .Include(jg => jg.GroupType) .Include(jg => jg.Jobs) .ThenInclude(j => j.AutoControl) .Include(jg => jg.Jobs) .ThenInclude(j => j.UnitFilters) .ThenInclude(uf => uf.RelationshipFilters) .ThenInclude(rf => rf.UnitField) .Include(jg => jg.Jobs) .ThenInclude(jg => jg.Tnk) .FirstOrDefaultAsync(jg => jg.Id == jobGroupId); if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any()) { logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст"); return; } var jobsInGroup = jobGroup.Jobs.ToList(); // 2. Поиск Job с максимальным MaxValueRelationships var maxJob = jobsInGroup .Where(j => j.MaxValueRelationships.HasValue) .OrderByDescending(j => j.MaxValueRelationships) .FirstOrDefault(); if (maxJob == null) { logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships"); return; } logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}).", maxJob.Id, maxJob.MaxValueRelationships); // 3. Получение отфильтрованных юнитов через UnitFilterService logger.LogDebug("Получение отфильтрованных юнитов через UnitFilterService для Job {JobId}.", maxJob.Id); var unitFilterResults = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id); if (unitFilterResults == null || !unitFilterResults.Any()) { logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями"); await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); return; } // 4. Применение специфичных правил фильтрации для групповых шаблонов logger.LogDebug("Применение специфичных правил фильтрации для групповых шаблонов."); var finalFilteredUnits = await groupedTemplateUnitFilter.FilterAsync(unitFilterResults, jobGroup); if (!finalFilteredUnits.Any()) { logger.LogInformation("После применения правил фильтрации в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации"); await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); return; } // 5. Разрешение конфликтов связей и построение первичного маппинга logger.LogDebug("Разрешение конфликтов связей и построение первичного маппинга."); var initialReverseMapping = await unitInTemplateConflictMapper.BuildMappingAsync(finalFilteredUnits, maxJob); if (!initialReverseMapping.Any()) { logger.LogInformation("После разрешения конфликтов в JobGroup {JobGroupId} не осталось связей.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Нет связей после разрешения конфликтов"); return; } // 6. Построение структуры групп (трансформация, внутренняя группировка, разбиение) logger.LogDebug("Построение структуры групп для шаблонов."); var templateGroups = await groupedTemplateBuilder.BuildAsync(initialReverseMapping, jobGroup, maxJob); if (!templateGroups.Any()) { logger.LogInformation("После построения структуры групп в JobGroup {JobGroupId} не осталось данных.", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, "Нет данных после построения групп"); return; } // 7. Обработка групп: сравнение, обновление, создание, отправка MQ logger.LogDebug("Обработка групп шаблонов: сравнение, обновление и создание."); var expectedTemplateKeys = await groupedTemplateProcessor.ProcessAsync( templateGroups, jobsInGroup, maxJob, initiator); // 8. Деактивация лишних шаблонов await DeactivateUnusedTemplatesAsync(expectedTemplateKeys, jobGroupId, jobsInGroup, initiator); // 9. Успешное завершение await UpdateMatchingStatusAsync(jobGroupId, "Синхронизация завершена успешно"); await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId); } catch (Exception ex) { logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId}", jobGroupId); await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}"); throw; } } public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId); } private async Task DeactivateUnusedTemplatesAsync( HashSet<(Guid JobId, Guid UnitId, int Index)> expectedKeys, Guid jobGroupId, List jobsInGroup, HistoryInitiator initiator) { var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet(); var allExistingTemplatesInGroup = await templateService.Get() .AsNoTracking() .Include(t => t.Unit) .Include(t => t.UnitsInTemplate) .Where(t => allJobIdsInGroup.Contains(t.JobId) && t.StatusTypeId == TemplateStatusTypeEnum.Used && t.Job!.GroupId == jobGroupId) .ToListAsync(); foreach (var existingTemplate in allExistingTemplatesInGroup) { var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1); if (!expectedKeys.Contains(key)) { logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Unit {UnitId}, Index {Index}).", existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index); await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator); } } } private async Task UpdateMatchingStatusAsync(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) ); } }