using System.IO;
using System.Text;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
namespace SharepointToolbox.Services.Export;
///
/// Exports permission entries to CSV format.
/// Ports PowerShell Merge-PermissionRows + Export-Csv functionality.
///
public class CsvExportService
{
private static string BuildHeader()
{
var T = TranslationSource.Instance;
return $"\"{T["report.col.object"]}\",\"{T["report.col.title"]}\",\"{T["report.col.url"]}\",\"HasUniquePermissions\",\"Users\",\"UserLogins\",\"Type\",\"Permissions\",\"GrantedThrough\"";
}
///
/// Builds a CSV string from the supplied permission entries.
/// Merges rows with identical (Users, PermissionLevels, GrantedThrough) by pipe-joining URLs and Titles.
///
public string BuildCsv(IReadOnlyList entries)
{
var sb = new StringBuilder();
sb.AppendLine(BuildHeader());
// Merge: group by (Users, PermissionLevels, GrantedThrough) — port of PS Merge-PermissionRows
var merged = entries
.GroupBy(e => (e.Users, e.PermissionLevels, e.GrantedThrough))
.Select(g => new
{
ObjectType = g.First().ObjectType,
Title = string.Join(" | ", g.Select(e => e.Title).Distinct()),
Url = string.Join(" | ", g.Select(e => e.Url).Distinct()),
HasUnique = g.First().HasUniquePermissions,
Users = g.Key.Users,
UserLogins = g.First().UserLogins,
PrincipalType = g.First().PrincipalType,
Permissions = g.Key.PermissionLevels,
GrantedThrough = g.Key.GrantedThrough
});
foreach (var row in merged)
sb.AppendLine(string.Join(",", new[]
{
Csv(row.ObjectType), Csv(row.Title), Csv(row.Url),
Csv(row.HasUnique.ToString()), Csv(row.Users), Csv(row.UserLogins),
Csv(row.PrincipalType), Csv(row.Permissions), Csv(row.GrantedThrough)
}));
return sb.ToString();
}
///
/// Writes the CSV to the specified file path using UTF-8 with BOM (for Excel compatibility).
///
public async Task WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct)
{
var csv = BuildCsv(entries);
await ExportFileWriter.WriteCsvAsync(filePath, csv, ct);
}
///
/// Header for simplified CSV export — includes "SimplifiedLabels" and "RiskLevel" columns.
///
private static string BuildSimplifiedHeader()
{
var T = TranslationSource.Instance;
return $"\"{T["report.col.object"]}\",\"{T["report.col.title"]}\",\"{T["report.col.url"]}\",\"HasUniquePermissions\",\"Users\",\"UserLogins\",\"Type\",\"Permissions\",\"SimplifiedLabels\",\"RiskLevel\",\"GrantedThrough\"";
}
///
/// Builds a CSV string from simplified permission entries.
/// Includes SimplifiedLabels and RiskLevel columns after raw Permissions.
/// Uses the same merge logic as the standard BuildCsv.
///
public string BuildCsv(IReadOnlyList entries)
{
var sb = new StringBuilder();
sb.AppendLine(BuildSimplifiedHeader());
var merged = entries
.GroupBy(e => (e.Users, e.PermissionLevels, e.GrantedThrough))
.Select(g => new
{
ObjectType = g.First().ObjectType,
Title = string.Join(" | ", g.Select(e => e.Title).Distinct()),
Url = string.Join(" | ", g.Select(e => e.Url).Distinct()),
HasUnique = g.First().HasUniquePermissions,
Users = g.Key.Users,
UserLogins = g.First().UserLogins,
PrincipalType = g.First().PrincipalType,
Permissions = g.Key.PermissionLevels,
SimplifiedLabels = g.First().SimplifiedLabels,
RiskLevel = g.First().RiskLevel.ToString(),
GrantedThrough = g.Key.GrantedThrough
});
foreach (var row in merged)
sb.AppendLine(string.Join(",", new[]
{
Csv(row.ObjectType), Csv(row.Title), Csv(row.Url),
Csv(row.HasUnique.ToString()), Csv(row.Users), Csv(row.UserLogins),
Csv(row.PrincipalType), Csv(row.Permissions), Csv(row.SimplifiedLabels),
Csv(row.RiskLevel), Csv(row.GrantedThrough)
}));
return sb.ToString();
}
///
/// Writes simplified CSV to the specified file path.
///
public async Task WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct)
{
var csv = BuildCsv(entries);
await ExportFileWriter.WriteCsvAsync(filePath, csv, ct);
}
///
/// Writes permission entries with optional per-site partitioning.
/// Single → writes one file at .
/// BySite → one file per site-collection URL, suffixed on the base path.
///
public Task WriteAsync(
IReadOnlyList entries,
string basePath,
ReportSplitMode splitMode,
CancellationToken ct)
=> ReportSplitHelper.WritePartitionedAsync(
entries, basePath, splitMode,
PartitionBySite,
(part, path, c) => WriteAsync(part, path, c),
ct);
/// Simplified-entry split variant.
public Task WriteAsync(
IReadOnlyList entries,
string basePath,
ReportSplitMode splitMode,
CancellationToken ct)
=> ReportSplitHelper.WritePartitionedAsync(
entries, basePath, splitMode,
PartitionBySite,
(part, path, c) => WriteAsync(part, path, c),
ct);
internal static IEnumerable<(string Label, IReadOnlyList Partition)> PartitionBySite(
IReadOnlyList entries)
{
return entries
.GroupBy(e => ReportSplitHelper.DeriveSiteCollectionUrl(e.Url), StringComparer.OrdinalIgnoreCase)
.Select(g => (
Label: ReportSplitHelper.DeriveSiteLabel(g.Key),
Partition: (IReadOnlyList)g.ToList()));
}
internal static IEnumerable<(string Label, IReadOnlyList Partition)> PartitionBySite(
IReadOnlyList entries)
{
return entries
.GroupBy(e => ReportSplitHelper.DeriveSiteCollectionUrl(e.Url), StringComparer.OrdinalIgnoreCase)
.Select(g => (
Label: ReportSplitHelper.DeriveSiteLabel(g.Key),
Partition: (IReadOnlyList)g.ToList()));
}
/// RFC 4180 CSV field escaping with formula-injection guard.
private static string Csv(string value) => CsvSanitizer.Escape(value);
}