using FrameworkDAL.CustomCode.DirectAction; using FrameworkDAL.DTO.DirectAction; using GB5Shared.DTO.Framework.Login; using Microsoft.Extensions.Logging; using System; using System.Linq; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace FrameworkBLL.DirectAction { /// /// Executes a direct action based on the MDIRECTACTIONDETAIL configuration. /// For NEEDSINPUT=0: posts to the configured API endpoint using IHttpClientFactory. /// For NEEDSINPUT=1: conversational flow — logs a warning; EIP integration is future work. /// public class GenericApiDirectActionHandler : IGenericApiDirectActionHandler { private readonly IDirectActionConfigDAL _configDAL; private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; public GenericApiDirectActionHandler( IDirectActionConfigDAL configDAL, IHttpClientFactory httpClientFactory, ILogger logger) { _configDAL = configDAL ?? throw new ArgumentNullException(nameof(configDAL)); _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } // Called by ActionSubmitEndpoint (NEEDSINPUT=1 path) — substitutes {Remarks} in payload public async Task ExecuteAsync( string actionCode, int contextId, int assigneeUserId, string? remarks, LoginDTO login, CancellationToken ct = default) { var config = await _configDAL.GetDetailByActionCodeAsync(actionCode, login, ct) .ConfigureAwait(false); if (config != null && !string.IsNullOrWhiteSpace(remarks)) config.PayloadTemplate = (config.PayloadTemplate ?? string.Empty) .Replace("{Remarks}", remarks, StringComparison.OrdinalIgnoreCase); return await RunAsync(config, actionCode, contextId, remarks, string.Empty, login, ct).ConfigureAwait(false); } // Called by ActionExecuteEndpoint (NEEDSINPUT=0 path) public async Task ExecuteAsync( string actionCode, int contextId, int assigneeUserId, string? remarks, LoginDTO login, string frameworkBaseUrl = "", CancellationToken ct = default) { var config = await _configDAL.GetDetailByActionCodeAsync(actionCode, login, ct) .ConfigureAwait(false); return await RunAsync(config, actionCode, contextId, remarks, frameworkBaseUrl, login, ct).ConfigureAwait(false); } private async Task RunAsync( DirectActionDetailDTO? config, string actionCode, int contextId, string? remarks, string frameworkBaseUrl, LoginDTO login, CancellationToken ct) { if (config == null) { _logger.LogWarning("DirectAction: no config found for ActionCode={Code}", actionCode); return DirectActionResult.Fail($"No action configured for code '{actionCode}'."); } // NOTE: there used to be an unconditional "NeedsInput==1 -> Fail" guard here. // It fired regardless of whether input had already been collected, which broke // BOTH legitimate NeedsInput=1 callers: ActionSubmitEndpoint (remarks already // typed and passed in via `remarks`) and the EIP conversational handoff's // DIRECTACTION_EXECUTE completion handler (a full flow already ran to collect // input). By the time RunAsync is reached, NeedsInput==1 has already been // satisfied by whichever caller got here — there is nothing left to gate on. if (string.IsNullOrWhiteSpace(config.ApiEndpoint)) { _logger.LogError("DirectAction: APIENDPOINT is empty | ActionCode={Code}", actionCode); return DirectActionResult.Fail("Action endpoint is not configured."); } var payload = (config.PayloadTemplate ?? string.Empty) .Replace("{ContextId}", contextId.ToString(), StringComparison.OrdinalIgnoreCase) .Replace("{Remarks}", remarks ?? string.Empty, StringComparison.OrdinalIgnoreCase); // Build full URL: frameworkBaseUrl (captured from live HTTP request by ActionExecuteEndpoint, // same pattern as WIP deriving CALLBACKENDPOINT from login.RequestUrl) + APIENDPOINT from DB. // Zero config — no appsettings key, no manual DB update needed. if (string.IsNullOrWhiteSpace(frameworkBaseUrl)) { _logger.LogError( "DirectAction: frameworkBaseUrl is empty — cannot call API | ActionCode={Code}. " + "ActionExecuteEndpoint must pass Request.Scheme + '://' + Request.Host.", actionCode); return DirectActionResult.Fail("Framework base URL could not be determined from the incoming request."); } var fullUrl = frameworkBaseUrl.TrimEnd('/') + "/" + (config.ApiEndpoint ?? string.Empty).TrimStart('/'); _logger.LogInformation( "DirectAction [START] | ActionCode={Code} ContextId={Ctx} Url={Url} Method={Method}", actionCode, contextId, fullUrl, config.HttpMethod); using var client = _httpClientFactory.CreateClient("gb5-internal"); client.DefaultRequestHeaders.Remove("Login"); client.DefaultRequestHeaders.Add("Login", JsonSerializer.Serialize(login)); using var content = new StringContent(payload, Encoding.UTF8, "application/json"); HttpResponseMessage response = config.HttpMethod switch { "POST" => await client.PostAsync(fullUrl, content, ct).ConfigureAwait(false), "PUT" => await client.PutAsync(fullUrl, content, ct).ConfigureAwait(false), "PATCH" => await client.PatchAsync(fullUrl, content, ct).ConfigureAwait(false), "DELETE" => await client.DeleteAsync(fullUrl, ct).ConfigureAwait(false), _ => throw new InvalidOperationException($"Unsupported HTTP method: {config.HttpMethod}") }; var responseBody = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false); if (!response.IsSuccessStatusCode) { _logger.LogError( "DirectAction [FAIL] | ActionCode={Code} Status={Status} Body={Body}", actionCode, (int)response.StatusCode, responseBody); return DirectActionResult.Fail($"API returned {(int)response.StatusCode}."); } // HTTP 200 — GB5 standard response still carries business failures. // ResponseStatus.Failed = 400; Body.GeneralErrors holds the message array. try { using var doc = JsonDocument.Parse(responseBody); var root = doc.RootElement; if (root.TryGetProperty("Status", out var statusEl) && statusEl.TryGetInt32(out var statusInt) && statusInt == 400) { var errorMsg = "Action failed"; if (root.TryGetProperty("Body", out var bodyEl) && bodyEl.ValueKind == JsonValueKind.Object && bodyEl.TryGetProperty("GeneralErrors", out var errorsEl) && errorsEl.ValueKind == JsonValueKind.Array) { var msgs = string.Join("; ", errorsEl.EnumerateArray() .Where(e => e.ValueKind == JsonValueKind.String) .Select(e => e.GetString()) .Where(s => !string.IsNullOrWhiteSpace(s))); if (!string.IsNullOrWhiteSpace(msgs)) errorMsg = msgs; } _logger.LogError( "DirectAction [FAIL] | ActionCode={Code} ContextId={Ctx} GB5Status=Failed Error={Err}", actionCode, contextId, errorMsg); return DirectActionResult.Fail(errorMsg); } } catch (JsonException) { // Non-JSON or unexpected shape — fall through to success } _logger.LogInformation( "DirectAction [OK] | ActionCode={Code} ContextId={Ctx} Status={Status}", actionCode, contextId, (int)response.StatusCode); return DirectActionResult.Ok(); } } }