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();
}
}
}