using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using PARR.AIHITRelationshipsSyncer.Models; using PARR.AIHITRelationshipsSyncer.Services.Interfaces; using PARR.Core.Repositories.Interfaces.Unit; using PARR.Domain.Entities.Unit; using PARR.Domain.Settings; namespace PARR.AIHITRelationshipsSyncer.Services.Implementations { internal class RelationshipsSyncService : IRelationshipsSyncService { private readonly ILogger logger; private readonly IUnitRepository unitService; private readonly SettingsFromDb _settingsFromDb; public RelationshipsSyncService( ILogger logger, IUnitRepository unitService, SettingsFromDb settingsFromDb ) { this.logger = logger; this.unitService = unitService; _settingsFromDb = settingsFromDb; } public async Task SyncAsync(List aihitdata) { // 1. Сначала фильтруем и нормализуем входящий мусор var validPairs = ValidateInput(aihitdata); // Если пришел пустой список, то и проверять порог нет смысла (лог уже записан внутри) if (!validPairs.Any()) return; // 2. FAIL-FAST: Быстро узнаем общее число связей в БД без выкачивания самих данных int previousCount = await unitService.Get() .SelectMany(u => u.ChildUnits) .CountAsync(); // 3. SAFEGUARD COMPLIANCE: Проверяем защитный порог падения данных if (previousCount > 0) { // 1. Считаем в decimal с абсолютной точностью decimal exactPercentage = ((decimal)validPairs.Count / previousCount) * 100; // 2. Округляем до 1 знака после запятой (например, 96.98% -> 97.0%) // Это защитит от ложных срабатываний из-за пары недостающих связей на больших объемах decimal currentPercentage = Math.Round(exactPercentage, 1, MidpointRounding.AwayFromZero); // 3. Строгое сравнение (<) гарантирует пропуск при ровно 97% и работу "0" как выключателя if (currentPercentage < _settingsFromDb.MinRelationshipsThresholdPct) { logger.LogWarning( "Синхронизация отменена: количество полученных связей ниже порогового значения! " + "Получено: {CurrentCount} ({CurrentPercentage:F1}%), ожидалось >= {Threshold}% от прошлого объема ({PreviousCount}).", validPairs.Count, currentPercentage, _settingsFromDb.MinRelationshipsThresholdPct, previousCount); return; } } // 4. HAPPY PATH: Если проверка пройдена, выполняем тяжелую работу var sourceUnitNames = ExtractUnitNames(validPairs); var existingUnits = await GetOrCreateUnitsAsync(sourceUnitNames); var (toRemove, toAdd) = await GetDeltaAsync(validPairs); await RemoveRelationshipsAsync(toRemove); await AddRelationshipsAsync(toAdd, existingUnits); await FinalCommitAsync(toRemove.Count, toAdd.Count); } private List<(string Parent, string Child)> ValidateInput(List data) { if (data == null || data.Count == 0) { logger.LogWarning("Получены пустые или null данные из АИХ ИТ. Пропускаю синхронизацию"); return null!; } var validPairs = data .Where(t => !string.IsNullOrWhiteSpace(t.ParentName) && !string.IsNullOrWhiteSpace(t.ChildName)) .Where(t => t.ParentName != t.ChildName) .Select(t => ( Parent: t.ParentName!.Trim().ToUpperInvariant(), Child: t.ChildName!.Trim().ToUpperInvariant() )) .Distinct() .ToList(); if (validPairs.Count == 0) { logger.LogWarning("После нормализации не осталось валидных связей. Пропускаю синхронизацию"); return new(); } logger.LogInformation("Получено {Count} валидных связей", validPairs.Count); return validPairs; } private static List ExtractUnitNames(List<(string Parent, string Child)> pairs) { return pairs .SelectMany(x => new[] { x.Parent, x.Child }) .Distinct() .ToList(); } private async Task> GetOrCreateUnitsAsync(List names) { var existing = await unitService.Get() .AsNoTracking() .ToDictionaryAsync(u => u.Name, StringComparer.Ordinal); var missing = names.Except(existing.Keys).ToList(); if (!missing.Any()) return existing; logger.LogInformation("Создание отсутствующих Unit:{Count}", missing.Count); foreach (var name in missing) { var unit = new Unit { Id = Guid.NewGuid(), Name = name, DateCreated = DateTimeOffset.UtcNow }; if (await unitService.CreateAsync(unit)) { logger.LogInformation("Создан Unit: {Name}", name); existing[name] = unit; } else logger.LogError("Не удалось создать Unit: {Name}", name); } await unitService.CommitAsync(); return existing; } private async Task<(List ToRemove, List<(string, string)> ToAdd)> GetDeltaAsync(List<(string Parent, string Child)> sourcePairs) { var allExistings = await unitService.Get() .AsNoTracking() .Include(u => u.ChildUnits).ThenInclude(r => r.ChildUnit) .Where(u => u.ChildUnits.Any()) .SelectMany(u => u.ChildUnits, (parent, relationship) => new ExistingRelationship { ParentName = parent.Name, ChildName = relationship.ChildUnit!.Name, Relationship = relationship } ) .ToListAsync(); var sourceSet = new HashSet<(string, string)>(sourcePairs); var existingSet = new HashSet<(string, string)>(allExistings.Select(x => (x.ParentName, x.ChildName))); var toRemove = allExistings.Where(r => !sourceSet.Contains((r.ParentName, r.ChildName))).ToList(); var toAdd = sourcePairs.Where(p => !existingSet.Contains(p)).ToList(); logger.LogInformation("Связей для удаления: {ToRemove}, для добавления: {ToAdd}", toRemove.Count, toAdd.Count); return (toRemove, toAdd); } private async Task RemoveRelationshipsAsync(List toRemove) { if (!toRemove.Any()) return; var parentNames = toRemove.Select(r => r.ParentName).Distinct().ToList(); var parents = await unitService.Get() .Include(u => u.ChildUnits) .Where(u => parentNames.Contains(u.Name)) .ToDictionaryAsync(u => u.Name, StringComparer.Ordinal); foreach (var rel in toRemove) { if (!parents.TryGetValue(rel.ParentName, out var parent)) { logger.LogWarning("Родитель '{Parent}' не найден при попытке удаления связи - '{Child}'", rel.ParentName, rel.ChildName); continue; } var relationshipToRemove = parent.ChildUnits .FirstOrDefault(r => r.ChildUnitId == rel.Relationship.ChildUnitId); if (relationshipToRemove != null) { logger.LogInformation("Удаление связи: '{Parent}' - '{Child}'", rel.ParentName, rel.ChildName); parent.ChildUnits.Remove(relationshipToRemove); } else { logger.LogWarning("Связь '{Parent}'-'{Child}' не найдена в коллекции ChildUnits для удаления", rel.ParentName, rel.ChildName); } } } private async Task AddRelationshipsAsync(List<(string Parent, string Child)> toAdd, Dictionary allUnits) { if (!toAdd.Any()) return; var parentNames = toAdd.Select(x => x.Parent).Distinct().ToList(); var parents = await unitService.Get() .Include(u => u.ChildUnits) .Where(u => parentNames.Contains(u.Name)) .ToDictionaryAsync(u => u.Name, StringComparer.Ordinal); foreach (var (Parent, Child) in toAdd) { if (!parents.TryGetValue(Parent, out var parent) || !allUnits.TryGetValue(Child, out var child)) { logger.LogWarning("Пропущена связь '{Parent}' - '{Child}': Unit не найден", Parent, Child); continue; } if (parent!.ChildUnits.Any(r => r.ChildUnitId == child.Id)) { logger.LogWarning("Связь уже существует: '{Parent}' - '{Child}'", Parent, Child); continue; } parent.ChildUnits.Add(new UnitInUnit { ParentUnitId = parent.Id, ChildUnitId = child.Id, DateCreated = DateTimeOffset.UtcNow }); logger.LogInformation("Добавлена связь: '{Parent}' - '{Child}'", Parent, Child); } } private async Task FinalCommitAsync(int removedCount, int addedCount) { if (removedCount > 0 || addedCount > 0) { if (await unitService.CommitAsync()) { logger.LogInformation("Синхронизация завершена. Удалено: {Removed}, добавлено: {Added}", removedCount, addedCount); } else logger.LogError("Не удалось сохранить изменения в БД"); } else logger.LogInformation("Изменений не обнаружено"); } private sealed class ExistingRelationship { public string ParentName { get; init; } public string ChildName { get; init; } public UnitInUnit Relationship { get; init; } } } }