using FrameworkBLL.EIPConversation.EIPHandlers.EIPActionExecutor; using FrameworkBLL.EIPConversation.EIPHandlers.EIPOTP; using FrameworkBLL.EIPConversation.EIPHandlers.EIPRisk; using FrameworkDAL.DTO.EIPConversation; using GB5Shared.DTO.Framework.Login; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace FrameworkBLL.EIPConversation.EIPHandlers.EIPEngine.CapabilityEngine { public class EIPCapabilityEngine : IEIPCapabilityEngine { private readonly IEIPRiskAssessment _riskAssessment; private readonly IEIPOtpService _otpService; private readonly IEIPActionExecutor _actionExecutor; private readonly ILogger _logger; public EIPCapabilityEngine( IEIPRiskAssessment riskAssessment, IEIPOtpService otpService, IEIPActionExecutor actionExecutor, ILogger logger) { _riskAssessment = riskAssessment ?? throw new ArgumentNullException(nameof(riskAssessment)); _otpService = otpService ?? throw new ArgumentNullException(nameof(otpService)); _actionExecutor = actionExecutor ?? throw new ArgumentNullException(nameof(actionExecutor)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task ExecuteCapabilityAsync( string capabilityCode, EIPExecutionContextDTO context, LoginDTO loginDTO, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(capabilityCode)) throw new ArgumentException("CapabilityCode cannot be null or empty.", nameof(capabilityCode)); if (context == null) throw new ArgumentNullException(nameof(context)); if (loginDTO == null) throw new InvalidOperationException("LoginDTO cannot be null in execution context."); try { _logger.LogInformation( "Phase4 | Capability execution started | Capability={Capability} | Conversation={ConversationId} | Tenant={Tenant} | User={UserId}", capabilityCode, context.ConversationId, loginDTO.ClientId, loginDTO.UserId); // ---------------- 1️⃣ Resume-or-Risk-Evaluate ---------------- // A still-pending OTP for this (tenant, user, capability) means we're resuming // after having already paused to ask for a code — treat the inbound message as // the verification attempt instead of re-running risk assessment from scratch. if (await _otpService.HasPendingOtpAsync(capabilityCode, context, cancellationToken) .ConfigureAwait(false)) { var attempt = context.NormalizedMessage?.Trim(); if (string.IsNullOrWhiteSpace(attempt)) attempt = context.Message?.Trim(); if (string.IsNullOrWhiteSpace(attempt)) return EIPCapabilityExecutionResultDTO.Pause( "Please enter the verification code sent to you to continue."); var verified = await _otpService .VerifyOtpAsync(capabilityCode, attempt, context, cancellationToken) .ConfigureAwait(false); if (!verified) return EIPCapabilityExecutionResultDTO.Pause( "That code was incorrect or has expired. Please try again."); // Verified — fall through to execute the capability below. } else { var riskResult = await _riskAssessment .EvaluateRiskAsync(capabilityCode, context, cancellationToken) .ConfigureAwait(false); if (riskResult?.RequiresOtp == true) { _logger.LogWarning( "Phase4 | OTP required | Capability={Capability} | RiskLevel={RiskLevel} | Conversation={ConversationId}", capabilityCode, riskResult.RiskLevel, context.ConversationId); await _otpService.GenerateOtpAsync(capabilityCode, context, cancellationToken) .ConfigureAwait(false); return EIPCapabilityExecutionResultDTO.Pause( "A verification code has been sent. Please enter it to continue."); } } // ---------------- 2️⃣ Build Action Context ---------------- // Merge flow-collected INPUT variables into Payload so action handlers can read them. // Variables are written by INPUT steps; Payload is what EIPActionContextDTO exposes. context.Payload ??= new Dictionary(); if (context.Variables?.Count > 0) foreach (var kv in context.Variables) if (!context.Payload.ContainsKey(kv.Key)) context.Payload[kv.Key] = kv.Value; var actionContext = new EIPActionContextDTO { ActionCode = capabilityCode, ConversationId = context.ConversationId, UserIdentifier = context.UserIdentifier, Payload = context.Payload, IdempotencyKey = context.CorrelationId.ToString(), TenantId = context.TenantId }; // ---------------- 3️⃣ Execute Action ---------------- var actionResult = await _actionExecutor.ExecuteAsync( capabilityCode, actionContext, loginDTO, cancellationToken); _logger.LogInformation( "Phase4 | Capability executed successfully | Capability={Capability} | Conversation={ConversationId}", capabilityCode, context.ConversationId); return EIPCapabilityExecutionResultDTO.Completed( actionResult?.Message ?? $"Capability '{capabilityCode}' executed successfully."); } catch (Exception ex) { _logger.LogError(ex, "Phase4 | Error executing capability | Capability={Capability} | Conversation={ConversationId} | Tenant={Tenant} | User={UserId}", capabilityCode, context?.ConversationId, loginDTO?.ClientId, loginDTO?.UserId); throw; } } } }