using ScadaLink.Commons.Entities.Templates; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Interfaces.Services; using ScadaLink.Commons.Types; namespace ScadaLink.TemplateEngine.Services; public class TemplateFolderService { private readonly ITemplateEngineRepository _repository; private readonly IAuditService _auditService; public TemplateFolderService(ITemplateEngineRepository repository, IAuditService auditService) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _auditService = auditService ?? throw new ArgumentNullException(nameof(auditService)); } public async Task> CreateFolderAsync( string name, int? parentFolderId, string user, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(name)) return Result.Failure("Folder name is required."); if (parentFolderId.HasValue) { var parent = await _repository.GetFolderByIdAsync(parentFolderId.Value, cancellationToken); if (parent == null) return Result.Failure($"Parent folder with ID {parentFolderId.Value} not found."); } var all = await _repository.GetAllFoldersAsync(cancellationToken); if (all.Any(f => f.ParentFolderId == parentFolderId && string.Equals(f.Name, name, StringComparison.OrdinalIgnoreCase))) return Result.Failure($"A folder named '{name}' already exists at this level."); var folder = new TemplateFolder(name) { ParentFolderId = parentFolderId }; await _repository.AddFolderAsync(folder, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); await _auditService.LogAsync(user, "Create", "TemplateFolder", folder.Id.ToString(), name, folder, cancellationToken); return Result.Success(folder); } public async Task> RenameFolderAsync( int folderId, string newName, string user, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(newName)) return Result.Failure("Folder name is required."); var folder = await _repository.GetFolderByIdAsync(folderId, cancellationToken); if (folder == null) return Result.Failure($"Folder with ID {folderId} not found."); var all = await _repository.GetAllFoldersAsync(cancellationToken); if (all.Any(f => f.Id != folderId && f.ParentFolderId == folder.ParentFolderId && string.Equals(f.Name, newName, StringComparison.OrdinalIgnoreCase))) return Result.Failure($"A folder named '{newName}' already exists at this level."); folder.Name = newName; await _repository.UpdateFolderAsync(folder, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); await _auditService.LogAsync(user, "Update", "TemplateFolder", folder.Id.ToString(), newName, folder, cancellationToken); return Result.Success(folder); } public async Task> MoveFolderAsync( int folderId, int? newParentId, string user, CancellationToken cancellationToken = default) { var folder = await _repository.GetFolderByIdAsync(folderId, cancellationToken); if (folder == null) return Result.Failure($"Folder with ID {folderId} not found."); if (newParentId.HasValue) { if (newParentId.Value == folderId) return Result.Failure("Cannot move a folder into itself (cycle)."); var newParent = await _repository.GetFolderByIdAsync(newParentId.Value, cancellationToken); if (newParent == null) return Result.Failure($"Target folder with ID {newParentId.Value} not found."); var all = await _repository.GetAllFoldersAsync(cancellationToken); var byId = all.ToDictionary(f => f.Id); var cursor = newParentId; // Walk up from newParentId — if we encounter folderId, the move would create a cycle. // Bound iterations by byId.Count to defensively terminate on a malformed graph. var iterations = 0; while (cursor.HasValue) { if (cursor.Value == folderId) return Result.Failure("Cannot move a folder under one of its descendants (cycle)."); if (++iterations > byId.Count) return Result.Failure("Folder hierarchy contains a cycle; cannot determine ancestry."); cursor = byId.TryGetValue(cursor.Value, out var node) ? node.ParentFolderId : null; } // Sibling-name uniqueness in destination. if (all.Any(f => f.Id != folderId && f.ParentFolderId == newParentId && string.Equals(f.Name, folder.Name, StringComparison.OrdinalIgnoreCase))) return Result.Failure($"A folder named '{folder.Name}' already exists in the target folder."); } else { var all = await _repository.GetAllFoldersAsync(cancellationToken); if (all.Any(f => f.Id != folderId && f.ParentFolderId == null && string.Equals(f.Name, folder.Name, StringComparison.OrdinalIgnoreCase))) return Result.Failure($"A folder named '{folder.Name}' already exists at the root."); } folder.ParentFolderId = newParentId; await _repository.UpdateFolderAsync(folder, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); await _auditService.LogAsync(user, "Move", "TemplateFolder", folder.Id.ToString(), folder.Name, folder, cancellationToken); return Result.Success(folder); } public async Task> DeleteFolderAsync( int folderId, string user, CancellationToken cancellationToken = default) { var folder = await _repository.GetFolderByIdAsync(folderId, cancellationToken); if (folder == null) return Result.Failure($"Folder with ID {folderId} not found."); var allFolders = await _repository.GetAllFoldersAsync(cancellationToken); var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken); var childFolderCount = allFolders.Count(f => f.ParentFolderId == folderId); var childTemplateCount = allTemplates.Count(t => t.FolderId == folderId); if (childFolderCount > 0 || childTemplateCount > 0) { var parts = new List(); if (childTemplateCount > 0) parts.Add($"{childTemplateCount} template{(childTemplateCount == 1 ? "" : "s")}"); if (childFolderCount > 0) parts.Add($"{childFolderCount} subfolder{(childFolderCount == 1 ? "" : "s")}"); return Result.Failure( $"Cannot delete folder '{folder.Name}': it contains {string.Join(" and ", parts)}. " + "Move or delete contents first."); } await _repository.DeleteFolderAsync(folderId, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); await _auditService.LogAsync(user, "Delete", "TemplateFolder", folderId.ToString(), folder.Name, null, cancellationToken); return Result.Success(true); } }