using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.BLL.Domain.Mq; using PARR.BLL.Services.Interfaces; using PARR.Common.Domain; using PARR.Constants; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.Models; using PARR.DAL.Models.Job; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.TransformServices; using PARR.TemplateMatcher.Settings; using System.Text.Json; namespace PARR.TemplateMatcher { internal class TemplateMatcher : ITemplateMatcher { private const int UnusedCandidateBatchSize = 10; private const bool DefaultUnusedTemplateState = false; private const bool DefaultUnusedScheduleState = false; private const bool DefaultUsedTemplateState = false; private const bool DefaultUsedScheduleState = false; private readonly ILogger logger; private readonly IUnitFilterService unitFilterService; private readonly MqSettings mqSettings; private readonly IMqService mqService; private readonly ITemplateService templateService; private readonly IJobService jobService; private readonly IShortcodesService shortcodesService; private readonly IEsppScheduleTransformService esppScheduleTransformService; public TemplateMatcher( ILogger logger, IUnitFilterService unitFilterService, MqSettings mqSettings, IMqService mqService, ITemplateService templateService, IJobService jobService, IShortcodesService shortcodesService, IEsppScheduleTransformService esppScheduleTransformService ) { this.logger = logger; this.unitFilterService = unitFilterService; this.mqSettings = mqSettings; this.mqService = mqService; this.templateService = templateService; this.jobService = jobService; this.shortcodesService = shortcodesService; this.esppScheduleTransformService = esppScheduleTransformService; } public async Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId); var expectedUnitIds = await GetExpectedUnitIdsAsync(jobId); if (!expectedUnitIds.Any()) { logger.LogInformation("Для JobId {JobId} не найдено Unit'ов по фильтрам.", jobId); return; } var job = await GetJobWithGroupAndAutoControlAsync(jobId); if (job == null) { logger.LogError("Job с Id {JobId} не найден.", jobId); return; } var existingTemplates = await templateService.Get() .Where(t => t.JobId == jobId) .ToListAsync(); logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.", jobId, expectedUnitIds.Count, existingTemplates.Count); // 4. Шаблоны, НЕ в фильтре → отправляем на деактивацию через worker var templatesToDeactivate = existingTemplates .Where(t => !expectedUnitIds.Contains(t.UnitId)) .ToList(); foreach (var template in templatesToDeactivate) { if (template.StatusTypeId == TemplateStatusTypeEnum.Updating) continue; logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация через worker.", template.Id, template.UnitId); // Только переводим в Updating — остальное пусть делает worker template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для деактивации.", template.Id); continue; } var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, JobId = jobId, UnitId = template.UnitId, Name = template.Name, IsActiveTemplate = DefaultUnusedTemplateState, IsActiveSchedule = DefaultUnusedScheduleState, LastRun = template.LastRun, NextRun = template.NextRun, Index = template.Index, StatusTypeId = TemplateStatusTypeEnum.Unused, Initiator = initiator }; await SendTemplateUpdateMessage(updateRequest); } // 5. Перечитываем шаблоны existingTemplates = await templateService.Get() .Where(t => t.JobId == jobId) .ToListAsync(); var unitToTemplate = existingTemplates.ToDictionary(t => t.UnitId, t => t); // 6. UnitId без шаблона var unitIdsMissingTemplates = expectedUnitIds .Where(unitId => !unitToTemplate.ContainsKey(unitId)) .ToList(); var unitIdsToCreateFresh = new List(); foreach (var unitId in unitIdsMissingTemplates) { var reused = await TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator); if (reused != null) { logger.LogInformation("Переиспользован шаблон {TemplateId} для UnitId {UnitId}.", reused.Id, unitId); var expectedName = await GetNormalizedTemplateNameAsync(job, unitId); var nextRun = await GetNextRunAsync(job); // новый шаблон → всегда пересчитываем var updateRequest = new TemplateUpdaterMq { TemplateId = reused.Id, JobId = jobId, UnitId = unitId, Name = expectedName, IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState, IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, NextRun = nextRun }; await SendTemplateUpdateMessage(updateRequest); } else { logger.LogInformation("Нет доступных Unused-шаблонов для UnitId {UnitId} → создадим новый.", unitId); unitIdsToCreateFresh.Add(unitId); } } // 9. Шаблоны В ФИЛЬТРЕ → обновление имени / реактивация var templatesInFilter = existingTemplates .Where(t => expectedUnitIds.Contains(t.UnitId)) .ToList(); foreach (var template in templatesInFilter) { var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId); bool needsUpdate = template.Name != expectedName || template.StatusTypeId != TemplateStatusTypeEnum.Used; if (needsUpdate && template.StatusTypeId != TemplateStatusTypeEnum.Updating) { logger.LogInformation("Шаблон {TemplateId}: требуется обновление имени или реактивация.", template.Id); template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id); continue; } var nextRun = await GetNextRunAsync(job, template.NextRun); var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, JobId = jobId, UnitId = template.UnitId, Name = expectedName, IsActiveTemplate = template.IsActiveTemplate, IsActiveSchedule = template.IsActiveSchedule, LastRun = template.LastRun, NextRun = nextRun, Index = template.Index, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator }; await SendTemplateUpdateMessage(updateRequest); } } // 10. Создание новых шаблонов foreach (var unitId in unitIdsToCreateFresh) { logger.LogInformation("Создание нового шаблона для UnitId {UnitId}.", unitId); var mqRequest = new TemplateGeneratorWorkerMq { JobId = jobId, UnitId = unitId, HistoryInitiator = initiator }; var msg = JsonSerializer.Serialize(mqRequest); var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg }); if (!result.IsSuccess) logger.LogError("Ошибка отправки команды создания шаблона для UnitId {UnitId}.", unitId); } logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId); } public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId); var existingTemplates = await templateService.Get() .Where(t => t.JobId == jobId) .ToListAsync(); if (!existingTemplates.Any()) return; var job = await GetJobWithGroupAndAutoControlAsync(jobId); if (job == null) return; var currentUnitIds = await GetExpectedUnitIdsAsync(jobId); logger.LogDebug("JobId {JobId}: {Count} UnitId по текущему фильтру.", jobId, currentUnitIds.Count); foreach (var template in existingTemplates) { logger.LogDebug("Обработка шаблона {TemplateId} (UnitId {UnitId}).", template.Id, template.UnitId); bool unitStillInFilter = currentUnitIds.Contains(template.UnitId); var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId); TemplateStatusTypeEnum targetStatus; bool targetIsActiveTemplate; bool targetIsActiveSchedule; if (unitStillInFilter) { targetStatus = TemplateStatusTypeEnum.Used; targetIsActiveTemplate = template.IsActiveTemplate; targetIsActiveSchedule = template.IsActiveSchedule; } else { targetStatus = TemplateStatusTypeEnum.Unused; targetIsActiveTemplate = DefaultUnusedTemplateState; targetIsActiveSchedule = DefaultUnusedScheduleState; logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.", template.Id, template.UnitId); } bool needsUpdate = template.StatusTypeId != TemplateStatusTypeEnum.Updating && ( template.StatusTypeId != targetStatus || template.Name != expectedName || template.IsActiveTemplate != targetIsActiveTemplate || template.IsActiveSchedule != targetIsActiveSchedule ); if (needsUpdate) { template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id); continue; } var nextRun = await GetNextRunAsync(job, template.NextRun); var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, JobId = jobId, UnitId = template.UnitId, Name = expectedName, IsActiveTemplate = targetIsActiveTemplate, IsActiveSchedule = targetIsActiveSchedule, LastRun = template.LastRun, NextRun = nextRun, Index = template.Index, StatusTypeId = targetStatus, Initiator = initiator }; await SendTemplateUpdateMessage(updateRequest); } } logger.LogInformation("Обновление шаблонов завершено для JobId {JobId}.", jobId); } private async Task SendTemplateUpdateMessage(TemplateUpdaterMq updateRequest) { logger.LogDebug("Отправка сообщения в очередь '{Queue}' для шаблона {TemplateId}", mqSettings.TemplateUpdater.QueueName, updateRequest.TemplateId); var msg = JsonSerializer.Serialize(updateRequest); var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new[] { msg }); if (result.IsSuccess) { logger.LogInformation("Отправлен запрос на обновление шаблона {TemplateId}", updateRequest.TemplateId); } else { logger.LogError("Ошибка при отправке запроса на обновление шаблона {TemplateId}: {Message}", updateRequest.TemplateId, msg); } } private async Task TryReuseOneUnusedTemplateAsync(Guid jobId, Guid unitId, HistoryInitiator initiator, int maxAttempts = 3) { for (int attempt = 1; attempt <= maxAttempts; attempt++) { try { var unusedCandidates = await templateService.Get() .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) .OrderBy(t => t.DateModified ?? t.DateCreated) .Take(UnusedCandidateBatchSize) .ToListAsync(); if (!unusedCandidates.Any()) { logger.LogDebug("Нет Unused-шаблонов (попытка {Attempt}).", attempt); return null; } foreach (var candidate in unusedCandidates) { var originalStatus = candidate.StatusTypeId; var originalModified = candidate.DateModified; try { candidate.StatusTypeId = TemplateStatusTypeEnum.Updating; candidate.DateModified = DateTimeOffset.UtcNow; if (await templateService.CommitAsync(initiator)) { logger.LogInformation("Успешно захвачен шаблон {TemplateId} для UnitId {UnitId} (попытка {Attempt}).", candidate.Id, unitId, attempt); return candidate; } // Откат при неудаче candidate.StatusTypeId = originalStatus; candidate.DateModified = originalModified; } catch (Exception ex) when ( ex is DbUpdateException || ex.InnerException?.Message.Contains("deadlock", StringComparison.OrdinalIgnoreCase) == true || ex.InnerException?.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) == true) { logger.LogWarning(ex, "Конфликт при захвате шаблона {TemplateId} (попытка {Attempt}).", candidate.Id, attempt); candidate.StatusTypeId = originalStatus; candidate.DateModified = originalModified; } } if (attempt < maxAttempts) await Task.Delay(Random.Shared.Next(5, 15) * attempt); } catch (Exception ex) { logger.LogError(ex, "Ошибка в попытке захвата (попытка {Attempt}).", attempt); if (attempt == maxAttempts) throw; } } return null; } private async Task GetJobWithGroupAndAutoControlAsync(Guid jobId) { return await jobService.Get() .Include(j => j.Group) .Include(j => j.AutoControl) .FirstOrDefaultAsync(j => j.Id == jobId); } private async Task> GetExpectedUnitIdsAsync(Guid jobId) { var units = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); return units?.ToHashSet() ?? new HashSet(); } private async Task GetNormalizedTemplateNameAsync(Job job, Guid unitId) { var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, job.Id); return rawName.ToUpper(); } private async Task GetNextRunAsync(Job job, DateTimeOffset? currentNextRun = null) { var now = DateTimeOffset.UtcNow; if (currentNextRun.HasValue && currentNextRun.Value > now) { return currentNextRun.Value; } var referenceDate = job.Group?.ReferenceDate ?? now; return await esppScheduleTransformService.GetNextDateAsync(job.GroupId, referenceDate); } } }