using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Types; namespace ZB.MOM.WW.ScadaBridge.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; /// /// Initializes a new instance of the class. /// /// The template engine repository for data access. /// The audit service for logging operations. public SharedScriptService(ITemplateEngineRepository repository, IAuditService auditService) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _auditService = auditService ?? throw new ArgumentNullException(nameof(auditService)); } /// /// Creates a new shared script. /// /// The shared script name. /// The shared script code. /// Optional parameter definitions JSON. /// Optional return definition JSON. /// The user creating the script. /// A cancellation token that can be used to cancel the operation. /// A result containing the created script or an error message. 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 }; // TemplateEngine-020: save the entity first so EF Core populates the // auto-generated key, then write the audit row with the real // script.Id, then save the audit row. The pre-fix order logged // EntityId = "0" because the audit row was queued before // SaveChangesAsync ran. await _repository.AddSharedScriptAsync(script, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); await _auditService.LogAsync(user, "Create", "SharedScript", script.Id.ToString(), name, script, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); return Result.Success(script); } /// /// Updates an existing shared script. /// /// The shared script ID to update. /// The updated shared script code. /// Optional updated parameter definitions JSON. /// Optional updated return definition JSON. /// The user updating the script. /// A cancellation token that can be used to cancel the operation. /// A result containing the updated script or an error message. 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); } /// /// Deletes a shared script. /// /// The shared script ID to delete. /// The user deleting the script. /// A cancellation token that can be used to cancel the operation. /// A result containing true if successful or an error message. 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); } /// /// Gets a shared script by ID. /// /// The shared script ID. /// A cancellation token that can be used to cancel the operation. /// The shared script if found; null otherwise. public async Task GetSharedScriptByIdAsync(int scriptId, CancellationToken cancellationToken = default) { return await _repository.GetSharedScriptByIdAsync(scriptId, cancellationToken); } /// /// Gets all shared scripts. /// /// A cancellation token that can be used to cancel the operation. /// A read-only list of all shared scripts. public async Task> GetAllSharedScriptsAsync(CancellationToken cancellationToken = default) { return await _repository.GetAllSharedScriptsAsync(cancellationToken); } /// /// Basic structural validation of C# script code. /// Checks for balanced braces/brackets/parentheses. The scan is string- and /// comment-aware (see ) so a /// delimiter inside a regular/verbatim/interpolated/raw string literal, a /// char literal, or a comment does not produce a false syntax error. /// Full Roslyn compilation would be added in a later phase when the scripting sandbox is available. /// /// The C# code to validate. /// An error message if validation fails; null if valid. internal static string? ValidateSyntax(string code) { if (string.IsNullOrWhiteSpace(code)) return "Script code cannot be empty."; return Validation.CSharpDelimiterScanner.Scan(code) switch { Validation.CSharpDelimiterScanner.Mismatch.None => null, Validation.CSharpDelimiterScanner.Mismatch.UnexpectedCloseBrace => "Syntax error: unmatched closing brace '}'.", Validation.CSharpDelimiterScanner.Mismatch.UnclosedBrace => "Syntax error: unmatched opening brace '{'.", Validation.CSharpDelimiterScanner.Mismatch.UnexpectedCloseBracket => "Syntax error: unmatched closing bracket ']'.", Validation.CSharpDelimiterScanner.Mismatch.UnclosedBracket => "Syntax error: unmatched opening bracket '['.", Validation.CSharpDelimiterScanner.Mismatch.UnexpectedCloseParen => "Syntax error: unmatched closing parenthesis ')'.", Validation.CSharpDelimiterScanner.Mismatch.UnclosedParen => "Syntax error: unmatched opening parenthesis '('.", Validation.CSharpDelimiterScanner.Mismatch.UnclosedBlockComment => "Syntax error: unclosed block comment.", Validation.CSharpDelimiterScanner.Mismatch.UnterminatedString => "Syntax error: unterminated string literal.", Validation.CSharpDelimiterScanner.Mismatch.UnterminatedChar => "Syntax error: unterminated character literal.", _ => null, }; } }