using ScadaLink.Commons.Entities.Scripts; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Interfaces.Services; using ScadaLink.Commons.Types; namespace ScadaLink.TemplateEngine; /// /// WP-5: Shared Script CRUD. /// System-wide scripts not associated with templates. /// Same parameter/return definition structure as template scripts. /// Includes syntax/structural validation (basic C# compilation check). /// public class SharedScriptService { private readonly ITemplateEngineRepository _repository; private readonly IAuditService _auditService; public SharedScriptService(ITemplateEngineRepository repository, IAuditService auditService) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _auditService = auditService ?? throw new ArgumentNullException(nameof(auditService)); } public async Task> CreateSharedScriptAsync( string name, string code, string? parameterDefinitions, string? returnDefinition, string user, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(name)) return Result.Failure("Shared script name is required."); if (string.IsNullOrWhiteSpace(code)) return Result.Failure("Shared script code is required."); // Check unique name var existing = await _repository.GetSharedScriptByNameAsync(name, cancellationToken); if (existing != null) return Result.Failure($"A shared script named '{name}' already exists."); // Syntax/structural validation var syntaxError = ValidateSyntax(code); if (syntaxError != null) return Result.Failure(syntaxError); var script = new SharedScript(name, code) { ParameterDefinitions = parameterDefinitions, ReturnDefinition = returnDefinition }; await _repository.AddSharedScriptAsync(script, cancellationToken); await _auditService.LogAsync(user, "Create", "SharedScript", "0", name, script, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); return Result.Success(script); } public async Task> UpdateSharedScriptAsync( int scriptId, string code, string? parameterDefinitions, string? returnDefinition, string user, CancellationToken cancellationToken = default) { var script = await _repository.GetSharedScriptByIdAsync(scriptId, cancellationToken); if (script == null) return Result.Failure($"Shared script with ID {scriptId} not found."); if (string.IsNullOrWhiteSpace(code)) return Result.Failure("Shared script code is required."); // Syntax/structural validation var syntaxError = ValidateSyntax(code); if (syntaxError != null) return Result.Failure(syntaxError); script.Code = code; script.ParameterDefinitions = parameterDefinitions; script.ReturnDefinition = returnDefinition; await _repository.UpdateSharedScriptAsync(script, cancellationToken); await _auditService.LogAsync(user, "Update", "SharedScript", scriptId.ToString(), script.Name, script, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); return Result.Success(script); } public async Task> DeleteSharedScriptAsync( int scriptId, string user, CancellationToken cancellationToken = default) { var script = await _repository.GetSharedScriptByIdAsync(scriptId, cancellationToken); if (script == null) return Result.Failure($"Shared script with ID {scriptId} not found."); await _repository.DeleteSharedScriptAsync(scriptId, cancellationToken); await _auditService.LogAsync(user, "Delete", "SharedScript", scriptId.ToString(), script.Name, null, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); return Result.Success(true); } public async Task GetSharedScriptByIdAsync(int scriptId, CancellationToken cancellationToken = default) { return await _repository.GetSharedScriptByIdAsync(scriptId, cancellationToken); } public async Task> GetAllSharedScriptsAsync(CancellationToken cancellationToken = default) { return await _repository.GetAllSharedScriptsAsync(cancellationToken); } /// /// Basic structural validation of C# script code. /// Checks for balanced braces and basic syntax structure. /// Full Roslyn compilation would be added in a later phase when the scripting sandbox is available. /// internal static string? ValidateSyntax(string code) { if (string.IsNullOrWhiteSpace(code)) return "Script code cannot be empty."; // Check for balanced braces int braceCount = 0; int bracketCount = 0; int parenCount = 0; foreach (var ch in code) { switch (ch) { case '{': braceCount++; break; case '}': braceCount--; break; case '[': bracketCount++; break; case ']': bracketCount--; break; case '(': parenCount++; break; case ')': parenCount--; break; } if (braceCount < 0) return "Syntax error: unmatched closing brace '}'."; if (bracketCount < 0) return "Syntax error: unmatched closing bracket ']'."; if (parenCount < 0) return "Syntax error: unmatched closing parenthesis ')'."; } if (braceCount != 0) return "Syntax error: unmatched opening brace '{'."; if (bracketCount != 0) return "Syntax error: unmatched opening bracket '['."; if (parenCount != 0) return "Syntax error: unmatched opening parenthesis '('."; return null; } }