using FrameworkBLL.EIPConversation.EIPHandlers.EIPEngine.ActionHandler; using FrameworkDAL.CustomCode.EIPConversation.EIPIdempotency; using FrameworkDAL.DTO.EIPConversation; using GB5Shared.DTO.Framework.Login; using Microsoft.Extensions.Logging; using System.Diagnostics; namespace FrameworkBLL.EIPConversation.EIPHandlers.EIPEngine.ActionEngine { public class EIPActionEngine : IEIPActionEngine { private readonly ILogger _logger; private readonly IReadOnlyDictionary _handlerMap; private readonly IEIPIdempotencyDAL _idempotencyDAL; public EIPActionEngine( ILogger logger, IEnumerable handlers, IEIPIdempotencyDAL idempotencyDAL) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _idempotencyDAL = idempotencyDAL ?? throw new ArgumentNullException(nameof(idempotencyDAL)); _handlerMap = handlers.ToDictionary(h => h.ActionCode, StringComparer.OrdinalIgnoreCase); } public async Task ExecuteAsync( EIPActionContextDTO context, LoginDTO loginDTO, CancellationToken cancellationToken = default) { if (context == null) throw new ArgumentNullException(nameof(context)); if (loginDTO == null) throw new ArgumentNullException(nameof(loginDTO)); try { _logger.LogInformation("Action execution started | ActionCode={ActionCode}", context.ActionCode); // DB-backed idempotency: INSERT into TEIPIDEMPOTENCYRECORD with UNIQUE key. // Persisted across restarts; safe across multiple service instances. // SqlException 2627 (unique constraint) in DAL → returns false → duplicate skipped. if (!string.IsNullOrWhiteSpace(context.IdempotencyKey)) { var isNew = await _idempotencyDAL.TryRecordAsync( context.IdempotencyKey, context.ActionCode, loginDTO, cancellationToken).ConfigureAwait(false); if (!isNew) { _logger.LogWarning("Duplicate action blocked | Key={Key}", context.IdempotencyKey); return new EIPActionResultDTO { IsSuccess = true, Message = "Duplicate request ignored" }; } } if (string.IsNullOrWhiteSpace(context.ActionCode)) throw new ArgumentException("ActionCode is required.", nameof(context.ActionCode)); if (!_handlerMap.TryGetValue(context.ActionCode, out var handler)) throw new InvalidOperationException($"No handler found for action: {context.ActionCode}"); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(context.TimeoutSeconds)); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); var stopwatch = Stopwatch.StartNew(); var result = await handler.HandleAsync(context, loginDTO, linkedCts.Token); stopwatch.Stop(); _logger.LogInformation( "Action completed | ActionCode={ActionCode} | Duration={Duration}ms", context.ActionCode, stopwatch.ElapsedMilliseconds); return result; } catch (OperationCanceledException) { _logger.LogError("Action timeout | ActionCode={ActionCode}", context.ActionCode); return new EIPActionResultDTO { IsSuccess = false, Message = "Action timeout", ShouldRetry = true }; } catch (Exception ex) { _logger.LogError(ex, "Action execution failed | ActionCode={ActionCode}", context.ActionCode); return new EIPActionResultDTO { IsSuccess = false, Message = ex.Message, ShouldRetry = false }; } } } }