using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.DTO.WorkFlow;
using GB5Shared.WorkFlow.Wip;
using Microsoft.Extensions.Logging;
namespace FrameworkBLL.WorkFlow
{
///
/// Dispatches approved WIP records via an internal HTTP POST to the original save endpoint.
///
/// Protocol:
/// POST {WipDispatchInfo.ApiEndpoint}
/// Header: X-Wip-Approval: {WipId}
/// Header: Login: {serialized approverLogin JSON}
/// Body (JSON): the body format stored in TWORKFLOWWIP.DATAJSON (object or array)
///
/// Body format is determined at WIP creation time by EventHandler, which reads
/// login.IsBodyArray (set by BaseEndpoint via reflection on TRequest) and stores
/// DATAJSON as "[{...}]" for List<T> endpoints or "{...}" for single-T endpoints.
/// The dispatcher replays the body as-is — no guesswork or retry needed.
///
/// The target endpoint detects X-Wip-Approval → sets login.WipApprovalId → EventHandler
/// injects __WipApproval=true → CheckWorkFlowApplicability returns NONE → entity saves directly.
///
/// __Wip* flags in DATAJSON are consumed and removed before sending — never reach the endpoint.
///
public class HttpWipApprovalDispatcher : IWipApprovalDispatcher
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
public HttpWipApprovalDispatcher(
IHttpClientFactory httpClientFactory,
ILogger logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public async Task DispatchAsync(
WipDispatchInfo dispatch,
LoginDTO approverLogin,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(dispatch.ApiEndpoint))
{
_logger.LogWarning(
"WIP dispatch skipped for WipId={WipId}: ApiEndpoint is empty.",
dispatch.WipId);
return;
}
try
{
// ── Build body — DATAJSON already carries the correct format ──────────
// "{...}" → endpoint expects single T (IsBodyArray was false)
// "[{...}]" → endpoint expects List (IsBodyArray was true)
// __Wip* flags are consumed and removed; PK reset for new entities.
var bodyNode = BuildMergedBody(dispatch.DataJson);
string bodyJson = bodyNode.ToJsonString();
bool isArray = bodyNode is JsonArray;
// Use the original submitter's login stored in TWORKFLOWWIP.LOGINJSON so the
// entity saves under the correct session context (WorkOUId, BranchId, UserId).
// Fall back to the approver's login for WIP rows created before this column existed.
string loginJson = !string.IsNullOrWhiteSpace(dispatch.LoginJson)
? dispatch.LoginJson
: JsonSerializer.Serialize(approverLogin);
var client = _httpClientFactory.CreateClient("wip-replay");
var request = new HttpRequestMessage(HttpMethod.Post, dispatch.ApiEndpoint);
request.Headers.Add("X-Wip-Approval", dispatch.WipId.ToString());
request.Headers.Add("Login", loginJson);
request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json");
_logger.LogInformation(
"Dispatching WIP callback: WipId={WipId} Endpoint={Endpoint} IsArrayBody={IsArray}",
dispatch.WipId, dispatch.ApiEndpoint, isArray);
var response = await client.SendAsync(request, ct);
if (!response.IsSuccessStatusCode)
{
string responseBody = await response.Content.ReadAsStringAsync(ct);
_logger.LogError(
"WIP dispatch failed for WipId={WipId}: HTTP {Status}. " +
"Endpoint={Endpoint} IsArrayBody={IsArray} " +
"RequestBodyPreview={Preview} Response={Body}",
dispatch.WipId, (int)response.StatusCode,
dispatch.ApiEndpoint, isArray,
bodyJson.Length > 300 ? bodyJson[..300] + "…" : bodyJson,
responseBody);
}
else
{
_logger.LogInformation(
"WIP dispatch succeeded for WipId={WipId} IsArrayBody={IsArray}.",
dispatch.WipId, isArray);
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"WIP dispatch threw an exception for WipId={WipId} Endpoint={Endpoint}.",
dispatch.WipId, dispatch.ApiEndpoint);
// Do not rethrow — dispatch failure must not roll back the already-committed workflow.
}
}
///
/// Builds the request body from DATAJSON.
///
/// DATAJSON can be in two formats stored by EventHandler at WIP creation:
/// "{...}" — single DTO (endpoint uses T body)
/// "[{...}]" — single-element array (endpoint uses List<T> body)
///
/// Steps:
/// 1. Parse and detect format; extract the inner DTO object.
/// 2. Remove internal __Wip* flags (never sent to endpoint).
/// 3. Reset PK to 0 for new entities (__WipIsNew + __WipPkField).
/// 4. Patch Status = 1 (Approved).
/// 5. Return JsonObject or JsonArray matching the original format.
///
private static JsonNode BuildMergedBody(string? dataJson)
{
var body = new JsonObject();
bool wasArray = false;
// ── Parse DATAJSON — handle both "{...}" and "[{...}]" formats ──────────
if (!string.IsNullOrWhiteSpace(dataJson))
{
try
{
var dataNode = JsonNode.Parse(dataJson);
if (dataNode is JsonArray dataArr)
{
// Stored as array by EventHandler when login.IsBodyArray = true
wasArray = true;
if (dataArr.Count > 0 && dataArr[0] is JsonObject firstObj)
{
foreach (var kv in firstObj)
body[kv.Key] = kv.Value?.DeepClone();
}
}
else if (dataNode is JsonObject dataObj)
{
foreach (var kv in dataObj)
body[kv.Key] = kv.Value?.DeepClone();
}
}
catch
{
// Unparseable DataJson — proceed with empty DTO fields
}
}
// ── Resolve new-vs-update intent stored by EventHandler at WIP creation ─
// __WipIsNew=true → new entity: reset PK to 0 so module BLL takes INSERT path
// __WipIsNew=false → existing entity: PK stays intact, module BLL takes UPDATE path
bool wipIsNew = false;
if (body.TryGetPropertyValue("__WipIsNew", out var wipIsNewNode))
{
wipIsNew = wipIsNewNode?.GetValue() ?? false;
body.Remove("__WipIsNew");
}
if (body.TryGetPropertyValue("__WipPkField", out var pkFieldNode))
{
string? pkField = pkFieldNode?.GetValue();
body.Remove("__WipPkField");
if (wipIsNew && !string.IsNullOrEmpty(pkField) && body.ContainsKey(pkField))
body[pkField] = 0;
}
// ── Patch record STATUS = 1 (Approved) ───────────────────────────────────
bool statusPatched = false;
foreach (var key in new[] { "Status", "status", "STATUS" })
{
if (body.ContainsKey(key))
{
body[key] = 1;
statusPatched = true;
break;
}
}
// Fallback: STATUS aliased as {EntityName}Status (e.g. TaskStatus, AccountStatus)
if (!statusPatched)
{
foreach (var key in body.Select(kv => kv.Key).ToList())
{
if (key.EndsWith("Status", StringComparison.Ordinal)
&& Regex.IsMatch(key, @"^[A-Z][a-z]+Status$"))
{
body[key] = 1;
statusPatched = true;
break;
}
}
}
if (!statusPatched)
body["Status"] = 1;
// ── Restore original body format ─────────────────────────────────────────
// wasArray = true → DATAJSON was "[{...}]" → endpoint expects List → return [{...}]
// wasArray = false → DATAJSON was "{...}" → endpoint expects T → return {...}
return wasArray ? (JsonNode)new JsonArray { body } : body;
}
}
}