using FrameworkDAL.DTO.EIPConversation; using Microsoft.Extensions.Logging; using System.Text.Json; namespace FrameworkBLL.EIPConversation.EIPHandlers.EIPEngine.ChannelNormalizer { public class EIPChannelNormalizer : IEIPChannelNormalizer { private readonly ILogger _logger; public EIPChannelNormalizer(ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// Phase 1 – Normalize inbound message for EIP execution. /// FlowCode and UserIdentifier are mandatory. /// public async Task NormalizeAsync( EIPConversationDTO payload, CancellationToken cancellationToken) { if (payload == null) throw new ArgumentNullException(nameof(payload)); cancellationToken.ThrowIfCancellationRequested(); try { _logger.LogInformation( "PHASE1 START | Tenant={Tenant} | Channel={Channel} | RawMessage={Message}", payload.TenantId, payload.ChannelType, payload.Message); // ---- Validate mandatory fields ---- if (string.IsNullOrWhiteSpace(payload.UserIdentifier)) { _logger.LogError( "UserIdentifier missing | Tenant={Tenant} | Channel={Channel}", payload.TenantId, payload.ChannelType); throw new InvalidOperationException("UserIdentifier is mandatory."); } // ---- Sanitize message ---- var sanitizedMessage = payload.Message?.Trim() ?? string.Empty; _logger.LogDebug("SanitizedMessage={SanitizedMessage}", sanitizedMessage); // ---- Extract RawPayload (if JSON) ---- var rawPayload = ExtractRawPayload(sanitizedMessage); _logger.LogDebug("RawPayload={RawPayload}", rawPayload); // ---- Normalize keyword ---- var normalizedKeyword = string.IsNullOrWhiteSpace(rawPayload) ? sanitizedMessage.ToLowerInvariant() : rawPayload.Trim().ToLowerInvariant(); _logger.LogDebug("NormalizedKeyword={NormalizedKeyword}", normalizedKeyword); // ---- Resolve FlowCode (strict) ---- var flowCode = payload.FlowCode?.Trim(); if (string.IsNullOrWhiteSpace(flowCode)) { flowCode = ExtractFlowCodeFromMessage(sanitizedMessage); } if (string.IsNullOrWhiteSpace(flowCode)) { // FlowCode not in payload — routing engine will resolve it from routing rules _logger.LogInformation( "FlowCode not in payload, routing engine will resolve | Tenant={Tenant} | User={User}", payload.TenantId, payload.UserIdentifier); } else { _logger.LogInformation( "FlowCode resolved from payload | Tenant={Tenant} | FlowCode={FlowCode}", payload.TenantId, flowCode); } // ---- Build execution context ---- var executionContext = new EIPExecutionContextDTO { TenantId = payload.TenantId, ChannelType = payload.ChannelType, UserIdentifier = payload.UserIdentifier.Trim(), OriginalMessage = sanitizedMessage, RawPayload = rawPayload ?? string.Empty, NormalizedMessage = normalizedKeyword ?? string.Empty, FlowCode = flowCode, CorrelationId = Guid.NewGuid(), ConversationId = payload.ConversationId > 0 ? payload.ConversationId : 0, IsCompleted = false, ReceivedAtUtc = DateTime.UtcNow }; _logger.LogInformation( "PHASE1 COMPLETE | Tenant={Tenant} | CorrelationId={CorrelationId} | User={User} | FlowCode={FlowCode}", executionContext.TenantId, executionContext.CorrelationId, executionContext.UserIdentifier, executionContext.FlowCode); await Task.CompletedTask; return executionContext; } catch (OperationCanceledException) { _logger.LogWarning( "PHASE1 CANCELLED | Tenant={Tenant}", payload.TenantId); throw; } catch (Exception ex) { _logger.LogError(ex, "PHASE1 ERROR | Tenant={Tenant} | Message={Message}", payload.TenantId, payload.Message); throw; } } // -------------------------------------------------------- // Extract RawPayload from JSON safely // -------------------------------------------------------- private string ExtractRawPayload(string message) { if (string.IsNullOrWhiteSpace(message)) return string.Empty; try { using var doc = JsonDocument.Parse(message); // Only attempt property access on a JSON object. // Plain values like "1", "true", "3.14" are valid JSON but not objects // and TryGetProperty throws InvalidOperationException on non-Object elements. if (doc.RootElement.ValueKind == JsonValueKind.Object && doc.RootElement.TryGetProperty("RawPayload", out var element)) { return element.GetString()?.Trim() ?? string.Empty; } } catch (JsonException ex) { _logger.LogDebug("RawPayload JSON parse skipped: {Reason}", ex.Message); } return message; } // -------------------------------------------------------- // Extract FlowCode from JSON safely // -------------------------------------------------------- private string? ExtractFlowCodeFromMessage(string message) { if (string.IsNullOrWhiteSpace(message)) return null; try { using var doc = JsonDocument.Parse(message); // Guard: TryGetProperty throws on non-Object elements (e.g. numbers, booleans) if (doc.RootElement.ValueKind != JsonValueKind.Object) return null; if (doc.RootElement.TryGetProperty("FlowCode", out var fc)) return fc.GetString()?.Trim(); if (doc.RootElement.TryGetProperty("flowcode", out var fcLower)) return fcLower.GetString()?.Trim(); } catch (JsonException ex) { _logger.LogDebug("FlowCode JSON parse skipped: {Reason}", ex.Message); } return null; } } }