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.Contracts; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.DomainServices.Shortcodes; using PARR.DAL.DomainServices.UnitFilterService; using PARR.DAL.Models; using PARR.DAL.Models.Job; using PARR.DAL.NextRunServices; 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; // Маска для неиспользуемых шаблонов — хардкод, недоступна из GUI private const string UnusedTemplateNameMask = "%П-1%_НЕАКТУАЛЬНЫЕ_%ЭК%_%ТИКТАК%"; 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; private readonly SettingsFromDb settingsFromDb; private readonly INextRunService nextRunService; 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, SettingsFromDb settingsFromDb, INextRunService nextRunService ) { 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; this.settingsFromDb = settingsFromDb; this.nextRunService = nextRunService; } public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator) { // === Специальная обработка для Job неиспользуемых шаблонов === if (jobId == settingsFromDb.JobIdForUnusedTemplates) { logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов {JobId}", jobId); await SyncUnusedTemplatesAsync(jobId, initiator); return; } // === Обычная логика для всех остальных Job === 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 filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(jobId); if (filteredUnits == null || !filteredUnits.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; } // Извлекаем ID юнитов для последующих операций var unitIds = filteredUnits.Select(u => u.Id).ToList(); #if DEBUG // Отладка: проверить, есть ли юнит в unitIds 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(); 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); // === Создаём временный Template для нормализации имени === var tempTemplateForName = new Template { Id = reusableTemplate.Id, Name = reusableTemplate.Name, JobId = jobId, UnitId = unitId, Index = reusableTemplate.Index, Job = job, // загруженный job Unit = reusableTemplate.Unit, // может быть null — нормально UnitsInTemplate = new List() // для простого шаблона }; var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); //var nextRun = await GetNextRunAsync(job); var nextRun = await nextRunService.GetNextRunForTemplateAsync(reusableTemplate.Id, true); 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); } } // === Обработка существующих шаблонов (проверка имени) === 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} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName); //var nextRun = await GetNextRunAsync(job, template.NextRun); var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false); 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); } } } // === Деактивация лишних шаблонов === foreach (var unusedTemplate in unusedTemplates) { logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId); await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator); } // === Успешное завершение === 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 filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(jobId); if (filteredUnits == null || !filteredUnits.Any()) { logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); await UpdateMatchingStatusAsync(jobId, "Нет Unit'ов — обновление не требуется"); await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); return; } // Извлекаем ID юнитов для последующих операций var unitIds = filteredUnits.Select(u => u.Id).ToList(); #if DEBUG // Отладка: проверить, есть ли юнит в unitIds 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(); 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} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName); //var nextRun = await GetNextRunAsync(job, template.NextRun); var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false); 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); } } } 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 SyncUnusedTemplatesAsync(Guid unusedJobId, HistoryInitiator initiator) { // === Проверка: уже запущена? === var existingStatus = await matchingStatusService.GetStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job); if (existingStatus.DetailsJobs?.Any() == true) { logger.LogWarning("Синхронизация для Job неиспользуемых шаблонов {JobId} уже запущена. Пропускаем.", unusedJobId); return; } // === Устанавливаем статус "в процессе" === var initialStatus = new MatchingStatusItemDto { DateStart = DateTimeOffset.UtcNow, Action = TemplateMatcherActionEnum.Sync, Comment = "Синхронизация неиспользуемых шаблонов" }; await matchingStatusService.SetMatchingStatusAsync( unusedJobId, SyncTaskEntityTypeEnum.Job, new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) }, TimeSpan.FromMinutes(30) ); try { // Загружаем Job неиспользуемых шаблонов с Group var unusedJob = await jobService.Get() .AsNoTracking() .Include(j => j.Group) .FirstOrDefaultAsync(j => j.Id == unusedJobId); if (unusedJob == null) { logger.LogError("Job неиспользуемых шаблонов {JobId} не найден.", unusedJobId); await UpdateMatchingStatusAsync(unusedJobId, "Job не найден"); return; } // Находим все шаблоны со статусом Unused var unusedTemplates = await templateService.Get() .Include(t => t.Unit) .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) .ToListAsync(); if (!unusedTemplates.Any()) { logger.LogInformation("Не найдено шаблонов со статусом Unused."); await UpdateMatchingStatusAsync(unusedJobId, "Нет шаблонов для обработки"); await matchingStatusService.DeleteMatchingStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job); return; } await UpdateMatchingStatusAsync(unusedJobId, $"Найдено {unusedTemplates.Count} шаблонов для обработки"); int processed = 0; foreach (var template in unusedTemplates) { // Генерируем ожидаемое имя один раз var expectedName = await GenerateUnusedTemplateNameAsync(template, unusedJob); // Проверяем, нужно ли обновление bool needsUpdate = template.JobId != unusedJobId || !string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase); if (!needsUpdate) { logger.LogDebug("Шаблон {TemplateId} уже имеет актуальное имя и JobId. Пропускаем.", template.Id); continue; } //var nextRun = await GetNextRunAsync(unusedJob); var nextRun = await nextRunService.GetNextRunForTemplateAsync(template.Id, false); var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, JobId = unusedJobId, UnitId = template.UnitId, Name = expectedName, IsActiveTemplate = false, IsActiveSchedule = false, LastRun = template.LastRun, NextRun = nextRun, Index = null, StatusTypeId = TemplateStatusTypeEnum.Unused, Initiator = initiator, UnitsInTemplate = new List() }; await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest); processed++; await UpdateMatchingStatusAsync(unusedJobId, $"Обработано: {processed}/{unusedTemplates.Count}"); } await UpdateMatchingStatusAsync(unusedJobId, "Синхронизация неиспользуемых шаблонов завершена"); await matchingStatusService.DeleteMatchingStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job); logger.LogInformation("Синхронизация неиспользуемых шаблонов завершена. Обработано {Count} шаблонов.", unusedTemplates.Count); } catch (Exception ex) { logger.LogError(ex, "Ошибка при синхронизации неиспользуемых шаблонов для Job {JobId}", unusedJobId); await UpdateMatchingStatusAsync(unusedJobId, $"Ошибка: {ex.Message}"); throw; } } private async Task GenerateUnusedTemplateNameAsync(Template template, Job unusedJob) { var tempJob = new Job { Id = unusedJob.Id, Name = unusedJob.Name, WorkName = unusedJob.WorkName, MinValueRelationships = unusedJob.MinValueRelationships, MaxValueRelationships = unusedJob.MaxValueRelationships, IsParentRelationships = unusedJob.IsParentRelationships, TemplateNameMask = UnusedTemplateNameMask, WorkGroupMask = unusedJob.WorkGroupMask, ResponseAreaMask = unusedJob.ResponseAreaMask, TnkId = unusedJob.TnkId, GroupId = unusedJob.GroupId, Group = unusedJob.Group, Tnk = unusedJob.Tnk, UnitFilters = unusedJob.UnitFilters, Templates = unusedJob.Templates, AutoControl = unusedJob.AutoControl }; var tempTemplateForName = new Template { Id = template.Id, Name = template.Name, JobId = unusedJob.Id, UnitId = template.UnitId, Index = null, Job = tempJob, Unit = template.Unit, UnitsInTemplate = new List() }; return await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName); } 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) ); } }