using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
using System.IO;
using System.Text;
namespace SharepointToolbox.Services.Export;
///
/// Exports a flat list of StorageNode objects to a UTF-8 BOM CSV.
/// Compatible with Microsoft Excel (BOM signals UTF-8 encoding).
///
public class StorageCsvExportService
{
///
/// Builds a single-section CSV: header row plus one row per
/// with library, site, file count, total size
/// (MB), version size (MB), and last-modified date.
///
public string BuildCsv(IReadOnlyList nodes)
{
var T = TranslationSource.Instance;
var sb = new StringBuilder();
// Header
sb.AppendLine($"{T["report.col.library"]},{T["report.col.site"]},{T["report.stat.files"]},{T["report.col.total_size_mb"]},{T["report.col.version_size_mb"]},{T["report.col.last_modified"]}");
foreach (var node in nodes)
{
sb.AppendLine(string.Join(",",
Csv(node.Name),
Csv(node.SiteTitle),
node.TotalFileCount.ToString(),
FormatMb(node.TotalSizeBytes),
FormatMb(node.VersionSizeBytes),
node.LastModified.HasValue
? Csv(node.LastModified.Value.ToString("yyyy-MM-dd"))
: string.Empty));
}
return sb.ToString();
}
/// Writes the library-level CSV to with UTF-8 BOM.
public async Task WriteAsync(IReadOnlyList nodes, string filePath, CancellationToken ct)
{
var csv = BuildCsv(nodes);
await ExportFileWriter.WriteCsvAsync(filePath, csv, ct);
}
///
/// Builds a CSV with library details followed by a file-type breakdown section.
///
public string BuildCsv(IReadOnlyList nodes, IReadOnlyList fileTypeMetrics)
{
var T = TranslationSource.Instance;
var sb = new StringBuilder();
// Library details
sb.AppendLine($"{T["report.col.library"]},{T["report.col.site"]},{T["report.stat.files"]},{T["report.col.total_size_mb"]},{T["report.col.version_size_mb"]},{T["report.col.last_modified"]}");
foreach (var node in nodes)
{
sb.AppendLine(string.Join(",",
Csv(node.Name),
Csv(node.SiteTitle),
node.TotalFileCount.ToString(),
FormatMb(node.TotalSizeBytes),
FormatMb(node.VersionSizeBytes),
node.LastModified.HasValue
? Csv(node.LastModified.Value.ToString("yyyy-MM-dd"))
: string.Empty));
}
// File type breakdown
if (fileTypeMetrics.Count > 0)
{
sb.AppendLine();
sb.AppendLine($"{T["report.col.file_type"]},{T["report.col.size_mb"]},{T["report.col.file_count"]}");
foreach (var m in fileTypeMetrics)
{
string label = string.IsNullOrEmpty(m.Extension) ? T["report.text.no_extension"] : m.Extension;
sb.AppendLine(string.Join(",", Csv(label), FormatMb(m.TotalSizeBytes), m.FileCount.ToString()));
}
}
return sb.ToString();
}
/// Writes the two-section CSV (libraries + file-type breakdown) with UTF-8 BOM.
public async Task WriteAsync(IReadOnlyList nodes, IReadOnlyList fileTypeMetrics, string filePath, CancellationToken ct)
{
var csv = BuildCsv(nodes, fileTypeMetrics);
await ExportFileWriter.WriteCsvAsync(filePath, csv, ct);
}
///
/// Writes storage metrics with optional per-site partitioning.
/// Single → one file. BySite → one file per SiteTitle. File-type metrics
/// are replicated across all partitions because the tenant-level scan
/// does not retain per-site breakdowns.
///
public Task WriteAsync(
IReadOnlyList nodes,
IReadOnlyList fileTypeMetrics,
string basePath,
ReportSplitMode splitMode,
CancellationToken ct)
=> ReportSplitHelper.WritePartitionedAsync(
nodes, basePath, splitMode,
PartitionBySite,
(part, path, c) => WriteAsync(part, fileTypeMetrics, path, c),
ct);
///
/// Splits the flat StorageNode list into per-site slices while preserving
/// the DFS hierarchy (each root library followed by its indented descendants).
/// Siblings sharing a SiteTitle roll up into the same partition.
///
internal static IEnumerable<(string Label, IReadOnlyList Partition)> PartitionBySite(
IReadOnlyList nodes)
{
var buckets = new Dictionary>(StringComparer.OrdinalIgnoreCase);
string currentSite = string.Empty;
foreach (var node in nodes)
{
if (node.IndentLevel == 0)
currentSite = string.IsNullOrWhiteSpace(node.SiteTitle)
? ReportSplitHelper.DeriveSiteLabel(node.Url)
: node.SiteTitle;
if (!buckets.TryGetValue(currentSite, out var list))
{
list = new List();
buckets[currentSite] = list;
}
list.Add(node);
}
return buckets.Select(kv => (kv.Key, (IReadOnlyList)kv.Value));
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static string FormatMb(long bytes)
=> (bytes / (1024.0 * 1024.0)).ToString("F2");
/// RFC 4180 CSV field quoting with formula-injection guard.
private static string Csv(string value) => CsvSanitizer.EscapeMinimal(value);
}