using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.Core.Common.Interfaces.RabbitServices; using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces.Unit; using PARR.Domain.Common.Rabbit.Messages.TemplateMatching; using PARR.Domain.Entities; using PARR.Domain.Entities.Base.History; using PARR.Domain.Entities.Job; using PARR.Domain.Enums; using PARR.TemplateMatcher.Models; using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Settings; namespace PARR.TemplateMatcher.Services.GroupedSync; internal class GroupedTemplateProcessor : IGroupedTemplateProcessor { private readonly ILogger logger; private readonly ITemplateRepository templateRepository; private readonly IUnitRepository unitRepository; private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateAllocationService templateAllocationService; private readonly ITemplateMqPublisher templateMqPublisher; public GroupedTemplateProcessor( ILogger logger, ITemplateRepository templateRepository, IUnitRepository unitRepository, ITemplateNameNormalizer templateNameNormalizer, ITemplateAllocationService templateAllocationService, ITemplateMqPublisher templateMqPublisher, MqSettings mqSettings, IRabbitService mqService) { this.logger = logger; this.templateRepository = templateRepository; this.unitRepository = unitRepository; this.templateNameNormalizer = templateNameNormalizer; this.templateAllocationService = templateAllocationService; this.templateMqPublisher = templateMqPublisher; } public async Task> ProcessAsync( List groups, List jobsInGroup, Job maxJob, HistoryInitiator initiator, CancellationToken ct = default) { var expectedTemplateKeys = new HashSet<(Guid JobId, Guid UnitId, int Index)>(); foreach (var group in groups) { var potentialUnitId = group.PotentialUnitId; foreach (var subGroup in group.SubGroups) { var unitsInTemplateSubGroup = subGroup.Entries; var globalIndex = subGroup.GlobalIndex; var originatingInnerGroupName = subGroup.InnerGroupName; logger.LogDebug("Обработка подгруппы {Index} ('{GroupingValue}') для UnitId {PotentialUnitId}, размер {Size}.", globalIndex, originatingInnerGroupName, potentialUnitId, unitsInTemplateSubGroup.Count); Job targetJob = SelectTargetJob(jobsInGroup, unitsInTemplateSubGroup.Count, maxJob); expectedTemplateKeys.Add((targetJob.Id, potentialUnitId, globalIndex)); // Поиск существующего шаблона var existingTemplate = await templateRepository.Get() .AsNoTracking() .Include(t => t.Unit) .Include(t => t.Job).ThenInclude(t => t!.Tnk) .Include(t => t.Job).ThenInclude(t => t!.Group).ThenInclude(t => t!.GroupType) .Include(t => t.UnitsInTemplate).ThenInclude(uit => uit.Unit) .Where(t => t.JobId == targetJob.Id && t.UnitId == potentialUnitId && t.Index == globalIndex && t.StatusTypeId == TemplateStatusTypeEnum.Used) .FirstOrDefaultAsync(ct); if (existingTemplate != null) { await HandleExistingTemplateAsync(existingTemplate, unitsInTemplateSubGroup, targetJob, globalIndex, initiator, ct); } else { await HandleNewOrReusableTemplateAsync(potentialUnitId, unitsInTemplateSubGroup, targetJob, globalIndex, initiator, ct); } } } return expectedTemplateKeys; } private async Task HandleExistingTemplateAsync( Template existingTemplate, List<(Guid UnitId, Guid UnitFieldValueId)> proposedEntries, Job targetJob, int globalIndex, HistoryInitiator initiator, CancellationToken ct) { var currentEntries = existingTemplate.UnitsInTemplate .Select(uit => (uit.UnitId, uit.UnitFieldValueId)) .ToList(); // Сравнение var allUnitIdsForSort = currentEntries.Select(e => e.UnitId) .Concat(proposedEntries.Select(e => e.UnitId)) .Distinct() .ToList(); var unitNamesForSort = await unitRepository.Get() .AsNoTracking() .Where(u => allUnitIdsForSort.Contains(u.Id)) .ToDictionaryAsync(u => u.Id, u => u.Name ?? u.Id.ToString(), ct); var sortedCurrent = currentEntries .OrderBy(e => unitNamesForSort.GetValueOrDefault(e.UnitId, e.UnitId.ToString())) .ThenBy(e => e.UnitFieldValueId) .ToList(); var sortedProposed = proposedEntries .OrderBy(e => unitNamesForSort.GetValueOrDefault(e.UnitId, e.UnitId.ToString())) .ThenBy(e => e.UnitFieldValueId) .ToList(); bool unitsAreEqual = sortedCurrent.SequenceEqual(sortedProposed); if (unitsAreEqual) { logger.LogDebug("Шаблон {TemplateId} актуален по составу.", existingTemplate.Id); // Проверка имени var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(existingTemplate); if (!string.Equals(existingTemplate.Name, expectedName, StringComparison.OrdinalIgnoreCase)) { logger.LogInformation("Шаблон {TemplateId} требует обновления имени.", existingTemplate.Id); var updateRequest = new TemplateUpdaterMessage { TemplateId = existingTemplate.Id, JobId = targetJob.Id, UnitId = existingTemplate.UnitId, Name = expectedName, IsActiveTemplate = existingTemplate.IsActiveTemplate, IsActiveSchedule = existingTemplate.IsActiveSchedule, IsNew = false, Index = globalIndex, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, UnitsInTemplate = sortedProposed.Select(t => new UnitInTemplateMessage { UnitId = t.UnitId, UnitFieldValueId = t.UnitFieldValueId }).ToList() }; await templateMqPublisher.PublishUpdateAsync(updateRequest); } } else { logger.LogInformation("Шаблон {TemplateId} требует обновления состава.", existingTemplate.Id); await UpdateTemplateUnitsAsync(existingTemplate, sortedProposed, targetJob, globalIndex, initiator); } } private async Task HandleNewOrReusableTemplateAsync( Guid potentialUnitId, List<(Guid UnitId, Guid UnitFieldValueId)> unitsInTemplateSubGroup, Job targetJob, int globalIndex, HistoryInitiator initiator, CancellationToken ct) { // Вычисляем флаги активности var isActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? false; var isActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? false; // Маппим кортежи в сообщения var unitsInTemplateMsg = unitsInTemplateSubGroup .Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }) .ToList(); var request = new TemplateAllocationRequest( TargetJob: targetJob, TargetUnitId: potentialUnitId, TargetUnit: null, Index: globalIndex, UnitsInTemplate: unitsInTemplateMsg, IsActiveTemplate: isActiveTemplate, IsActiveSchedule: isActiveSchedule, Initiator: initiator); await templateAllocationService.AllocateAsync(request, ct); } private async Task UpdateTemplateUnitsAsync( Template template, List<(Guid UnitId, Guid UnitFieldValueId)> newUnitEntries, Job targetJob, int newIndex, HistoryInitiator initiator) { // 1. Устанавливаем статус и дату template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; // 2. КОММИТ В БАЗУ СРАЗУ // Важно зафиксировать изменение статуса до отправки сообщения в очередь if (!await templateRepository.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id); return; } // 3. Формируем временный объект для генерации имени var tempTemplateForName = new Template { Id = template.Id, Name = template.Name, JobId = targetJob.Id, UnitId = template.UnitId, Index = newIndex, Job = targetJob, Unit = template.Unit, UnitsInTemplate = newUnitEntries.Select(e => new UnitsInTemplate { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList() }; var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); // 4. Отправляем сообщение в очередь var updateRequest = new TemplateUpdaterMessage { TemplateId = template.Id, JobId = targetJob.Id, UnitId = template.UnitId, Name = expectedName, IsActiveTemplate = template.IsActiveTemplate, IsActiveSchedule = template.IsActiveSchedule, IsNew = false, Index = newIndex, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, UnitsInTemplate = newUnitEntries.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList() }; await templateMqPublisher.PublishUpdateAsync(updateRequest); } private static Job SelectTargetJob(List jobsInGroup, int subGroupSize, Job maxJob) { Job? targetJob = jobsInGroup .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value == subGroupSize) .FirstOrDefault(); if (targetJob == null) { targetJob = jobsInGroup .Where(j => j.MaxValueRelationships.HasValue && j.MaxValueRelationships.Value >= subGroupSize) .OrderBy(j => j.MaxValueRelationships!.Value) .FirstOrDefault(); } return targetJob ?? maxJob; } }