using Microsoft.Playwright; namespace JobTrackerApi.Services; public sealed record CvPdfArtifact(string FileName, string StoragePath, byte[] Bytes); public interface ICvPdfExporter { Task ExportAsync(TailoredCvRenderResult renderResult, CancellationToken cancellationToken); } public sealed class PlaywrightCvPdfExporter : ICvPdfExporter { private readonly AppPaths _paths; private readonly ILogger _logger; public PlaywrightCvPdfExporter(AppPaths paths, ILogger logger) { _paths = paths; _logger = logger; } public async Task ExportAsync(TailoredCvRenderResult renderResult, CancellationToken cancellationToken) { var now = DateTimeOffset.UtcNow; var folder = Path.Combine(_paths.CvExportsRoot, now.ToString("yyyyMMdd")); Directory.CreateDirectory(folder); var fileName = string.IsNullOrWhiteSpace(renderResult.SuggestedFileName) ? $"tailored-cv-{now:yyyyMMddHHmmss}.pdf" : renderResult.SuggestedFileName; var storagePath = Path.Combine(folder, fileName); try { using var playwright = await Playwright.CreateAsync(); await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true, }); var page = await browser.NewPageAsync(); await page.SetContentAsync(renderResult.Html, new PageSetContentOptions { WaitUntil = WaitUntilState.Load, }); var bytes = await page.PdfAsync(new PagePdfOptions { Format = "A4", PrintBackground = true, Margin = new() { Top = "0", Right = "0", Bottom = "0", Left = "0", } }); await File.WriteAllBytesAsync(storagePath, bytes, cancellationToken); return new CvPdfArtifact(fileName, storagePath, bytes); } catch (Exception ex) { _logger.LogError(ex, "Failed to export CV PDF to {Path}", storagePath); throw new InvalidOperationException("CV PDF export is unavailable. Ensure Chromium is installed for Playwright on this machine.", ex); } } }