using ScadaLink.Commons.Interfaces.Services; using ScadaLink.Commons.Messages.InboundApi; using ScadaLink.Communication; namespace ScadaLink.InboundAPI; /// /// WP-4: Route.To() helper for cross-site calls from inbound API scripts. /// Resolves instance to site, routes via CommunicationService, blocks until response or timeout. /// Site unreachable returns error (no store-and-forward). /// public class RouteHelper { private readonly IInstanceLocator _instanceLocator; private readonly CommunicationService _communicationService; public RouteHelper( IInstanceLocator instanceLocator, CommunicationService communicationService) { _instanceLocator = instanceLocator; _communicationService = communicationService; } /// /// Creates a route target for the specified instance. /// public RouteTarget To(string instanceCode) { return new RouteTarget(instanceCode, _instanceLocator, _communicationService); } } /// /// WP-4: Represents a route target (an instance) for cross-site calls. /// public class RouteTarget { private readonly string _instanceCode; private readonly IInstanceLocator _instanceLocator; private readonly CommunicationService _communicationService; internal RouteTarget( string instanceCode, IInstanceLocator instanceLocator, CommunicationService communicationService) { _instanceCode = instanceCode; _instanceLocator = instanceLocator; _communicationService = communicationService; } /// /// Calls a script on the remote instance. Synchronous from API caller's perspective. /// public async Task Call( string scriptName, IReadOnlyDictionary? parameters = null, CancellationToken cancellationToken = default) { var siteId = await ResolveSiteAsync(cancellationToken); var correlationId = Guid.NewGuid().ToString(); var request = new RouteToCallRequest( correlationId, _instanceCode, scriptName, parameters, DateTimeOffset.UtcNow); var response = await _communicationService.RouteToCallAsync( siteId, request, cancellationToken); if (!response.Success) { throw new InvalidOperationException( response.ErrorMessage ?? "Remote script call failed"); } return response.ReturnValue; } /// /// Gets a single attribute value from the remote instance. /// public async Task GetAttribute( string attributeName, CancellationToken cancellationToken = default) { var result = await GetAttributes(new[] { attributeName }, cancellationToken); return result.TryGetValue(attributeName, out var value) ? value : null; } /// /// Gets multiple attribute values from the remote instance (batch read). /// public async Task> GetAttributes( IEnumerable attributeNames, CancellationToken cancellationToken = default) { var siteId = await ResolveSiteAsync(cancellationToken); var correlationId = Guid.NewGuid().ToString(); var request = new RouteToGetAttributesRequest( correlationId, _instanceCode, attributeNames.ToList(), DateTimeOffset.UtcNow); var response = await _communicationService.RouteToGetAttributesAsync( siteId, request, cancellationToken); if (!response.Success) { throw new InvalidOperationException( response.ErrorMessage ?? "Remote attribute read failed"); } return response.Values; } /// /// Sets a single attribute value on the remote instance. /// public async Task SetAttribute( string attributeName, string value, CancellationToken cancellationToken = default) { await SetAttributes( new Dictionary { { attributeName, value } }, cancellationToken); } /// /// Sets multiple attribute values on the remote instance (batch write). /// public async Task SetAttributes( IReadOnlyDictionary attributeValues, CancellationToken cancellationToken = default) { var siteId = await ResolveSiteAsync(cancellationToken); var correlationId = Guid.NewGuid().ToString(); var request = new RouteToSetAttributesRequest( correlationId, _instanceCode, attributeValues, DateTimeOffset.UtcNow); var response = await _communicationService.RouteToSetAttributesAsync( siteId, request, cancellationToken); if (!response.Success) { throw new InvalidOperationException( response.ErrorMessage ?? "Remote attribute write failed"); } } private async Task ResolveSiteAsync(CancellationToken cancellationToken) { var siteId = await _instanceLocator.GetSiteIdForInstanceAsync(_instanceCode, cancellationToken); if (siteId == null) { throw new InvalidOperationException( $"Instance '{_instanceCode}' not found or has no assigned site"); } return siteId; } }