using System.Net.Http.Json; using System.Text.Json; using FLSBLL.Dispatch; using FLSDAL.CustomCode.Bridge; using FLSDAL.CustomCode.Event; using FLSDAL.CustomCode.Instance; using FLSDAL.DTO.Event; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; namespace FLSBLL.Event; public class FlsEventBLL : IFlsEventBLL { private readonly IFlsEventDAL _eventDal; private readonly IFlsInstanceDAL _instanceDal; private readonly IFlsBridgeDAL _bridgeDal; private readonly IFlsDispatchBLL _dispatchBll; private readonly IHttpClientFactory _httpFactory; private readonly ILogger _logger; public FlsEventBLL( IFlsEventDAL eventDal, IFlsInstanceDAL instanceDal, IFlsBridgeDAL bridgeDal, IFlsDispatchBLL dispatchBll, IHttpClientFactory httpFactory, ILogger logger) { _eventDal = eventDal; _instanceDal = instanceDal; _bridgeDal = bridgeDal; _dispatchBll = dispatchBll; _httpFactory = httpFactory; _logger = logger; } public async Task PublishAsync( int flsInstanceId, int flsRespondentId, int groupId, int stepNo, FlsEventType eventType, int? templateDataId, string? sourceReference, LoginDTO login, CancellationToken ct, int depth = 0) { if (depth > 2) throw new InvalidOperationException( $"FlsEventBLL.PublishAsync depth exceeded (depth={depth}). Possible recursive event chain."); try { GB5Trace.Step("fls-event-publish", new { flsInstanceId, eventType, depth }); var payload = new FlsEventPayload( flsInstanceId, flsRespondentId, groupId, stepNo, (int)eventType, templateDataId, sourceReference, login.ClientId, login.UserId); string payloadJson = JsonSerializer.Serialize(payload); var instance = await _instanceDal.GetByIdAsync(flsInstanceId, login, ct).ConfigureAwait(false); if (instance is null) { _logger.LogWarning("PublishAsync: instance {Id} not found", flsInstanceId); return; } var subscriptions = await _eventDal.GetSubscriptionsAsync( instance.FlsRegistrationId, (int)eventType, login, ct).ConfigureAwait(false); foreach (var sub in subscriptions) { long eventLogId = await _eventDal.LogEventAsync( flsInstanceId, flsRespondentId, sub.BridgeConfigId, (int)eventType, payloadJson, correlationId: null, login, ct).ConfigureAwait(false); await DeliverSubscriptionAsync(sub, payload, eventLogId, flsInstanceId, groupId, login, ct) .ConfigureAwait(false); } // SignalR push to monitor hub is wired in FLSSL via IHubContext injection // After FormSubmitted: check for InstanceCompleted if (eventType == FlsEventType.FormSubmitted) await CheckInstanceCompletionAsync(flsInstanceId, groupId, login, ct, depth).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("fls-event-publish-failed", ex); _logger.LogError(ex, "PublishAsync failed for instance {Id} event {Event}", flsInstanceId, eventType); throw; } } public async Task> GetSelectListEventCategory( int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO login) { try { if (firstNumber <= 0 && maxResult <= 0) { firstNumber = -1; maxResult = -1; } return await _eventDal.GetSelectListEventCategory(firstNumber, maxResult, criteriaDTO, login).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-eventcategory-failed", ex); _logger.LogError(ex, "GetSelectListEventCategory failed"); throw; } } private async Task DeliverSubscriptionAsync( FlsEventSubscriptionRow sub, FlsEventPayload payload, long eventLogId, int flsInstanceId, int groupId, LoginDTO login, CancellationToken ct) { try { switch (sub.DeliveryType) { case 0: // HTTP bridge await DeliverHttpAsync(sub, payload, eventLogId, login, ct).ConfigureAwait(false); break; case 1: // Dapr pub/sub await DeliverDaprAsync(sub, payload, eventLogId, login, ct).ConfigureAwait(false); break; case 2: // Bridge SP — EndpointUrl stores the SP name when DeliveryType=2 if (!string.IsNullOrEmpty(sub.EndpointUrl)) await _bridgeDal.CallSummarySpAsync( sub.EndpointUrl, flsInstanceId, groupId, login, ct).ConfigureAwait(false); await _eventDal.UpdateEventStatusAsync( eventLogId, status: 2, failureReason: null, incrementRetry: false, nextRetryOn: null, login, ct).ConfigureAwait(false); break; } } catch (Exception ex) { _logger.LogError(ex, "DeliverSubscription failed for sub {SubId} event {LogId}", sub.EventSubscriptionId, eventLogId); var nextRetry = sub.RetryBackoffMin > 0 ? DateTime.UtcNow.AddMinutes(sub.RetryBackoffMin) : (DateTime?)null; await _eventDal.UpdateEventStatusAsync( eventLogId, status: 3, failureReason: ex.Message, incrementRetry: true, nextRetryOn: nextRetry, login, ct).ConfigureAwait(false); } } private async Task DeliverHttpAsync( FlsEventSubscriptionRow sub, FlsEventPayload payload, long eventLogId, LoginDTO login, CancellationToken ct) { var http = _httpFactory.CreateClient("FlsBridge"); var response = await http.PostAsJsonAsync(sub.EndpointUrl, payload, ct).ConfigureAwait(false); response.EnsureSuccessStatusCode(); await _eventDal.UpdateEventStatusAsync( eventLogId, status: 2, failureReason: null, incrementRetry: false, nextRetryOn: null, login, ct).ConfigureAwait(false); } private async Task DeliverDaprAsync( FlsEventSubscriptionRow sub, FlsEventPayload payload, long eventLogId, LoginDTO login, CancellationToken ct) { using var daprHttp = _httpFactory.CreateClient("Dapr"); var daprEndpoint = $"v1.0/publish/pubsub/{sub.EndpointUrl}"; var response = await daprHttp.PostAsJsonAsync(daprEndpoint, payload, ct).ConfigureAwait(false); response.EnsureSuccessStatusCode(); await _eventDal.UpdateEventStatusAsync( eventLogId, status: 2, failureReason: null, incrementRetry: false, nextRetryOn: null, login, ct).ConfigureAwait(false); } private async Task CheckInstanceCompletionAsync( int flsInstanceId, int groupId, LoginDTO login, CancellationToken ct, int depth) { var summaryRows = await _instanceDal.GetSummaryAsync(flsInstanceId, login, ct).ConfigureAwait(false); var rows = summaryRows.ToList(); int total = rows.Sum(r => r.RespondentCount); int submitted = rows.Sum(r => r.SubmittedCount); if (total > 0 && total == submitted) { _logger.LogInformation("FLS instance {Id} is 100% complete — publishing InstanceCompleted", flsInstanceId); await PublishAsync( flsInstanceId, flsRespondentId: 0, groupId, stepNo: 0, FlsEventType.InstanceCompleted, templateDataId: null, sourceReference: null, login, ct, depth: depth + 1).ConfigureAwait(false); } } }