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