using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
namespace ZB.MOM.WW.OtOpcUa.Driver.Calculation;
///
/// Static factory registration helper for . The Server's
/// DriverFactoryBootstrap calls once at startup; the bootstrapper then
/// materialises Calculation DriverInstance rows into live driver instances. Mirrors
/// ModbusDriverFactoryExtensions, with the addition of a so a
/// script failure fans out onto the script-logs topic (via the root logger's
/// ScriptLogTopicSink) — the calc driver has no Akka access of its own.
///
public static class CalculationDriverFactoryExtensions
{
/// The DriverInstance.DriverType string this factory registers under.
public const string DriverTypeName = "Calculation";
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
Converters = { new JsonStringEnumConverter() },
};
///
/// Register the Calculation factory with the driver registry. The optional
/// + are captured at registration
/// time; both may be null (the guard test invokes Register(registry) reflectively with the
/// trailing parameters defaulted), in which case the driver runs with the null logger + a no-op
/// script-logger.
///
/// The driver factory registry to register with.
/// Optional logger factory for per-instance diagnostics loggers.
/// Optional root script logger (fans failure entries onto the script-logs topic).
public static void Register(
DriverFactoryRegistry registry,
ILoggerFactory? loggerFactory = null,
ScriptRootLogger? scriptRoot = null)
{
ArgumentNullException.ThrowIfNull(registry);
registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory, scriptRoot));
}
/// Public overload for the Server-side bootstrapper + test consumers.
/// The unique identifier for the driver instance.
/// The merged driver config JSON (carries the RawTags array).
/// The constructed .
public static CalculationDriver CreateInstance(string driverInstanceId, string driverConfigJson)
=> CreateInstance(driverInstanceId, driverConfigJson, loggerFactory: null, scriptRoot: null);
/// Logger/script-logger-aware overload — used by 's closure via DI.
/// The unique identifier for the driver instance.
/// The merged driver config JSON (carries the RawTags array).
/// Optional logger factory for per-instance diagnostics loggers.
/// Optional root script logger for failure fan-out.
/// The constructed .
public static CalculationDriver CreateInstance(
string driverInstanceId, string driverConfigJson,
ILoggerFactory? loggerFactory, ScriptRootLogger? scriptRoot)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
var dto = JsonSerializer.Deserialize(driverConfigJson, JsonOptions)
?? throw new InvalidOperationException(
$"Calculation driver config for '{driverInstanceId}' deserialised to null");
var options = new CalculationDriverOptions
{
RawTags = dto.RawTags is { Count: > 0 } ? [.. dto.RawTags] : [],
RunTimeout = dto.RunTimeoutMs is { } ms && ms > 0
? TimeSpan.FromMilliseconds(ms) : TimeSpan.FromSeconds(2),
};
return new CalculationDriver(
options, driverInstanceId, scriptRoot,
logger: loggerFactory?.CreateLogger());
}
/// The merged-config DTO the calc factory binds from. The Calculation driver has no
/// backend/endpoint config — only the injected raw tags (+ an optional evaluation-timeout override).
internal sealed class CalculationDriverConfigDto
{
/// The driver's authored raw calc tags (RawPath + TagConfig blob + WriteIdempotent + DeviceName).
public List? RawTags { get; init; }
/// Optional per-script evaluation wall-clock budget override, in milliseconds (default 2000).
public int? RunTimeoutMs { get; init; }
}
}