using System;
using System.Threading;
using System.Threading.Tasks;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.DTO.Promotion;
using GB5Shared.Promotion;
using Newtonsoft.Json;
using WiBLL.WiMaster;
using WiDAL.CustomCode.Promotion;
using WiDAL.DTOs;
namespace WiBLL.Promotion
{
// ============================================================
// WiHeaderPromotionAdapter — IPromotableEntity for MWI ("WORKINSTRUCTION"), the actual Work
// Instruction document. Parent row only — Sections/Steps/Contents/SkillRequirements/
// CompetencyRequirements are not bundled (same "children deferred as a follow-up" precedent as
// ReportView's Fields/Dashboard's Pages).
//
// Load-bearing, not just a scope-reduction shortcut: WiMasterBLL.SaveWiFull's own DAL
// implementation (checked directly, not assumed) unconditionally soft-deletes every existing
// Section/Step/Content for this WiId before inserting whatever req.Sections contains — the
// exact same destructive-update-on-children shape already found and fixed for TMS's
// TrainingProgrammeBLL.SaveTrainingProgramme. A parent-only adapter that constructed a bare
// WiSaveRequestDTO on every re-promotion would silently wipe a destination WI's real content on
// every update.
//
// Fixed the same way as TMS Programme: on update (existingDestinationId resolves), Import first
// reads the destination's own current full detail via GetWiFullDetail — its returned JSON
// shape (Header/Sections/SkillRequirements/CompetencyRequirements) matches WiSaveRequestDTO's
// own properties exactly, confirmed by reading WiMasterDAL.GetWiFullDetail directly — then
// overlays only the promotable header fields onto it in place, leaving Sections/
// SkillRequirements/CompetencyRequirements exactly as they already are at the destination. A
// fresh insert has nothing to preserve, so it starts from an empty request.
//
// WiProcessId/WiSubProcessId are resolved via natural-key lookups — the destination must
// already have a WiProcess/WiSubProcess with matching codes (same "promote the parent hierarchy
// first" precedent as PortletType before Portlet). ApprovedById is a raw UserId reference with
// no code-based lookup available — conservatively blocked (RawFlags-carried for validation
// only, never mapped at import, same pattern as Portlet's own blocked cross-references).
// ============================================================
public class WiHeaderPromotionAdapter : IPromotableEntity
{
public string EntityTypeCode => "WORKINSTRUCTION";
private readonly IWiMasterBLL _WiMasterBLL;
private readonly IWiProcessCodeLookupDAL _WiProcessCodeLookup;
private readonly IWiSubProcessCodeLookupDAL _WiSubProcessCodeLookup;
private readonly IWiHeaderCodeLookupDAL _WiHeaderCodeLookup;
public WiHeaderPromotionAdapter(
IWiMasterBLL wiMasterBLL, IWiProcessCodeLookupDAL wiProcessCodeLookup,
IWiSubProcessCodeLookupDAL wiSubProcessCodeLookup, IWiHeaderCodeLookupDAL wiHeaderCodeLookup)
{
_WiMasterBLL = wiMasterBLL;
_WiProcessCodeLookup = wiProcessCodeLookup;
_WiSubProcessCodeLookup = wiSubProcessCodeLookup;
_WiHeaderCodeLookup = wiHeaderCodeLookup;
}
private class PortablePayload
{
public string WiCode { get; set; } = string.Empty;
public string WiTitle { get; set; } = string.Empty;
public string WiProcessCode { get; set; } = string.Empty;
public string WiSubProcessCode { get; set; } = string.Empty;
public byte ActivityType { get; set; }
public string? ActivitySubType { get; set; }
public string WiVersion { get; set; } = "1.0";
public byte WiStatus { get; set; }
public byte DisplayContext { get; set; }
public byte StepAdvanceMode { get; set; }
public decimal StdCycleTimeMins { get; set; }
public DateTime? EffectiveDate { get; set; }
public DateTime? ReviewDueDate { get; set; }
public string? Tags { get; set; }
public string? WiDescription { get; set; }
public int IsActive { get; set; }
public byte Status { get; set; } = 1;
public short SortOrder { get; set; } = 9999;
/// Seen only by ValidateForPromotionAsync — ImportPromotedAsync never maps
/// this onto the destination row. See the class header comment.
public RawFlags Flags { get; set; } = new();
}
private class RawFlags
{
public int ApprovedById { get; set; }
}
public async Task ExportForPromotionAsync(
int originEntityId, LoginDTO login, CancellationToken ct)
{
var json = await _WiMasterBLL.GetWiById(originEntityId, login, ct).ConfigureAwait(false);
var dto = JsonConvert.DeserializeObject(json)
?? throw new InvalidOperationException($"WorkInstruction {originEntityId} not found.");
var wiProcessCode = await _WiProcessCodeLookup.GetWiProcessCodeById(dto.WiProcessId, login, ct)
.ConfigureAwait(false)
?? throw new InvalidOperationException(
$"WorkInstruction {originEntityId} references WiProcessId {dto.WiProcessId}, which has no WiProcessCode — cannot promote.");
var wiSubProcessCode = await _WiSubProcessCodeLookup.GetWiSubProcessCodeById(dto.WiSubProcessId, login, ct)
.ConfigureAwait(false)
?? throw new InvalidOperationException(
$"WorkInstruction {originEntityId} references WiSubProcessId {dto.WiSubProcessId}, which has no WiSubProcessCode — cannot promote.");
var payload = new PortablePayload
{
WiCode = dto.WiCode,
WiTitle = dto.WiTitle,
WiProcessCode = wiProcessCode,
WiSubProcessCode = wiSubProcessCode,
ActivityType = (byte)dto.ActivityType,
ActivitySubType = dto.ActivitySubType,
WiVersion = dto.WiVersion,
WiStatus = (byte)dto.WiStatus,
DisplayContext = (byte)dto.DisplayContext,
StepAdvanceMode = (byte)dto.StepAdvanceMode,
StdCycleTimeMins = dto.StdCycleTimeMins,
EffectiveDate = dto.EffectiveDate,
ReviewDueDate = dto.ReviewDueDate,
Tags = dto.Tags,
WiDescription = dto.WiDescription,
IsActive = dto.IsActive,
Status = dto.Status,
SortOrder = dto.SortOrder,
Flags = new RawFlags { ApprovedById = dto.ApprovedById }
};
return new PromotionBundleDTO
{
EntityTypeCode = EntityTypeCode,
OriginEntityId = originEntityId,
PayloadJson = System.Text.Json.JsonSerializer.Serialize(payload)
};
}
public Task ValidateForPromotionAsync(
PromotionBundleDTO bundle, CancellationToken ct)
{
var payload = System.Text.Json.JsonSerializer.Deserialize(bundle.PayloadJson);
if (payload is null)
return Task.FromResult(PromotionValidationResultDTO.Block("WorkInstruction payload could not be parsed."));
var reasons = new System.Collections.Generic.List();
if (string.IsNullOrWhiteSpace(payload.WiCode))
reasons.Add("WorkInstruction.WiCode is required to promote.");
if (string.IsNullOrWhiteSpace(payload.WiProcessCode))
reasons.Add("WorkInstruction.WiProcessId has no resolvable WiProcessCode — cannot promote.");
if (string.IsNullOrWhiteSpace(payload.WiSubProcessCode))
reasons.Add("WorkInstruction.WiSubProcessId has no resolvable WiSubProcessCode — cannot promote.");
if (payload.Flags.ApprovedById != 0)
reasons.Add("WorkInstruction.ApprovedById is set — approver-identity remapping is not yet supported.");
var result = reasons.Count == 0
? PromotionValidationResultDTO.Ok()
: new PromotionValidationResultDTO { IsPromotable = false, BlockingReasons = reasons };
return Task.FromResult(result);
}
public async Task ImportPromotedAsync(
PromotionBundleDTO bundle, byte destinationSourceType, int? existingDestinationId,
LoginDTO login, CancellationToken ct)
{
var payload = System.Text.Json.JsonSerializer.Deserialize(bundle.PayloadJson)
?? throw new InvalidOperationException("WorkInstruction promotion payload could not be parsed.");
var wiProcessId = await _WiProcessCodeLookup.GetWiProcessIdByCode(payload.WiProcessCode, login, ct)
.ConfigureAwait(false)
?? throw new InvalidOperationException(
$"No WiProcess with WiProcessCode '{payload.WiProcessCode}' exists at this destination.");
var wiSubProcessId = await _WiSubProcessCodeLookup.GetWiSubProcessIdByCode(payload.WiSubProcessCode, login, ct)
.ConfigureAwait(false)
?? throw new InvalidOperationException(
$"No WiSubProcess with WiSubProcessCode '{payload.WiSubProcessCode}' exists at this destination.");
var resolvedId = existingDestinationId
?? await _WiHeaderCodeLookup.GetWiIdByCode(payload.WiCode, login, ct).ConfigureAwait(false)
?? 0;
// Update: start from the destination's OWN current full aggregate (its real
// Sections/Steps/Contents/SkillRequirements/CompetencyRequirements included) so those
// child collections survive SaveWiFull's delete-and-rebuild update path untouched — see
// the class header comment. Insert: nothing exists yet to preserve.
WiSaveRequestDTO req;
if (resolvedId != 0)
{
var existingJson = await _WiMasterBLL.GetWiFullDetail(resolvedId, login, ct).ConfigureAwait(false);
req = JsonConvert.DeserializeObject(existingJson!)
?? throw new InvalidOperationException(
$"WorkInstruction {resolvedId} could not be read at the destination for update.");
}
else
{
req = new WiSaveRequestDTO();
}
req.Header.WiId = resolvedId;
req.Header.WiCode = payload.WiCode;
req.Header.WiTitle = payload.WiTitle;
req.Header.WiProcessId = wiProcessId;
req.Header.WiSubProcessId = wiSubProcessId;
req.Header.ActivityType = (WiDAL.DTOs.ActivityType)payload.ActivityType;
req.Header.ActivitySubType = payload.ActivitySubType;
req.Header.WiVersion = payload.WiVersion;
req.Header.WiStatus = (WiDAL.DTOs.WiStatus)payload.WiStatus;
req.Header.DisplayContext = (WiDAL.DTOs.DisplayContext)payload.DisplayContext;
req.Header.StepAdvanceMode = (WiDAL.DTOs.StepAdvanceMode)payload.StepAdvanceMode;
req.Header.StdCycleTimeMins = payload.StdCycleTimeMins;
req.Header.EffectiveDate = payload.EffectiveDate;
req.Header.ReviewDueDate = payload.ReviewDueDate;
req.Header.Tags = payload.Tags;
req.Header.WiDescription = payload.WiDescription;
req.Header.IsActive = payload.IsActive;
req.Header.Status = payload.Status;
req.Header.SortOrder = payload.SortOrder;
req.Header.SourceType = destinationSourceType;
req.Header.ApprovedById = 0; // never mapped — see class header comment
req.Header.ApprovedOn = null;
await _WiMasterBLL.SaveWiFull(req, login, ct).ConfigureAwait(false);
return req.Header.WiId; // mutated in place by SaveWiFull when isNew
}
}
}