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.Contracts; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.DomainServices.Shortcodes.Models; 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.Services.Interfaces; using PARR.TemplateMatcher.Settings; using System.Text.Json; namespace PARR.TemplateMatcher.Services.Implemetaions; internal class SimpleTemplateSynchronizer : ITemplateSynchronizer { 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 ITemplateReuser templateReuser; private readonly IShortcodesService shortcodesService; private readonly IEsppScheduleTransformService esppScheduleTransformService; public SimpleTemplateSynchronizer( ILogger logger, IUnitFilterService unitFilterService, MqSettings mqSettings, IMqService mqService, ITemplateService templateService, IJobService jobService, ITemplateReuser templateReuser, IShortcodesService shortcodesService, IEsppScheduleTransformService esppScheduleTransformService) { this.logger = logger; this.unitFilterService = unitFilterService; this.mqSettings = mqSettings; this.mqService = mqService; this.templateService = templateService; this.jobService = jobService; this.templateReuser = templateReuser; this.shortcodesService = shortcodesService; this.esppScheduleTransformService = esppScheduleTransformService; } public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) { logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroupId {JobGroupId}. Это не поддерживаемая операция.", jobGroupId); // Не делаем ничего return; } public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId); var expectedUnitIds = await GetExpectedUnitIdsAsync(jobId) ?? new HashSet(); logger.LogDebug("JobId {JobId}: найдено {Count} UnitId по фильтрам.", jobId, expectedUnitIds.Count); var job = await GetJobWithGroupAndAutoControlAsync(jobId); if (job == null) { logger.LogError("Job с Id {JobId} не найден.", jobId); return; } // Проверяем, является ли Job "групповым" bool isGroupJob = job.Group != null && job.Group.GroupType?.Code == JobGroupTypesEnum.Group; if (isGroupJob && job.Group!.GroupingUnitFieldId.HasValue) { logger.LogInformation("Job {JobId} является групповым. Используйте SyncTemplatesForJobGroup для синхронизации.", jobId); return; // Ничего не делаем для группового Job } var existingTemplates = await templateService.Get() .AsNoTracking() .Where(t => t.JobId == jobId) .ToListAsync(); logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.", jobId, expectedUnitIds.Count, existingTemplates.Count); // Обработка случая: фильтр вернул 0 UnitId → деактивировать ВСЕ шаблоны if (!expectedUnitIds.Any()) { if (existingTemplates.Any()) { logger.LogInformation("Для JobId {JobId} фильтры не дали Unit'ов — будет деактивировано {Count} шаблонов.", jobId, existingTemplates.Count); foreach (var template in existingTemplates) { await DeactivateTemplateAsync(template, jobId, initiator); } } else { logger.LogInformation("Для JobId {JobId} нет Unit'ов по фильтрам и нет существующих шаблонов — синхронизация завершена.", jobId); } logger.LogInformation("Синхронизация завершена для JobId {JobId} (фильтр пуст).", jobId); return; } // Деактивация шаблонов, которые вышли из фильтра var templatesToDeactivate = existingTemplates .Where(t => !expectedUnitIds.Contains(t.UnitId)) .ToList(); foreach (var template in templatesToDeactivate) { await DeactivateTemplateAsync(template, jobId, initiator); } // Перечитываем шаблоны после деактивации existingTemplates = await templateService.Get() .AsNoTracking() .Include(t=>t.Job) .ThenInclude(t=>t.Tnk) .Where(t => t.JobId == jobId) .ToListAsync(); var unitToTemplate = existingTemplates.ToDictionary(t => t.UnitId, t => t); // UnitId без шаблона → попытка переиспользования или создание var unitIdsMissingTemplates = expectedUnitIds .Where(unitId => !unitToTemplate.ContainsKey(unitId)) .ToList(); var unitIdsToCreateFresh = new List(); foreach (var unitId in unitIdsMissingTemplates) { var reused = await templateReuser.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, UnitsInTemplate = new List() // Для простого шаблона }; await SendTemplateUpdateMessage(updateRequest); } else { logger.LogInformation("Нет доступных Unused-шаблонов для UnitId {UnitId} → создадим новый.", unitId); unitIdsToCreateFresh.Add(unitId); } } // Обновление/реактивация шаблонов, оставшихся в фильтре var templatesInFilter = existingTemplates .Where(t => expectedUnitIds.Contains(t.UnitId)) .ToList(); foreach (var template in templatesInFilter) { var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId); await ReactivateOrRenameTemplateAsync(template, job, expectedName, initiator); } // Создание новых шаблонов foreach (var unitId in unitIdsToCreateFresh) { await SendTemplateGeneratorMessageAsync(jobId, unitId, initiator); } logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId); } public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId); var existingTemplates = await templateService.Get() .AsNoTracking() .Where(t => t.JobId == jobId) .ToListAsync(); if (!existingTemplates.Any()) return; var job = await GetJobWithGroupAndAutoControlAsync(jobId); if (job == null) return; var currentUnitIds = await GetExpectedUnitIdsAsync(jobId) ?? new HashSet(); 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; expectedName = GetTemplateNameForUnused(expectedName); logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.", template.Id, template.UnitId); } bool needsUpdate = template.StatusTypeId != TemplateStatusTypeEnum.Updating && ( // 1. Статус изменился (например, Used → Unused или Unused → Used) template.StatusTypeId != targetStatus || // 2. Для Used — имя должно соответствовать шаблону (targetStatus == TemplateStatusTypeEnum.Used && template.Name != expectedName) || // 3. Флаги изменились (редко, но возможно через AutoControl изменение) template.IsActiveTemplate != targetIsActiveTemplate || template.IsActiveSchedule != targetIsActiveSchedule ); if (!needsUpdate) continue; 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, UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList() // Для простого шаблона это список из одного элемента или пустой }; await SendTemplateUpdateMessage(updateRequest); } logger.LogInformation("Обновление шаблонов завершено для JobId {JobId}.", jobId); } // --- Вспомогательные методы --- private async Task DeactivateTemplateAsync( Template template, Guid jobId, HistoryInitiator initiator) { if (template.StatusTypeId == TemplateStatusTypeEnum.Updating) return true; // уже в обработке logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.", template.Id, template.UnitId); template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id); return false; } var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, JobId = jobId, UnitId = template.UnitId, Name = GetTemplateNameForUnused(template.Name), IsActiveTemplate = DefaultUnusedTemplateState, IsActiveSchedule = DefaultUnusedScheduleState, LastRun = template.LastRun, NextRun = template.NextRun, Index = template.Index, StatusTypeId = TemplateStatusTypeEnum.Unused, Initiator = initiator, UnitsInTemplate = new List() // Для простого шаблона }; await SendTemplateUpdateMessage(updateRequest); return true; } private async Task ReactivateOrRenameTemplateAsync( Template template, Job job, string expectedName, HistoryInitiator initiator) { if (template.StatusTypeId == TemplateStatusTypeEnum.Updating) return true; bool needsUpdate = template.Name != expectedName || template.StatusTypeId != TemplateStatusTypeEnum.Used; if (!needsUpdate) return true; logger.LogInformation("Шаблон {TemplateId}: требуется обновление имени или реактивация.", template.Id); template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; if (!await templateService.CommitAsync(initiator)) { logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id); return false; } var nextRun = await GetNextRunAsync(job, template.NextRun); var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, JobId = job.Id, UnitId = template.UnitId, Name = expectedName, IsActiveTemplate = template.IsActiveTemplate, IsActiveSchedule = template.IsActiveSchedule, LastRun = template.LastRun, NextRun = nextRun, Index = template.Index, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator, UnitsInTemplate = template.UnitsInTemplate.Select(t => t.UnitId).ToList() // Для простого шаблона это список из одного элемента }; await SendTemplateUpdateMessage(updateRequest); return true; } private async Task SendTemplateGeneratorMessageAsync( Guid jobId, Guid unitId, HistoryInitiator initiator) { logger.LogInformation("Создание нового шаблона для UnitId {UnitId}.", unitId); var mqRequest = new TemplateGeneratorMq { JobId = jobId, UnitId = unitId, HistoryInitiator = initiator, UnitsInTemplate = new List() // Для простого шаблона }; var msg = JsonSerializer.Serialize(mqRequest); var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg }); if (!result.IsSuccess) logger.LogError("Ошибка отправки команды создания шаблона для UnitId {UnitId}.", unitId); return result.IsSuccess; } 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} в очередь '{Queue}'.", updateRequest.TemplateId, mqSettings.TemplateUpdater.QueueName); } } private string GetTemplateNameForUnused(string templateName) { return templateName + "_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds(); } private async Task GetJobWithGroupAndAutoControlAsync(Guid jobId) { return await jobService.Get() .AsNoTracking() .Include(j=>j.Tnk) .Include(j => j.Group) .ThenInclude(j => j.GroupType) .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) { // Создаём TemplateForShortcodes "на лету", без запроса к БД var templateForShortcodes = new TemplateForShortcodes { Id = Guid.Empty, // шаблон ещё не создан Index = null, JobId = job.Id, UnitId = unitId, Job = new JobForShortcodes { Group = job.Group == null ? null : new JobGroupForShortcodes { GroupingUnitFieldId = job.Group.GroupingUnitFieldId, GroupType = job.Group.GroupType == null ? null : new JobGroupTypeForShortcodes { Code = job.Group.GroupType.Code }, GroupName = job.Group.GroupName }, Tnk = job.Tnk == null ? null : new TnkForShortcodes { Name = job.Tnk.Name, ShortName = job.Tnk.ShortName ?? "" }, WorkName = job.WorkName, Name = job.Name }, UnitsInTemplate = new List() // для простого шаблона }; var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, templateForShortcodes); 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); } }