using System.IO;
using System.IO.Compression;
using System.Text;
namespace SharepointToolbox.Web.Services.Export;
/// How a multi-site report is bundled for download.
public enum ReportMergeMode
{
/// One document containing all sites, no per-site separation.
SingleMerged,
/// One HTML document with one tab per site (CSV falls back to merged).
SingleTabbed,
/// One file per site, delivered as a single ZIP.
MultipleFiles
}
/// Output format of a report.
public enum ReportFormat
{
Csv,
Html
}
/// A ready-to-download artifact: bytes plus filename and MIME type.
public sealed record MergeOutput(string FileName, byte[] Bytes, string Mime);
///
/// Bundles per-site report content into a single downloadable artifact
/// according to a . Format-agnostic: the caller
/// supplies a buildDoc delegate that renders one site's results to a
/// document string; this helper handles flattening, tabbing, and zipping.
///
public static class ReportMergeHelper
{
///
/// Builds the download artifact.
///
/// Per-site results (label + result list).
/// How to bundle the output.
/// File stem, e.g. "permissions".
/// Stamp appended to single-file names, e.g. "20260602_101500".
/// CSV or HTML.
/// Renders one result list to a complete document.
public static MergeOutput Build(
IReadOnlyList<(string Label, IReadOnlyList Results)> sites,
ReportMergeMode mode,
string baseName,
string timestamp,
ReportFormat format,
Func, string> buildDoc)
{
var ext = format == ReportFormat.Csv ? "csv" : "html";
var mime = format == ReportFormat.Csv ? "text/csv;charset=utf-8" : "text/html;charset=utf-8";
// CSV is BOM-prefixed for Excel; HTML is not.
var enc = new UTF8Encoding(encoderShouldEmitUTF8Identifier: format == ReportFormat.Csv);
// Tabs are an HTML-only concept — degrade to a single merged CSV.
if (mode == ReportMergeMode.SingleTabbed && format == ReportFormat.Csv)
mode = ReportMergeMode.SingleMerged;
switch (mode)
{
case ReportMergeMode.SingleTabbed:
{
var parts = sites
.Select(s => (s.Label, buildDoc(s.Results)))
.ToList();
var html = ReportSplitHelper.BuildTabbedHtml(parts, baseName);
return new MergeOutput($"{baseName}_{timestamp}.html", enc.GetBytes(html), mime);
}
case ReportMergeMode.MultipleFiles:
{
using var ms = new MemoryStream();
using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
{
var used = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var s in sites)
{
var name = UniqueName(used, $"{baseName}_{ReportSplitHelper.SanitizeFileName(s.Label)}.{ext}");
var entry = zip.CreateEntry(name, CompressionLevel.Optimal);
using var es = entry.Open();
var bytes = enc.GetBytes(buildDoc(s.Results));
es.Write(bytes, 0, bytes.Length);
}
}
return new MergeOutput($"{baseName}_{timestamp}.zip", ms.ToArray(), "application/zip");
}
case ReportMergeMode.SingleMerged:
default:
{
var flat = sites.SelectMany(s => s.Results).ToList();
var bytes = enc.GetBytes(buildDoc(flat));
return new MergeOutput($"{baseName}_{timestamp}.{ext}", bytes, mime);
}
}
}
///
/// Returns or, if already taken, a suffixed
/// variant ("name_2.ext", "name_3.ext", …) so ZIP entries never collide.
///
private static string UniqueName(HashSet used, string candidate)
{
if (used.Add(candidate)) return candidate;
var stem = Path.GetFileNameWithoutExtension(candidate);
var ext = Path.GetExtension(candidate);
for (int i = 2; ; i++)
{
var next = $"{stem}_{i}{ext}";
if (used.Add(next)) return next;
}
}
}