using Microsoft.Extensions.Logging; using OptimAI.BRE.RuleEngine.Domain; using OptimAI.BRE.Shared.Domain; namespace OptimAI.BRE.RuleEngine.Application; public sealed class ActionExecutor : IActionExecutor { private readonly IDeviationTypeRepository _deviationTypeRepo; private readonly ILogger _logger; public ActionExecutor(IDeviationTypeRepository deviationTypeRepo, ILogger logger) { _deviationTypeRepo = deviationTypeRepo; _logger = logger; } public async Task ExecuteAsync(RuleAction action, ExecutionContext context, RuleExecutionState state) { try { switch (action.Type) { case ActionType.SetDecision: ExecuteSetDecision(action, state); break; case ActionType.SetRisk: ExecuteSetRisk(action, state); break; case ActionType.SetTrafficLight: ExecuteSetTrafficLight(action, state); break; case ActionType.AddDeviation: await ExecuteAddDeviationAsync(action, context, state); break; case ActionType.SetField: ExecuteSetField(action, state); break; case ActionType.AddTag: ExecuteAddTag(action, state); break; case ActionType.SetScore: ExecuteSetScore(action, state); break; default: _logger.LogWarning("Unknown action type: {ActionType}", action.Type); break; } } catch (Exception ex) { _logger.LogError(ex, "Action execution failed: {ActionType}", action.Type); } } private static void ExecuteSetDecision(RuleAction action, RuleExecutionState state) { if (Enum.TryParse(action.Value, true, out var decision)) { // Higher severity decisions take priority: Reject > Deviation > Refer > Approve var priority = new Dictionary { [Decision.Reject] = 4, [Decision.Deviation] = 3, [Decision.Refer] = 2, [Decision.Approve] = 1, [Decision.Pending] = 0 }; if (priority.GetValueOrDefault(decision, 0) > priority.GetValueOrDefault(state.GlobalState.CurrentDecision, 0)) { state.GlobalState.CurrentDecision = decision; if (decision == Decision.Reject) state.GlobalState.ShouldStop = false; // Continue collecting all rejection reasons } } } private static void ExecuteSetRisk(RuleAction action, RuleExecutionState state) { if (Enum.TryParse(action.Value, true, out var risk)) { // Escalate risk level only var currentRisk = state.GlobalState.OutputFields.GetValueOrDefault("_riskCategory") as string; if (currentRisk == null || ShouldEscalate(currentRisk, risk.ToString())) state.GlobalState.OutputFields["_riskCategory"] = risk.ToString(); } } private static void ExecuteSetTrafficLight(RuleAction action, RuleExecutionState state) { if (Enum.TryParse(action.Value, true, out var tl)) state.GlobalState.TrafficLight = tl; } private async Task ExecuteAddDeviationAsync(RuleAction action, ExecutionContext context, RuleExecutionState state) { var deviationCode = action.Value ?? action.Parameters.GetValueOrDefault("code"); if (deviationCode == null) return; var deviationType = await _deviationTypeRepo.FindByCodeAsync(context.TenantId, deviationCode); var deviation = new ExecutionDeviation { TenantId = context.TenantId, DeviationCode = deviationCode, DeviationName = deviationType?.DeviationName ?? deviationCode, Severity = deviationType?.DefaultSeverity ?? ParseSeverity(action.Parameters.GetValueOrDefault("severity")), Reason = action.Parameters.GetValueOrDefault("reason") ?? deviationType?.Description ?? "Policy deviation detected", FieldPath = action.Parameters.GetValueOrDefault("field"), RecommendedAction = action.Parameters.GetValueOrDefault("action") ?? deviationType?.RecommendedAction, DeviationTypeId = deviationType?.Id }; // Resolve field values for context if (deviation.FieldPath != null) { var val = context.Data.GetValue(deviation.FieldPath); deviation.ActualValue = val?.ToString(); } state.GlobalState.Deviations.Add(deviation); // Automatically set decision to DEVIATION if not already REJECT if (state.GlobalState.CurrentDecision != Decision.Reject) state.GlobalState.CurrentDecision = Decision.Deviation; } private static void ExecuteSetField(RuleAction action, RuleExecutionState state) { if (action.Field != null) state.GlobalState.OutputFields[action.Field] = action.Value ?? ""; } private static void ExecuteAddTag(RuleAction action, RuleExecutionState state) { if (action.Value != null && !state.GlobalState.Tags.Contains(action.Value)) state.GlobalState.Tags.Add(action.Value); } private static void ExecuteSetScore(RuleAction action, RuleExecutionState state) { if (decimal.TryParse(action.Value, out var score)) { // Score adjustments: can be absolute or relative (+10, -5) if (action.Value!.StartsWith('+') || action.Value.StartsWith('-')) state.GlobalState.RiskScore = Math.Clamp(state.GlobalState.RiskScore + score, 0, 100); else state.GlobalState.RiskScore = Math.Clamp(score, 0, 100); } } private static bool ShouldEscalate(string current, string incoming) { var order = new[] { "Low", "Medium", "High", "Critical" }; var currentIdx = Array.IndexOf(order, current); var incomingIdx = Array.IndexOf(order, incoming); return incomingIdx > currentIdx; } private static Severity ParseSeverity(string? value) => Enum.TryParse(value, true, out var s) ? s : Severity.Medium; } public interface IDeviationTypeRepository { Task FindByCodeAsync(Guid tenantId, string code, CancellationToken ct = default); Task> GetAllActiveAsync(Guid tenantId, CancellationToken ct = default); }