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,
};
}
}