using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Report; using Microsoft.Extensions.Logging; namespace GB5Shared.Export.CSVExport { public class CSVExport : ICSVExport { private readonly ILogger _logger; public CSVExport(ILogger logger) { _logger = logger; } public string ContentType => "text/csv"; public string FileExtension => ".csv"; public async Task Export( IAsyncEnumerable> records, IAsyncEnumerable mainHeaders, IAsyncEnumerable columnTitles, IAsyncEnumerable columnFieldNames, Stream outputStream, string sheetName = "Report", CancellationToken cancellationToken = default, List? reportViewFields = null) { using var writer = new StreamWriter(outputStream, new UTF8Encoding(true), leaveOpen: true); // 1️⃣ Write main headers var headers = new List(); await foreach (var h in mainHeaders.WithCancellation(cancellationToken)) headers.Add(h); foreach (var h in headers) { var headerText = h.Expression?.Replace("\r", " ").Replace("\n", " ") ?? string.Empty; await writer.WriteLineAsync($"# {headerText}"); // CSV comment-like header } // 2️⃣ Write column titles (display text) and field names (row lookup keys) 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); await writer.WriteLineAsync(string.Join(",", titles.Select(EscapeCsv))); // 3️⃣ Write records, values looked up by field name; interleave subtotal rows on group change var tracker = new GroupSubtotalTracker(reportViewFields); await foreach (var record in records.WithCancellation(cancellationToken)) { if (tracker.ObserveRow(record, out var completedSubtotal)) await WriteTotalsLineAsync(writer, "Sub Total", completedSubtotal!, fieldNames); var values = fieldNames.Select(f => { record.TryGetValue(f, out var val); return EscapeCsv(ConvertValue(val)); }); await writer.WriteLineAsync(string.Join(",", values)); } var trailingSubtotal = tracker.FlushFinalSubtotal(); if (trailingSubtotal != null) await WriteTotalsLineAsync(writer, "Sub Total", trailingSubtotal, fieldNames); if (tracker.HasGrandTotal) await WriteTotalsLineAsync(writer, "Grand Total", tracker.FinalizeGrandTotal(), fieldNames); await writer.FlushAsync(); } // ── Writes a "Sub Total"/"Grand Total" row: label in column 1, aggregated values in their columns ── private static async Task WriteTotalsLineAsync( StreamWriter writer, string label, Dictionary totals, List fieldNames) { var cells = new List(fieldNames.Count); for (int i = 0; i < fieldNames.Count; i++) { if (i == 0) { cells.Add(EscapeCsv(label)); continue; } cells.Add(totals.TryGetValue(fieldNames[i], out var v) && v != null ? EscapeCsv(ConvertValue(v)) : string.Empty); } await writer.WriteLineAsync(string.Join(",", cells)); } private static string ConvertValue(object? value) { if (value == null) return string.Empty; return value switch { DateTime dt => dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), bool b => b ? "TRUE" : "FALSE", _ => value.ToString()?.Replace("\r", " ").Replace("\n", " ") ?? string.Empty }; } private static string EscapeCsv(string value) { if (value.Contains(",") || value.Contains("\"") || value.Contains("\n")) { value = value.Replace("\"", "\"\""); return $"\"{value}\""; } return value; } public async IAsyncEnumerable> ReportURICalling( ReportCallingDTO reportCallingDTO, LoginDTO? loginDTO, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { using var httpClient = new HttpClient(); LoginHeader(httpClient, loginDTO); var bodyJson = JsonSerializer.Serialize(reportCallingDTO.CriteriaDTO); using var content = new StringContent(bodyJson, Encoding.UTF8, "application/json"); using var response = await httpClient.PostAsync(reportCallingDTO.ReportUri, content, cancellationToken); response.EnsureSuccessStatusCode(); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); if (doc.RootElement.ValueKind == JsonValueKind.Array) { foreach (var element in doc.RootElement.EnumerateArray()) yield return ToDictionary(element); } else if (doc.RootElement.ValueKind == JsonValueKind.Object) { yield return ToDictionary(doc.RootElement); } } private static void LoginHeader(HttpClient httpClient, LoginDTO? loginDTO) { var loginJson = JsonSerializer.Serialize(loginDTO); if (httpClient.DefaultRequestHeaders.Contains("Login")) httpClient.DefaultRequestHeaders.Remove("Login"); httpClient.DefaultRequestHeaders.Add("Login", loginJson); } private static Dictionary ToDictionary(JsonElement element) { var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var prop in element.EnumerateObject()) { dict[prop.Name] = prop.Value.ValueKind switch { JsonValueKind.String => prop.Value.GetString(), JsonValueKind.Number => prop.Value.TryGetInt64(out var l) ? l : prop.Value.GetDecimal(), JsonValueKind.True => true, JsonValueKind.False => false, JsonValueKind.Null => null, _ => prop.Value.ToString() }; } return dict; } } }