using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.MetaReport; using FrameworkDAL.CustomCode.Promotion; using FrameworkDAL.DTO.MetaReport; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Promotion; using GB5Shared.Promotion; namespace FrameworkBLL.Promotion.Adapters { // ============================================================ // MetaReportPromotionAdapter — IPromotableEntity for MMETAREPORT // ("METAREPORT"). No SourceType column/concept exists on this aggregate at // all (confirmed across MetaReportDTO/Variant/Aspect/Option) — like // DrillDown, destinationSourceType is accepted but unused. // // Cross-environment references: each Variant optionally carries a // ReportId (resolved via the existing IReportCodeLookupDAL, already built // for ReportView/DrillDown) and/or a MenuId (new IMenuCodeLookupDAL). // Both are optional per variant — 0 means "not set" (GB5's real // AutoNumber-minted IDs are never 0). A set-but-unresolvable reference // fails fast at export (can't build a coherent bundle) or at import // (referential completeness — promote the Report/Menu itself first), // same pattern as ReportView's ReportId/ReportHeaderId. // // Option.ApplicableVariantIds is an INTRA-BUNDLE reference (which of this // same MetaReport's own Variants an Option applies to), not a // cross-environment one — resolved to VariantName (assumed unique within // one MetaReport; no VariantCode column exists) at export, and back to // real Variant IDs at import. // // TWO-PHASE IMPORT is required for the intra-bundle remap to work at all: // MetaReportBLL.AssignIds (read directly, not guessed) assigns real // AutoNumber IDs to every new Variant/Aspect/Option in place, but never // touches Option.ApplicableVariantIds — so a brand-new Option applying to // a brand-new Variant can't have the right ApplicableVariantIds until // AFTER a save has assigned that Variant its real ID. Phase 1 saves with // ApplicableVariantIds empty (establishes real/matched IDs for // everything); Phase 2 fills in the now-resolvable ApplicableVariantIds // and saves again (a pure update at this point — every ID already // real/existing, AssignIds mints nothing new). Predicting AssignIds' // reservation math instead of doing this was considered and rejected — // it would silently break if that method's internals ever changed. // // Update-merge: when existingDestinationId is set, the existing // destination aggregate is fetched first and its Variants/Aspects/Options // matched by name (VariantName; AspectName; (AspectName,Label) for // Options) so a re-promotion updates in place rather than duplicating // children — same "resend the full desired state" diff-save convention // SaveMetaReportDefinition/AssignIds already implement; anything no // longer present in the incoming bundle is naturally diff-deleted by the // DAL, matching the codebase's established convention (MenuGroupDetailDTO // et al.) referenced directly in MetaReportBLL's own comments. // ============================================================ public class MetaReportPromotionAdapter : IPromotableEntity { public string EntityTypeCode => "METAREPORT"; private readonly IMetaReportBLL _MetaReportBLL; private readonly IReportCodeLookupDAL _ReportCodeLookup; private readonly IMenuCodeLookupDAL _MenuCodeLookup; public MetaReportPromotionAdapter( IMetaReportBLL metaReportBLL, IReportCodeLookupDAL reportCodeLookup, IMenuCodeLookupDAL menuCodeLookup) { _MetaReportBLL = metaReportBLL; _ReportCodeLookup = reportCodeLookup; _MenuCodeLookup = menuCodeLookup; } private class VariantPayload { public string VariantName { get; set; } = string.Empty; public string? ReportCode { get; set; } public string? MenuCode { get; set; } public short SortOrder { get; set; } } private class OptionPayload { public string Label { get; set; } = string.Empty; public string Value { get; set; } = string.Empty; public byte ValueType { get; set; } public short SortOrder { get; set; } public List ApplicableVariantNames { get; set; } = new(); } private class AspectPayload { public string AspectName { get; set; } = string.Empty; public string AspectType { get; set; } = "radio"; public bool IsMandatory { get; set; } public short DisplayOrder { get; set; } public string? TabGroup { get; set; } public string? Tooltip { get; set; } public List Options { get; set; } = new(); } private class PortablePayload { public string MetaReportName { get; set; } = string.Empty; public string? Description { get; set; } public List Variants { get; set; } = new(); public List Aspects { get; set; } = new(); } public async Task ExportForPromotionAsync( int originEntityId, LoginDTO login, CancellationToken ct) { var dto = await _MetaReportBLL.GetMetaReport(originEntityId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"MetaReport {originEntityId} not found."); var variantNameById = new Dictionary(); var variantPayloads = new List(dto.Variants.Count); foreach (var variant in dto.Variants) { string? reportCode = null; if (variant.ReportId != 0) reportCode = await _ReportCodeLookup.GetReportCodeById(variant.ReportId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"MetaReport variant '{variant.VariantName}' references ReportId {variant.ReportId}, " + "which has no ReportCode — cannot promote."); string? menuCode = null; if (variant.MenuId != 0) menuCode = await _MenuCodeLookup.GetMenuCodeById(variant.MenuId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"MetaReport variant '{variant.VariantName}' references MenuId {variant.MenuId}, " + "which has no MenuCode — cannot promote."); variantNameById[variant.VariantId] = variant.VariantName; variantPayloads.Add(new VariantPayload { VariantName = variant.VariantName, ReportCode = reportCode, MenuCode = menuCode, SortOrder = variant.SortOrder }); } var aspectPayloads = dto.Aspects.Select(aspect => new AspectPayload { AspectName = aspect.AspectName, AspectType = aspect.AspectType, IsMandatory = aspect.IsMandatory, DisplayOrder = aspect.DisplayOrder, TabGroup = aspect.TabGroup, Tooltip = aspect.Tooltip, Options = aspect.Options.Select(option => new OptionPayload { Label = option.Label, Value = option.Value, ValueType = option.ValueType, SortOrder = option.SortOrder, ApplicableVariantNames = option.ApplicableVariantIds .Select(id => variantNameById.TryGetValue(id, out var name) ? name : null) .Where(name => name is not null) .Select(name => name!) .ToList() }).ToList() }).ToList(); var payload = new PortablePayload { MetaReportName = dto.MetaReportName, Description = dto.Description, Variants = variantPayloads, Aspects = aspectPayloads }; return new PromotionBundleDTO { EntityTypeCode = EntityTypeCode, OriginEntityId = originEntityId, PayloadJson = System.Text.Json.JsonSerializer.Serialize(payload) }; } public Task ValidateForPromotionAsync( PromotionBundleDTO bundle, CancellationToken ct) { // No tenant-literal or unsupported-reference concern found on this aggregate (no // ClientId anywhere on MetaReport/Variant/Aspect/Option) — nothing to block here. var payload = System.Text.Json.JsonSerializer.Deserialize(bundle.PayloadJson); var result = payload is null ? PromotionValidationResultDTO.Block("MetaReport payload could not be parsed.") : PromotionValidationResultDTO.Ok(); return Task.FromResult(result); } public async Task ImportPromotedAsync( PromotionBundleDTO bundle, byte destinationSourceType, int? existingDestinationId, LoginDTO login, CancellationToken ct) { // destinationSourceType is unused — MetaReport has no SourceType concept. var payload = System.Text.Json.JsonSerializer.Deserialize(bundle.PayloadJson) ?? throw new InvalidOperationException("MetaReport promotion payload could not be parsed."); var resolvedVariants = new List<(VariantPayload Payload, int ReportId, int MenuId)>(); foreach (var v in payload.Variants) { int reportId = 0, menuId = 0; if (!string.IsNullOrWhiteSpace(v.ReportCode)) reportId = await _ReportCodeLookup.GetReportIdByCode(v.ReportCode, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"No Report with ReportCode '{v.ReportCode}' exists at this destination."); if (!string.IsNullOrWhiteSpace(v.MenuCode)) menuId = await _MenuCodeLookup.GetMenuIdByCode(v.MenuCode, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException( $"No Menu with MenuCode '{v.MenuCode}' exists at this destination."); resolvedVariants.Add((v, reportId, menuId)); } // Update-merge: match existing children by name so a re-promotion updates in place. var existingVariantIdByName = new Dictionary(); var existingAspectIdByName = new Dictionary(); var existingOptionIdByAspectAndLabel = new Dictionary<(string, string), int>(); if (existingDestinationId.HasValue) { var existing = await _MetaReportBLL.GetMetaReport(existingDestinationId.Value, login, ct).ConfigureAwait(false); if (existing is not null) { foreach (var v in existing.Variants) existingVariantIdByName[v.VariantName] = v.VariantId; foreach (var a in existing.Aspects) { existingAspectIdByName[a.AspectName] = a.AspectId; foreach (var o in a.Options) existingOptionIdByAspectAndLabel[(a.AspectName, o.Label)] = o.OptionId; } } } var dto = new MetaReportDTO { MetaReportId = existingDestinationId ?? 0, MetaReportName = payload.MetaReportName, Description = payload.Description, Variants = resolvedVariants.Select(rv => new MetaReportVariantDTO { VariantId = existingVariantIdByName.TryGetValue(rv.Payload.VariantName, out var vid) ? vid : 0, VariantName = rv.Payload.VariantName, ReportId = rv.ReportId, MenuId = rv.MenuId, SortOrder = rv.Payload.SortOrder }).ToList(), Aspects = payload.Aspects.Select(a => new MetaReportAspectDTO { AspectId = existingAspectIdByName.TryGetValue(a.AspectName, out var aid) ? aid : 0, AspectName = a.AspectName, AspectType = a.AspectType, IsMandatory = a.IsMandatory, DisplayOrder = a.DisplayOrder, TabGroup = a.TabGroup, Tooltip = a.Tooltip, Options = a.Options.Select(o => new MetaReportAspectOptionDTO { OptionId = existingOptionIdByAspectAndLabel.TryGetValue((a.AspectName, o.Label), out var oid) ? oid : 0, Label = o.Label, Value = o.Value, ValueType = o.ValueType, SortOrder = o.SortOrder, ApplicableVariantIds = new List() // Phase 1 — filled in after real IDs exist }).ToList() }).ToList() }; // Phase 1: establish real/matched IDs for every Variant/Aspect/Option. await _MetaReportBLL.SaveMetaReportDefinition(dto, login, ct).ConfigureAwait(false); // Phase 2: now that every Variant has a real ID (dto mutated in place by Phase 1), // resolve each Option's ApplicableVariantNames and save again — a pure update, since // every ID is now real/existing and AssignIds mints nothing new this time. var realVariantIdByName = dto.Variants.ToDictionary(v => v.VariantName, v => v.VariantId); foreach (var aspectDto in dto.Aspects) { var aspectPayload = payload.Aspects.First(a => a.AspectName == aspectDto.AspectName); foreach (var optionDto in aspectDto.Options) { var optionPayload = aspectPayload.Options.First(o => o.Label == optionDto.Label); optionDto.ApplicableVariantIds = optionPayload.ApplicableVariantNames.Select(name => realVariantIdByName.TryGetValue(name, out var id) ? id : throw new InvalidOperationException( $"MetaReport option '{optionDto.Label}' applies to variant '{name}', " + "which was not found among this MetaReport's own promoted variants.") ).ToList(); } } await _MetaReportBLL.SaveMetaReportDefinition(dto, login, ct).ConfigureAwait(false); return dto.MetaReportId; } } }