using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Types; namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Services; /// /// Enforces template deletion constraints. /// Template deletion is blocked when: /// - Instances reference the template /// - Child templates reference it (as parent) /// - Other templates compose it /// Returns clear error messages listing the referencing entities. /// public class TemplateDeletionService { private readonly ITemplateEngineRepository _repository; /// /// Initializes a new with the given repository. /// /// Repository used to check deletion constraints and perform the delete. public TemplateDeletionService(ITemplateEngineRepository repository) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); } /// /// Checks whether a template can be safely deleted and returns any blocking reasons. /// /// The id of the template to check. /// Cancellation token for the operation. /// A task that resolves to a successful result if the template can be deleted, or a failure with blocking reasons. public async Task> CanDeleteTemplateAsync(int templateId, CancellationToken cancellationToken = default) { var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken); if (template == null) return Result.Failure($"Template with ID {templateId} not found."); if (template.IsDerived) return Result.Failure( $"Cannot delete template '{template.Name}': it is a derived template. " + "Remove the owning composition on its parent template instead."); var errors = new List(); // Check 1: Instances reference this template var instances = await _repository.GetInstancesByTemplateIdAsync(templateId, cancellationToken); if (instances.Count > 0) { var names = string.Join(", ", instances.Select(i => i.UniqueName).Take(10)); errors.Add($"Cannot delete template '{template.Name}': {instances.Count} instance(s) reference it ({names}{(instances.Count > 10 ? "..." : "")})."); } // Check 2: Child templates reference it as parent. Split derived vs. regular. // Reads ids, names, parent/derived/owner edges and composition rows only — the // no-tracking, script-body-free analysis projection carries all of them // (review 2026-10-21 04/P6, 08/P1). var allTemplates = await _repository.GetAllTemplatesForAnalysisAsync(cancellationToken); var inheritors = allTemplates.Where(t => t.ParentTemplateId == templateId).ToList(); var regularChildren = inheritors.Where(t => !t.IsDerived).ToList(); var derivatives = inheritors.Where(t => t.IsDerived).ToList(); if (regularChildren.Count > 0) { var names = string.Join(", ", regularChildren.Select(t => t.Name).Take(10)); errors.Add($"Cannot delete template '{template.Name}': {regularChildren.Count} child template(s) inherit from it ({names}{(regularChildren.Count > 10 ? "..." : "")})."); } if (derivatives.Count > 0) { var compIds = derivatives.Select(d => d.OwnerCompositionId).Where(id => id.HasValue).Select(id => id!.Value).ToHashSet(); var ownerLookup = allTemplates .SelectMany(t => t.Compositions.Select(c => new { Owner = t, Composition = c })) .Where(x => compIds.Contains(x.Composition.Id)) .ToDictionary(x => x.Composition.Id, x => $"'{x.Owner.Name}' (as '{x.Composition.InstanceName}')"); var details = string.Join(", ", derivatives.Take(10).Select(d => d.OwnerCompositionId.HasValue && ownerLookup.TryGetValue(d.OwnerCompositionId.Value, out var label) ? label : $"'{d.Name}'")); errors.Add($"Cannot delete template '{template.Name}': it is the base of {derivatives.Count} derived template(s) used in {details}{(derivatives.Count > 10 ? "..." : "")}. Remove those compositions first."); } // Check 3: Other templates compose it directly. // Read the Compositions navigation already loaded by GetAllTemplatesForAnalysisAsync // rather than issuing one GetCompositionsByTemplateIdAsync round-trip per // template — this is the same source TemplateService // .DeleteTemplateAsync uses for the equivalent check. var composingTemplates = allTemplates .SelectMany(t => t.Compositions .Where(comp => comp.ComposedTemplateId == templateId) .Select(comp => (TemplateName: t.Name, comp.InstanceName))) .ToList(); if (composingTemplates.Count > 0) { var details = string.Join(", ", composingTemplates.Take(10).Select(c => $"'{c.TemplateName}' (as '{c.InstanceName}')")); errors.Add($"Cannot delete template '{template.Name}': {composingTemplates.Count} template(s) compose it ({details}{(composingTemplates.Count > 10 ? "..." : "")})."); } if (errors.Count > 0) return Result.Failure(string.Join(" ", errors)); return Result.Success(true); } /// /// Deletes a template after checking all constraints. /// /// The id of the template to delete. /// Cancellation token for the operation. /// A task that resolves to a successful result when deleted, or a failure with the blocking reason. public async Task> DeleteTemplateAsync(int templateId, CancellationToken cancellationToken = default) { var canDelete = await CanDeleteTemplateAsync(templateId, cancellationToken); if (canDelete.IsFailure) return canDelete; await _repository.DeleteTemplateAsync(templateId, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); return Result.Success(true); } }