using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using GB5Shared.Auth.Jwt;
using GB5Shared.Telemetry;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace RecruitmentBLL.Integration
{
///
public class DxpPartyIntegrationService : IDxpPartyIntegrationService
{
// Must equal DXPBLL.Auth.DXPJwtService's private SigningKeyVaultPath constant — a token
// signed at any other path would never validate under DXP's own JwtBearer scheme.
private const string SigningKeyVaultPath = "dxp/jwt-signing-key";
// Sentinel "system" caller — matches DXPDAL.Common.DXPSystemContext.GetSystemLogin's own
// default parameter value. GetSystemLogin never validates this against MDXPUSER, it only
// embeds it into the resulting LoginDTO.UserId, so no real DXP user account is required.
private const int SystemDxpUserId = -1;
// Same "trailing integer token" convention DxpAuthIntegrationService.TrailingIdRegex
// already relies on for SuccessResponse.SaveSuccessMessage-style strings.
private static readonly Regex TrailingIdRegex = new(@"(-?\d+)\s*$", RegexOptions.Compiled);
private readonly IHttpClientFactory _HttpClientFactory;
private readonly IJwtAccessTokenIssuer _JwtIssuer;
private readonly IConfiguration _Configuration;
private readonly ILogger _Logger;
public DxpPartyIntegrationService(
IHttpClientFactory httpClientFactory,
IJwtAccessTokenIssuer jwtIssuer,
IConfiguration configuration,
ILogger logger)
{
_HttpClientFactory = httpClientFactory;
_JwtIssuer = jwtIssuer;
_Configuration = configuration;
_Logger = logger;
}
public async Task SavePartyAsync(int dxpPartyId, string legalName, byte partyTypeCode, CancellationToken ct)
{
try
{
GB5Trace.Step("dxp-save-party", new { dxpPartyId, legalName, partyTypeCode });
var token = await MintSystemTokenAsync(ct).ConfigureAwait(false);
var client = _HttpClientFactory.CreateClient("DXPModule");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// PartyDTO shape (DXPDAL.Party.PartyDTO) posted at top level — SaveParty is a
// DXPBaseEndpoint, no Login header/wrapper (caller identity comes
// from the Bearer token, not the body). DxpPartyId=0 -> DXP creates a new Party
// and assigns the AutoNumber id; nonzero -> DXP updates that Party's LegalName.
var payload = new { DxpPartyId = dxpPartyId, LegalName = legalName, PartyTypeCode = partyTypeCode };
var response = await client.PostAsJsonAsync("DXP/Party/SaveParty", payload, ct).ConfigureAwait(false);
var raw = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var error = ExtractErrorBody(raw) ?? $"DXP returned {(int)response.StatusCode}";
_Logger.LogWarning("DXP SaveParty failed for {LegalName}: {StatusCode} {Error}", legalName, response.StatusCode, error);
return new DxpPartyOutcome { Success = false, Error = error };
}
var body = ExtractBody(raw);
var resultPartyId = ExtractTrailingId(body) ?? (dxpPartyId != 0 ? dxpPartyId : -1);
return new DxpPartyOutcome { Success = true, DxpPartyId = resultPartyId };
}
catch (Exception ex)
{
GB5Trace.MarkFailed("dxp-save-party-failed", ex);
_Logger.LogError(ex, "DXP SavePartyAsync failed for {LegalName}", legalName);
return new DxpPartyOutcome { Success = false, Error = ex.Message };
}
}
// ── Helpers ──────────────────────────────────────────────────────────────
private async Task MintSystemTokenAsync(CancellationToken ct)
{
var accessTokenMinutes = _Configuration.GetValue("DXPJwt:AccessTokenMinutes", 15);
var issuer = _Configuration["DXPJwt:Issuer"] ?? "GB5-DXP";
var audience = _Configuration["DXPJwt:Audience"] ?? "GB5-DXP-Portal";
var claims = new DxpSystemAccessTokenClaims { DxpUserId = SystemDxpUserId };
var issued = await _JwtIssuer
.IssueAsync(claims, SigningKeyVaultPath, issuer, audience, accessTokenMinutes, ct)
.ConfigureAwait(false);
return issued.AccessToken;
}
// Same permissive-parse idiom as DxpAuthIntegrationService's Extract* helpers.
private static string? ExtractBody(string raw)
{
try
{
using var doc = JsonDocument.Parse(raw);
if (doc.RootElement.TryGetProperty("Body", out var bodyElement))
return bodyElement.ValueKind == JsonValueKind.String ? bodyElement.GetString() : bodyElement.GetRawText();
return null;
}
catch
{
return null;
}
}
private static string? ExtractErrorBody(string raw)
{
try
{
using var doc = JsonDocument.Parse(raw);
return doc.RootElement.TryGetProperty("ErrorBody", out var errorElement)
? errorElement.GetString()
: null;
}
catch
{
return null;
}
}
private static int? ExtractTrailingId(string? body)
{
if (body is null) return null;
var match = TrailingIdRegex.Match(body);
return match.Success && int.TryParse(match.Groups[1].Value, out var id) ? id : null;
}
}
}