using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FrameworkBLL.ReportViewer;
using FrameworkDAL.CustomCode.Promotion;
using FrameworkDAL.DTO.ReportViewer;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.DTO.Promotion;
using GB5Shared.Promotion;
using Newtonsoft.Json;
namespace FrameworkBLL.Promotion.Adapters
{
// ============================================================
// ReportViewPromotionAdapter — IPromotableEntity for MREPORTVIEW
// ("REPORTVIEW"). Parent row only in this first pass — MREPORTVIEWFIELDS
// (the per-column display settings) are deliberately NOT bundled yet: each
// field row carries its own REPORTVSFIELDSID/DIMENSIONFIELDID FK
// references, which have exactly the same cross-environment
// ID-portability problem as ReportView's own FKs below, just one level
// deeper. Solving that properly is a follow-up, not attempted here —
// promoting the view's own settings without its field-level formatting is
// still a useful, honest first slice, not a silent partial promise.
//
// ReportView has several raw ID references that do NOT survive a move
// between two separate databases (AutoNumber-assigned IDs mean nothing
// outside the database that assigned them):
// - ReportId (mandatory) — resolved via MREPORT.ReportCode, the natural
// key, both at export (Id -> Code) and import (Code -> local Id).
// Import fails clearly if the destination has no matching Report —
// referential completeness, not a silent broken FK.
// - ReportHeaderId (optional) — resolved via MREPORTHEADER.ReportHeaderCode,
// same natural-key pattern as ReportId, now that ReportHeaderPromotionAdapter
// exists (REPORTHEADER is itself a promotable entity type). Null/absent if
// the view has no header attached (ReportHeaderId at its -1/0 sentinel).
// - TemplateId / SecondTemplateId / PageId / ApplicableMenuIds — still
// optional, still conservatively BLOCKED if set to a real
// (non-sentinel) value — no adapter exists yet for Template/Page/Menu
// to resolve them via natural key the way ReportHeader now can.
// Remapping these is a real, scoped follow-up, not attempted here.
// - ClientIds — almost always tenant-specific data; treated as a
// hardcoded-tenant-literal validation block, not merely an
// ID-portability problem.
//
// IMPORTANT: these risky fields are still carried in the bundle payload
// (under RawFlags) even though ImportPromotedAsync never maps them onto
// the destination row. ValidateForPromotionAsync only sees the bundle
// (never the live DTO) and runs at BOTH export time (fail fast) and
// ingest time (never trust only the origin's copy) — if these fields
// were trimmed away before validation ran, "block if ClientIds is set"
// would never actually fire, since the dangerous value wouldn't be there
// to inspect. Import simply ignores RawFlags — it exists for validation
// to see, not for import to use.
//
// Note: TemplateId/PageId's DTO default (bare 0) doesn't match their own
// DB column default (-1) -- a separate, pre-existing inconsistency this
// adapter works around defensively by treating EITHER 0 or -1 as "unset",
// rather than fixing that mismatch here (out of scope for Promotion).
// ============================================================
public class ReportViewPromotionAdapter : IPromotableEntity
{
public string EntityTypeCode => "REPORTVIEW";
private readonly IReportViewBLL _ReportViewBLL;
private readonly IReportCodeLookupDAL _ReportCodeLookup;
private readonly IReportHeaderCodeLookupDAL _ReportHeaderCodeLookup;
public ReportViewPromotionAdapter(
IReportViewBLL reportViewBLL, IReportCodeLookupDAL reportCodeLookup,
IReportHeaderCodeLookupDAL reportHeaderCodeLookup)
{
_ReportViewBLL = reportViewBLL;
_ReportCodeLookup = reportCodeLookup;
_ReportHeaderCodeLookup = reportHeaderCodeLookup;
}
private class RawFlags
{
public string? ClientIds { get; set; }
public int TemplateId { get; set; }
public int SecondTemplateId { get; set; }
public int PageId { get; set; }
public string? ApplicableMenuIds { get; set; }
}
private class PortablePayload
{
public string ReportCode { get; set; } = string.Empty;
/// Null if the view has no header attached.
public string? ReportHeaderCode { get; set; }
public string ReportViewName { get; set; } = string.Empty;
public byte ReportViewType { get; set; }
public byte ReportViewIsSystemView { get; set; }
public byte ReportViewIsDefaultView { get; set; }
public byte ReportViewPageFormat { get; set; }
public byte ReportViewPageOrientation { get; set; }
public byte ReportViewColumnFreeze { get; set; }
public string ReportViewPivotFormat { get; set; } = string.Empty;
public byte ReportViewIsDataPivot { get; set; }
public string OrderByFields { get; set; } = string.Empty;
public string DynamicFooter { get; set; } = string.Empty;
public string ReportTitle { get; set; } = string.Empty;
public byte ReportViewIsHeaderRequired { get; set; }
public byte IsVisible { get; set; }
public string? ReportViewRemarks { get; set; }
public string? Remarks { get; set; }
public short SortOrder { get; set; }
/// Seen only by ValidateForPromotionAsync — never mapped onto the
/// destination row by ImportPromotedAsync. See the class header comment.
public RawFlags Flags { get; set; } = new();
}
public async Task ExportForPromotionAsync(
int originEntityId, LoginDTO login, CancellationToken ct)
{
var json = await _ReportViewBLL.GetReportView(originEntityId, login, ct).ConfigureAwait(false);
var dto = JsonConvert.DeserializeObject(json)
?? throw new InvalidOperationException($"ReportView {originEntityId} not found.");
var reportCode = await _ReportCodeLookup.GetReportCodeById(dto.ReportId, login, ct).ConfigureAwait(false)
?? throw new InvalidOperationException(
$"ReportView {originEntityId} references ReportId {dto.ReportId}, which has no ReportCode — cannot promote.");
string? reportHeaderCode = null;
if (dto.ReportHeaderId != 0 && dto.ReportHeaderId != -1)
{
reportHeaderCode = await _ReportHeaderCodeLookup.GetReportHeaderCodeById(dto.ReportHeaderId, login, ct)
.ConfigureAwait(false)
?? throw new InvalidOperationException(
$"ReportView {originEntityId} references ReportHeaderId {dto.ReportHeaderId}, " +
"which has no ReportHeaderCode — cannot promote.");
}
var payload = new PortablePayload
{
ReportCode = reportCode,
ReportHeaderCode = reportHeaderCode,
ReportViewName = dto.ReportViewName,
ReportViewType = dto.ReportViewType,
ReportViewIsSystemView = dto.ReportViewIsSystemView,
ReportViewIsDefaultView = dto.ReportViewIsDefaultView,
ReportViewPageFormat = dto.ReportViewPageFormat,
ReportViewPageOrientation = dto.ReportViewPageOrientation,
ReportViewColumnFreeze = dto.ReportViewColumnFreeze,
ReportViewPivotFormat = dto.ReportViewPivotFormat,
ReportViewIsDataPivot = dto.ReportViewIsDataPivot,
OrderByFields = dto.OrderByFields,
DynamicFooter = dto.DynamicFooter,
ReportTitle = dto.ReportTitle,
ReportViewIsHeaderRequired = dto.ReportViewIsHeaderRequired,
IsVisible = dto.IsVisible,
ReportViewRemarks = dto.ReportViewRemarks,
Remarks = dto.Remarks,
SortOrder = dto.SortOrder,
Flags = new RawFlags
{
ClientIds = dto.ClientIds,
TemplateId = dto.TemplateId,
SecondTemplateId = dto.SecondTemplateId,
PageId = dto.PageId,
ApplicableMenuIds = dto.ApplicableMenuIds
}
};
return new PromotionBundleDTO
{
EntityTypeCode = EntityTypeCode,
OriginEntityId = originEntityId,
PayloadJson = System.Text.Json.JsonSerializer.Serialize(payload),
ChildPayloadsJson = new Dictionary() // Fields deliberately not bundled yet
};
}
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("ReportView payload could not be parsed."));
var reasons = new List();
var flags = payload.Flags;
var clientIds = flags.ClientIds?.Trim();
if (!string.IsNullOrEmpty(clientIds) && clientIds != ",-1")
reasons.Add("ReportView.ClientIds references specific client(s) — not promotable.");
if (flags.TemplateId != 0 && flags.TemplateId != -1)
reasons.Add("ReportView.TemplateId is set — template remapping is not yet supported.");
if (flags.SecondTemplateId != 0 && flags.SecondTemplateId != -1)
reasons.Add("ReportView.SecondTemplateId is set — template remapping is not yet supported.");
if (flags.PageId != 0 && flags.PageId != -1)
reasons.Add("ReportView.PageId is set — page remapping is not yet supported.");
if (!string.IsNullOrWhiteSpace(flags.ApplicableMenuIds))
reasons.Add("ReportView.ApplicableMenuIds is set — menu 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("ReportView promotion payload could not be parsed.");
var reportId = await _ReportCodeLookup.GetReportIdByCode(payload.ReportCode, login, ct).ConfigureAwait(false)
?? throw new InvalidOperationException(
$"No Report with ReportCode '{payload.ReportCode}' exists at this destination — " +
"promote the Report itself first.");
var reportHeaderId = -1; // sentinel: no header attached
if (!string.IsNullOrWhiteSpace(payload.ReportHeaderCode))
{
reportHeaderId = await _ReportHeaderCodeLookup.GetReportHeaderIdByCode(payload.ReportHeaderCode, login, ct)
.ConfigureAwait(false)
?? throw new InvalidOperationException(
$"No ReportHeader with ReportHeaderCode '{payload.ReportHeaderCode}' exists at this " +
"destination — promote the ReportHeader itself first.");
}
// payload.Flags is deliberately NOT read here — see the class header comment.
// TemplateId/PageId are explicitly stamped to their real "unset" sentinel (-1, which
// has a genuine dummy MFILE row) rather than left at ReportViewDTO's own bare-0
// default — MREPORTVIEW.TEMPLATEID/PAGEID are NOT NULL with FKs to MFILE, which has
// no FILEID=0 row, so leaving them at 0 throws a real FK violation on every import.
// Found live 2026-08-21 verifying the first-ever real Promotion Import execution.
var dto = new ReportViewDTO
{
ReportViewId = existingDestinationId ?? 0,
ReportId = reportId,
ReportHeaderId = reportHeaderId,
TemplateId = -1,
PageId = -1,
ReportViewName = payload.ReportViewName,
ReportViewType = payload.ReportViewType,
ReportViewIsSystemView = payload.ReportViewIsSystemView,
ReportViewIsDefaultView = payload.ReportViewIsDefaultView,
ReportViewPageFormat = payload.ReportViewPageFormat,
ReportViewPageOrientation = payload.ReportViewPageOrientation,
ReportViewColumnFreeze = payload.ReportViewColumnFreeze,
ReportViewPivotFormat = payload.ReportViewPivotFormat,
ReportViewIsDataPivot = payload.ReportViewIsDataPivot,
OrderByFields = payload.OrderByFields,
DynamicFooter = payload.DynamicFooter,
ReportTitle = payload.ReportTitle,
ReportViewIsHeaderRequired = payload.ReportViewIsHeaderRequired,
IsVisible = payload.IsVisible,
ReportViewRemarks = payload.ReportViewRemarks,
Remarks = payload.Remarks,
SortOrder = payload.SortOrder,
ReportViewSourceType = destinationSourceType
};
await _ReportViewBLL.SaveReportView(dto, login, ct).ConfigureAwait(false);
return dto.ReportViewId; // mutated in place by SaveReportView when isNew
}
}
}