using GB5Shared.DTO.Framework.Login;
using GB5Shared.DTO.Report;
using GB5Shared.Export;
namespace GB5Shared.Export.NativePdf
{
///
/// Native (PdfSharpCore, no browser) PDF export — an additional, explicit option
/// (ReportFormat.PdfNative) alongside the existing Puppeteer-based PDF pipeline, not a
/// replacement for it. See Phase 11/12 of the ReportViewer plan for the spike that justified this
/// and the production design. Structured the same way as —
/// buffer rows, reshape to title-keyed dictionaries, build the multi-level group tree via
/// , drive a dedicated renderer.
///
public sealed class NativePdfExport : INativePdfExport
{
public async Task Export(
IAsyncEnumerable> records,
IAsyncEnumerable mainHeaders,
IAsyncEnumerable columnTitles,
IAsyncEnumerable columnFieldNames,
Stream outputStream,
LoginDTO login,
byte[]? logoBytes,
CancellationToken cancellationToken = default,
List? reportViewFields = null,
byte reportViewPageFormat = 4,
byte reportViewPageOrientation = 0)
{
reportViewFields ??= new List();
var headers = new List();
await foreach (var h in mainHeaders.WithCancellation(cancellationToken))
headers.Add(h);
var titles = new List();
await foreach (var t in columnTitles.WithCancellation(cancellationToken))
titles.Add(t);
var fieldNames = new List();
await foreach (var f in columnFieldNames.WithCancellation(cancellationToken))
fieldNames.Add(f);
// A group/subtotal/grandtotal/merge key field can be hidden from the visible column list
// (ISDISPLAY) yet still needed BY TITLE below (GroupTreeBuilder/ComputeAggregateRow) — same
// fix and reasoning as ExcelExport.cs's identical block. reportViewFields must be the FULL
// field list here (not display-filtered) for this to find anything — see the caller.
var structuralOnlyFields = reportViewFields
.Where(f => (f.ReportViewFieldsIsGroupcolumn == 0 || f.ReportViewFieldsIsSubTotal == 0
|| f.ReportViewFieldsIsGrandTotal == 0 || f.ReportViewFieldsIsMergeColumn == 0)
&& !string.IsNullOrWhiteSpace(f.ReportViewFieldsFieldTitle)
&& !titles.Contains(f.ReportViewFieldsFieldTitle, StringComparer.OrdinalIgnoreCase))
.GroupBy(f => f.ReportViewFieldsFieldTitle, StringComparer.OrdinalIgnoreCase)
.Select(g => g.First())
.ToList();
// Buffered, title-keyed rows — same shape ExcelExport.cs and the Phase 11 spike both use,
// needed to build GroupTreeBuilder's tree. ClosedXML/PdfSharpCore both fully materialize
// their output regardless, so this isn't a new memory-cost regression over what already
// exists for grouped Excel exports.
var rowsByTitle = new List>();
await foreach (var record in records.WithCancellation(cancellationToken))
{
var rowByTitle = new Dictionary(StringComparer.OrdinalIgnoreCase);
for (int col = 0; col < titles.Count && col < fieldNames.Count; col++)
{
record.TryGetValue(fieldNames[col], out var v);
rowByTitle[titles[col]] = v!;
}
foreach (var f in structuralOnlyFields)
{
record.TryGetValue(f.ReportVsFieldsFieldName, out var v);
rowByTitle[f.ReportViewFieldsFieldTitle] = v!;
}
rowsByTitle.Add(rowByTitle);
}
var groupColumns = reportViewFields
.Where(f => f.ReportViewFieldsIsGroupcolumn == 0)
.OrderBy(f => f.ReportViewFieldsDisplaySlNo)
.ToList();
var groupTitleSet = new HashSet(
groupColumns.Select(g => g.ReportViewFieldsFieldTitle ?? ""),
StringComparer.OrdinalIgnoreCase);
var subTotalFields = reportViewFields.Where(f => f.ReportViewFieldsIsSubTotal == 0).ToList();
var grandTotalFields = reportViewFields.Where(f => f.ReportViewFieldsIsGrandTotal == 0).ToList();
bool hasGrouping = groupColumns.Count > 0;
// Actual PDF table columns: RowLevel==0 fields that are NOT a group column — mirrors
// ReportExport.cs's HTML/PDF pipeline's NonGroupHeaders exactly (a group column renders as
// a full-width banner via WriteGroupNode instead; a RowLevel>0 field renders as an indented
// sub-line under its parent column instead — neither belongs in the grid as its own
// column). Passing the FULL unfiltered titles/fieldNames here (as before) would draw every
// group/row-level field as an extra column too, which is what silently blew the table past
// the printable page width.
var tableColumns = reportViewFields
.Where(f => f.ReportViewFieldsRowLevel == 0 && !groupTitleSet.Contains(f.ReportViewFieldsFieldTitle ?? ""))
.OrderBy(f => f.ReportViewFieldsDisplaySlNo)
.ToList();
var tableTitles = tableColumns.Select(f => f.ReportViewFieldsFieldTitle ?? "").ToList();
var tableFieldNames = tableColumns.Select(f => f.ReportVsFieldsFieldName ?? "").ToList();
Dictionary ComputeAggregateRow(IEnumerable> rows, List fields)
{
var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
var materialized = rows as IList> ?? rows.ToList();
foreach (var f in fields)
{
var title = f.ReportViewFieldsFieldTitle ?? "";
var agg = new ColumnAggregator(f.ReportViewFieldsAggregationType, f.ReportVsFieldsFieldType);
foreach (var row in materialized) { row.TryGetValue(title, out var v); agg.Accumulate(v); }
result[title] = agg.Result;
}
return result;
}
var renderer = new NativePdfRenderer(tableTitles, tableFieldNames, reportViewFields, ReportTheme.FromLogin(login),
logoBytes, reportViewPageFormat, reportViewPageOrientation);
renderer.Start(headers);
if (hasGrouping)
{
var groups = GroupTreeBuilder.BuildGroupNodes(
rowsByTitle, groupColumns, 0,
groupRows => subTotalFields.Count > 0 ? ComputeAggregateRow(groupRows, subTotalFields) : null);
foreach (var node in groups) renderer.WriteGroupNode(node);
}
else
{
foreach (var row in rowsByTitle) renderer.DrawDataRow(row);
}
if (grandTotalFields.Count > 0)
renderer.DrawTotalsRow("Grand Total", ComputeAggregateRow(rowsByTitle, grandTotalFields), isGrandTotal: true);
renderer.Finish(outputStream);
}
}
}