using ScadaLink.Commons.Entities.Instances; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Interfaces.Services; using ScadaLink.Commons.Types; using ScadaLink.Commons.Types.Enums; namespace ScadaLink.TemplateEngine.Services; /// /// Instance CRUD operations. /// - Create instance from template at site /// - Assign to area /// - Override non-locked attribute values /// - Cannot add or remove attributes (only override existing ones) /// - Per-attribute connection binding (bulk assignment support) /// - Enabled/disabled state with optimistic concurrency /// - Audit logging /// public class InstanceService { private readonly ITemplateEngineRepository _repository; private readonly IAuditService _auditService; public InstanceService(ITemplateEngineRepository repository, IAuditService auditService) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _auditService = auditService ?? throw new ArgumentNullException(nameof(auditService)); } /// /// Creates a new instance from a template at a site. /// public async Task> CreateInstanceAsync( string uniqueName, int templateId, int siteId, int? areaId, string user, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(uniqueName)) return Result.Failure("Instance unique name is required."); // Verify template exists var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken); if (template == null) return Result.Failure($"Template with ID {templateId} not found."); // Check for duplicate unique name var existing = await _repository.GetInstanceByUniqueNameAsync(uniqueName, cancellationToken); if (existing != null) return Result.Failure($"Instance with unique name '{uniqueName}' already exists."); // Verify area exists if specified if (areaId.HasValue) { var area = await _repository.GetAreaByIdAsync(areaId.Value, cancellationToken); if (area == null) return Result.Failure($"Area with ID {areaId.Value} not found."); if (area.SiteId != siteId) return Result.Failure($"Area with ID {areaId.Value} does not belong to site {siteId}."); } var instance = new Instance(uniqueName) { TemplateId = templateId, SiteId = siteId, AreaId = areaId, State = InstanceState.Disabled // New instances start disabled }; await _repository.AddInstanceAsync(instance, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); await _auditService.LogAsync(user, "Create", "Instance", instance.Id.ToString(), uniqueName, instance, cancellationToken); return Result.Success(instance); } /// /// Assigns an instance to an area. /// public async Task> AssignToAreaAsync( int instanceId, int? areaId, string user, CancellationToken cancellationToken = default) { var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken); if (instance == null) return Result.Failure($"Instance with ID {instanceId} not found."); if (areaId.HasValue) { var area = await _repository.GetAreaByIdAsync(areaId.Value, cancellationToken); if (area == null) return Result.Failure($"Area with ID {areaId.Value} not found."); if (area.SiteId != instance.SiteId) return Result.Failure($"Area with ID {areaId.Value} does not belong to the instance's site."); } instance.AreaId = areaId; await _repository.UpdateInstanceAsync(instance, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); await _auditService.LogAsync(user, "AssignArea", "Instance", instance.Id.ToString(), instance.UniqueName, instance, cancellationToken); return Result.Success(instance); } /// /// Sets an attribute override for an instance. Only non-locked attributes can be overridden. /// Cannot add or remove attributes — only override values of existing template attributes. /// public async Task> SetAttributeOverrideAsync( int instanceId, string attributeName, string? overrideValue, string user, CancellationToken cancellationToken = default) { var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken); if (instance == null) return Result.Failure($"Instance with ID {instanceId} not found."); // Verify attribute exists in the template and is not locked var templateAttrs = await _repository.GetAttributesByTemplateIdAsync(instance.TemplateId, cancellationToken); var templateAttr = templateAttrs.FirstOrDefault(a => a.Name == attributeName); if (templateAttr == null) return Result.Failure( $"Attribute '{attributeName}' does not exist in template {instance.TemplateId}. Cannot add new attributes via overrides."); if (templateAttr.IsLocked) return Result.Failure( $"Attribute '{attributeName}' is locked and cannot be overridden."); // Find existing override or create new one var overrides = await _repository.GetOverridesByInstanceIdAsync(instanceId, cancellationToken); var existingOverride = overrides.FirstOrDefault(o => o.AttributeName == attributeName); if (existingOverride != null) { existingOverride.OverrideValue = overrideValue; await _repository.UpdateInstanceAttributeOverrideAsync(existingOverride, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); await _auditService.LogAsync(user, "UpdateOverride", "InstanceAttributeOverride", existingOverride.Id.ToString(), attributeName, existingOverride, cancellationToken); return Result.Success(existingOverride); } else { var newOverride = new InstanceAttributeOverride(attributeName) { InstanceId = instanceId, OverrideValue = overrideValue }; await _repository.AddInstanceAttributeOverrideAsync(newOverride, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); await _auditService.LogAsync(user, "CreateOverride", "InstanceAttributeOverride", newOverride.Id.ToString(), attributeName, newOverride, cancellationToken); return Result.Success(newOverride); } } /// /// Sets connection bindings for an instance in bulk. /// public async Task>> SetConnectionBindingsAsync( int instanceId, IReadOnlyList<(string AttributeName, int DataConnectionId)> bindings, string user, CancellationToken cancellationToken = default) { var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken); if (instance == null) return Result>.Failure($"Instance with ID {instanceId} not found."); var existingBindings = await _repository.GetBindingsByInstanceIdAsync(instanceId, cancellationToken); var existingMap = existingBindings.ToDictionary(b => b.AttributeName, StringComparer.Ordinal); var results = new List(); foreach (var (attrName, connId) in bindings) { if (existingMap.TryGetValue(attrName, out var existing)) { existing.DataConnectionId = connId; await _repository.UpdateInstanceConnectionBindingAsync(existing, cancellationToken); results.Add(existing); } else { var binding = new InstanceConnectionBinding(attrName) { InstanceId = instanceId, DataConnectionId = connId }; await _repository.AddInstanceConnectionBindingAsync(binding, cancellationToken); results.Add(binding); } } await _repository.SaveChangesAsync(cancellationToken); await _auditService.LogAsync(user, "SetConnectionBindings", "Instance", instance.Id.ToString(), instance.UniqueName, bindings, cancellationToken); return Result>.Success(results); } /// /// Enables an instance. /// public async Task> EnableAsync(int instanceId, string user, CancellationToken cancellationToken = default) { var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken); if (instance == null) return Result.Failure($"Instance with ID {instanceId} not found."); instance.State = InstanceState.Enabled; await _repository.UpdateInstanceAsync(instance, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); await _auditService.LogAsync(user, "Enable", "Instance", instance.Id.ToString(), instance.UniqueName, instance, cancellationToken); return Result.Success(instance); } /// /// Disables an instance. /// public async Task> DisableAsync(int instanceId, string user, CancellationToken cancellationToken = default) { var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken); if (instance == null) return Result.Failure($"Instance with ID {instanceId} not found."); instance.State = InstanceState.Disabled; await _repository.UpdateInstanceAsync(instance, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); await _auditService.LogAsync(user, "Disable", "Instance", instance.Id.ToString(), instance.UniqueName, instance, cancellationToken); return Result.Success(instance); } /// /// Deletes an instance. /// public async Task> DeleteAsync(int instanceId, string user, CancellationToken cancellationToken = default) { var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken); if (instance == null) return Result.Failure($"Instance with ID {instanceId} not found."); await _repository.DeleteInstanceAsync(instanceId, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); await _auditService.LogAsync(user, "Delete", "Instance", instanceId.ToString(), instance.UniqueName, null, cancellationToken); return Result.Success(true); } /// /// Gets an instance by ID. /// public async Task GetByIdAsync(int instanceId, CancellationToken cancellationToken = default) => await _repository.GetInstanceByIdAsync(instanceId, cancellationToken); /// /// Gets all instances for a site. /// public async Task> GetBySiteIdAsync(int siteId, CancellationToken cancellationToken = default) => await _repository.GetInstancesBySiteIdAsync(siteId, cancellationToken); }