using System.Globalization; using System.Text; using System.Text.Json; using CsvHelper; using InboxIntel.Application.Abstractions; using DTOs = InboxIntel.Application.DTOs; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace InboxIntel.Infrastructure.Export; /// /// Builds inbox reports as PDF (QuestPDF), CSV (CsvHelper) or JSON. The report /// covers the inbox summary, sender stats, cleanup suggestions and storage use. /// public class ExportService : IExportService { private readonly IAnalyticsService _analytics; private readonly IAiService _ai; public ExportService(IAnalyticsService analytics, IAiService ai) { _analytics = analytics; _ai = ai; QuestPDF.Settings.License = LicenseType.Community; } public async Task<(byte[] Content, string ContentType, string FileName)> ExportReportAsync(Guid userId, ExportFormat format, CancellationToken ct = default) { var health = await _analytics.GetInboxHealthAsync(userId, ct); var topSenders = await _analytics.GetTopSendersAsync(userId, 25, ct); var suggestions = await _ai.SuggestCleanupAsync(userId, ct); var stamp = DateTime.UtcNow.ToString("yyyyMMdd-HHmm"); return format switch { ExportFormat.Json => (BuildJson(health, topSenders, suggestions), "application/json", $"inbox-report-{stamp}.json"), ExportFormat.Csv => (BuildCsv(topSenders), "text/csv", $"sender-stats-{stamp}.csv"), _ => (BuildPdf(health, topSenders, suggestions), "application/pdf", $"inbox-report-{stamp}.pdf") }; } private static byte[] BuildJson(object health, object senders, object suggestions) { var payload = new { generatedUtc = DateTimeOffset.UtcNow, health, topSenders = senders, cleanupSuggestions = suggestions }; return JsonSerializer.SerializeToUtf8Bytes(payload, new JsonSerializerOptions { WriteIndented = true }); } private static byte[] BuildCsv(IEnumerable senders) { using var ms = new MemoryStream(); using (var writer = new StreamWriter(ms, Encoding.UTF8, leaveOpen: true)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) { csv.WriteRecords(senders); } return ms.ToArray(); } private byte[] BuildPdf(DTOs.InboxHealthDto health, IReadOnlyList senders, IReadOnlyList suggestions) { var doc = Document.Create(container => { container.Page(page => { page.Margin(40); page.Size(PageSizes.A4); page.DefaultTextStyle(t => t.FontSize(10)); page.Header().Text("InboxIntel — Inbox Report").FontSize(20).Bold(); page.Content().PaddingVertical(10).Column(col => { col.Item().Text($"Generated: {DateTime.UtcNow:u}").FontColor(Colors.Grey.Medium); col.Item().PaddingTop(10).Text($"Inbox Health: {health.Score}/100 (Grade {health.Grade})").FontSize(14).Bold(); col.Item().Text($"Total emails: {health.TotalEmails:N0} Unread: {health.UnreadEmails:N0} Newsletters: {health.NewsletterCount:N0}"); col.Item().Text($"Estimated storage: {health.EstimatedStorageBytes / 1_048_576.0:N1} MB"); col.Item().PaddingTop(14).Text("Recommendations").FontSize(13).Bold(); foreach (var rec in health.Recommendations) col.Item().Text($"• {rec}"); col.Item().PaddingTop(14).Text("Top Senders").FontSize(13).Bold(); col.Item().Table(table => { table.ColumnsDefinition(c => { c.RelativeColumn(3); c.RelativeColumn(1); c.RelativeColumn(1); }); table.Header(h => { h.Cell().Text("Sender").Bold(); h.Cell().Text("Emails").Bold(); h.Cell().Text("Unread").Bold(); }); foreach (var s in senders) { table.Cell().Text(s.Address); table.Cell().Text(s.EmailCount.ToString("N0")); table.Cell().Text(s.UnreadCount.ToString("N0")); } }); col.Item().PaddingTop(14).Text("Cleanup Suggestions").FontSize(13).Bold(); foreach (var sug in suggestions) col.Item().Text($"• {sug.Title} — {sug.Rationale}"); }); page.Footer().AlignCenter().Text(t => { t.Span("InboxIntel • "); t.CurrentPageNumber(); t.Span(" / "); t.TotalPages(); }); }); }); return doc.GeneratePdf(); } }