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.Cache.Models; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.Models; using PARR.DAL.Models.Job; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Unit; using PARR.DAL.TransformServices; using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Settings; using System.Text.Json; namespace PARR.TemplateMatcher.Services.Implementations; internal class SimpleTemplateSynchronizer : ITemplateSynchronizer { #if DEBUG private readonly Guid targetUnitId = Guid.Parse("358437ac-1eeb-4c00-840c-998326f657ac"); #endif 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 IEsppScheduleTransformService esppScheduleTransformService; private readonly ITemplateDeactivator templateDeactivator; private readonly ITemplateNameNormalizer templateNameNormalizer; private readonly ITemplateUpdaterMqSender templateUpdaterMqSender; private readonly IMatchingStatusService matchingStatusService; public SimpleTemplateSynchronizer( ILogger logger, IUnitFilterService unitFilterService, IUnitInUnitService unitInUnitService, IUnitInValueService unitInValueService, IUnitService unitService, MqSettings mqSettings, IMqService mqService, ITemplateService templateService, IJobService jobService, ITemplateReuser templateReuser, IShortcodesService shortcodesService, IEsppScheduleTransformService esppScheduleTransformService, IUnitRegionalEkPtkGroupService regionalEkPtkGroupService, IUnitFieldService unitFieldService, ITemplateDeactivator templateDeactivator, ITemplateNameNormalizer templateNameNormalizer, ITemplateUpdaterMqSender templateUpdaterMqSender, IMatchingStatusService matchingStatusService ) { this.logger = logger; this.unitFilterService = unitFilterService; this.mqSettings = mqSettings; this.mqService = mqService; this.templateService = templateService; this.jobService = jobService; this.templateReuser = templateReuser; this.esppScheduleTransformService = esppScheduleTransformService; this.templateDeactivator = templateDeactivator; this.templateNameNormalizer = templateNameNormalizer; this.templateUpdaterMqSender = templateUpdaterMqSender; this.matchingStatusService = matchingStatusService; } public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало синхронизации шаблонов для Job {JobId}", jobId); // === Проверка: уже запущена? === var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); if (existingStatus.DetailsJobs?.Any() == true) { logger.LogWarning("Синхронизация для Job {JobId} уже запущена. Пропускаем.", jobId); return; } // === Устанавливаем статус "в процессе" === var initialStatus = new MatchingStatusItemDto { DateStart = DateTimeOffset.UtcNow, Action = TemplateMatcherActionEnum.Sync, Comment = "Начало синхронизации" }; await matchingStatusService.SetMatchingStatusAsync( jobId, SyncTaskEntityTypeEnum.Job, new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) }, TimeSpan.FromMinutes(35) ); try { var job = await jobService.Get() .AsNoTracking() .Include(j => j.AutoControl) .Include(j => j.Tnk) .Include(j => j.Group) .ThenInclude(g => g!.GroupType) .Include(j => j.UnitFilters) .ThenInclude(uf => uf.RelationshipFilters) .FirstOrDefaultAsync(j => j.Id == jobId); if (job == null) { logger.LogWarning("Job {JobId} не найден.", jobId); await UpdateMatchingStatusAsync(jobId, "Job не найден"); return; } var unitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); { if (unitIds == null || !unitIds.Any()) { logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); var existingTemplatesForDeactivation = await templateService.Get() .AsNoTracking() .Include(t => t.UnitsInTemplate) .Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used) .ToListAsync(); await UpdateMatchingStatusAsync(jobId, $"Нет Unit'ов. Деактивация {existingTemplatesForDeactivation.Count} шаблонов..."); foreach (var unusedTemplate in existingTemplatesForDeactivation) { logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId); await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator); } await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена: нет Unit'ов"); await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); return; } } #if DEBUG if (unitIds.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId); } else { logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId); } #endif var existingTemplates = await templateService.Get() .Include(t => t.UnitsInTemplate) .Include(t => t.Job) .ThenInclude(t => t!.Group) .ThenInclude(t => t.GroupType) .Include(t => t.Job) .ThenInclude(t => t!.Tnk) .Include(t => t.Unit) .Where(t => t.JobId == jobId) .ToListAsync(); var existingUsedTemplates = existingTemplates .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used) .ToList(); var existingUnitIds = existingUsedTemplates .Select(t => t.UnitId) .ToHashSet(); var newUnitIds = unitIds.Except(existingUnitIds).ToList(); var unusedTemplates = existingUsedTemplates .Where(t => !unitIds.Contains(t.UnitId)) .ToList(); // === Подсчёт операций === int toCreate = newUnitIds.Count; int toUpdate = 0; int toDeactivate = unusedTemplates.Count; // Подсчёт обновлений по имени foreach (var template in existingUsedTemplates) { if (unitIds.Contains(template.UnitId)) { var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) toUpdate++; } } await UpdateMatchingStatusAsync(jobId, $"Осталось: создать={toCreate}, обновить={toUpdate}, деактивировать={toDeactivate}"); // === Обработка новых юнитов === int created = 0, updated = 0, deactivated = 0; foreach (var unitId in newUnitIds) { var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator); if (reusableTemplate != null) { logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, UnitId {UnitId}.", reusableTemplate.Id, jobId, unitId); var tempTemplateForName = new Template { Id = reusableTemplate.Id, Name = reusableTemplate.Name, JobId = jobId, UnitId = unitId, Index = reusableTemplate.Index, Job = job, Unit = reusableTemplate.Unit, UnitsInTemplate = new List() }; var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); var nextRun = await GetNextRunAsync(job); var updateRequest = new TemplateUpdaterMq { TemplateId = reusableTemplate.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 templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); } else { logger.LogDebug("Создание нового шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId); await CreateSimpleTemplateAsync(jobId, unitId, initiator); } created++; await UpdateMatchingStatusAsync(jobId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}"); } // === Обработка существующих шаблонов (проверка имени) === foreach (var template in existingUsedTemplates) { if (unitIds.Contains(template.UnitId)) { var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) { logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", template.Id); 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, UnitsInTemplate = new List() }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); updated++; await UpdateMatchingStatusAsync(jobId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}"); } } } // === Деактивация лишних шаблонов === foreach (var unusedTemplate in unusedTemplates) { logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId); await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator); deactivated++; await UpdateMatchingStatusAsync(jobId, $"Прогресс: создано={created}, обновлено={updated}, деактивировано={deactivated}. Осталось: создать={toCreate - created}, обновить={toUpdate - updated}, деактивировать={toDeactivate - deactivated}"); } // === Успешное завершение === await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно"); await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); } catch (Exception ex) { logger.LogError(ex, "Ошибка при синхронизации Job {JobId}", jobId); await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}"); throw; } } public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) { logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId); } public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId); // === Проверка: уже запущена? === var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); if (existingStatus.DetailsJobs?.Any() == true) { logger.LogWarning("Обновление для Job {JobId} уже запущено. Пропускаем.", jobId); return; } var initialStatus = new MatchingStatusItemDto { DateStart = DateTimeOffset.UtcNow, Action = TemplateMatcherActionEnum.Update, Comment = "Начало обновления имён" }; await matchingStatusService.SetMatchingStatusAsync( jobId, SyncTaskEntityTypeEnum.Job, new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) }, TimeSpan.FromMinutes(30) ); try { var job = await jobService.Get() .AsNoTracking() .Include(j => j.AutoControl) .Include(j => j.Tnk) .Include(j => j.Group) .ThenInclude(g => g!.GroupType) .Include(j => j.UnitFilters) .ThenInclude(uf => uf.RelationshipFilters) .FirstOrDefaultAsync(j => j.Id == jobId); if (job == null) { logger.LogWarning("Job {JobId} не найден.", jobId); await UpdateMatchingStatusAsync(jobId, "Job не найден"); return; } var unitIds = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); if (unitIds == null || !unitIds.Any()) { logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); await UpdateMatchingStatusAsync(jobId, "Нет Unit'ов — обновление не требуется"); await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); return; } #if DEBUG if (unitIds.Contains(targetUnitId)) { logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId); } else { logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId); } #endif var existingTemplates = await templateService.Get() .AsNoTracking() .Include(t => t.Unit) .Include(t => t.UnitsInTemplate) .Include(t => t.Job) .ThenInclude(t => t!.Group) .ThenInclude(t => t!.GroupType) .Include(t => t.Job) .ThenInclude(t => t!.Tnk) .Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used) .ToListAsync(); int toUpdate = 0; foreach (var template in existingTemplates) { if (unitIds.Contains(template.UnitId)) { var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) toUpdate++; } } await UpdateMatchingStatusAsync(jobId, $"Обновление имён: {toUpdate} шаблонов"); int updated = 0; foreach (var template in existingTemplates) { if (unitIds.Contains(template.UnitId)) { var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) { logger.LogDebug("Шаблон {TemplateId} требует обновления имени.", template.Id); 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, UnitsInTemplate = new List() }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); updated++; await UpdateMatchingStatusAsync(jobId, $"Обновлено: {updated}/{toUpdate}"); } } } await UpdateMatchingStatusAsync(jobId, "Обновление завершено"); await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); logger.LogInformation("Обновление шаблонов завершено для Job {JobId}.", jobId); } catch (Exception ex) { logger.LogError(ex, "Ошибка при обновлении Job {JobId}", jobId); await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}"); throw; } } private async Task CreateSimpleTemplateAsync(Guid jobId, Guid unitId, HistoryInitiator initiator) { logger.LogInformation("Создание нового простого шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId); var mqRequest = new TemplateGeneratorMq { JobId = jobId, UnitId = unitId, UnitsInTemplate = new List(), HistoryInitiator = initiator }; var msg = JsonSerializer.Serialize(mqRequest); var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg }); if (!result.IsSuccess) logger.LogError("Ошибка отправки команды создания простого шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId); } private async Task GetNextRunAsync(Job job, DateTimeOffset? currentNextRun = null) { var referenceDate = job.Group?.ReferenceDate ?? DateTimeOffset.UtcNow; return await esppScheduleTransformService.GetNextDateAsync(job.GroupId, referenceDate); } private async Task UpdateMatchingStatusAsync(Guid jobId, string comment) { var status = new MatchingStatusItemDto { DateStart = DateTimeOffset.UtcNow, Action = TemplateMatcherActionEnum.Sync, Comment = comment }; await matchingStatusService.SetMatchingStatusAsync( jobId, SyncTaskEntityTypeEnum.Job, new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) }, TimeSpan.FromMinutes(30) ); } }