📄 MatDenDagen/Services/ExportService.cs
using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MatDenDagen.Infrastructure.Storage.BlobStorage;
using MatDenDagen.Infrastructure.Storage.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace MatDenDagen.Services;

public sealed class ExportService(
    ILogger<ExportService> logger,
    QuestionnaireContext questionnaireContext,
    BlobStorageService blobStorage
)
{
    public async Task ExportAllSubmissions(Stream outputStream, CancellationToken cancellationToken)
    {
        await using var archive = await ZipArchive.CreateAsync(
            outputStream,
            ZipArchiveMode.Create,
            leaveOpen: true,
            Encoding.UTF8,
            cancellationToken
        );

        var questions = await questionnaireContext.Questions.ToDictionaryAsync(
            q => q.Id,
            q => q.Text,
            cancellationToken
        );

        var submissions = await questionnaireContext
            .Submissions.Include(s => s.Answers)
            .Include(s => s.Uploads)
            .ToArrayAsync(cancellationToken);

        {
            var entry = archive.CreateEntry("export.md");
            await using var entryStream = await entry.OpenAsync(cancellationToken);
            await using var writer = new StreamWriter(entryStream);

            await writer.WriteLineAsync(
                $"""
                # Export

                Svar från {submissions.Length} deltagare
                """.AsMemory(),
                cancellationToken
            );
        }

        foreach (var submission in submissions)
        {
            var participant = await questionnaireContext.Participants.SingleAsync(
                p => p.Id == submission.Participant,
                cancellationToken
            );

            {
                var entry = archive.CreateEntry(Path.Join(participant.Name, "svar.md"));
                await using var entryStream = await entry.OpenAsync(cancellationToken);
                await using var writer = new StreamWriter(entryStream);

                await writer.WriteLineAsync(
                    $"""
                    # {participant.Name}

                    """.AsMemory(),
                    cancellationToken
                );

                foreach (var answer in submission.Answers)
                {
                    if (!questions.TryGetValue(answer.QuestionId, out var question))
                    {
                        logger.LogError("An answer to {QuestionId} exist, but no question.", answer.QuestionId);
                        question = answer.QuestionId.ToString();
                    }

                    await writer.WriteLineAsync(
                        $"""
                        ## {question}

                        {answer.Text}

                        """.AsMemory(),
                        cancellationToken
                    );
                }
            }

            foreach (var upload in submission.Uploads)
            {
                var entry = archive.CreateEntry(Path.Join(participant.Name, upload.Name));
                await using var entryStream = await entry.OpenAsync(cancellationToken);

                var blobStream = blobStorage.GetBlob(upload.Id);
                if (blobStream is null)
                {
                    logger.LogError("Upload {UploadId} is missing from blob storage.", upload.Id);
                    await using var writer = new StreamWriter(entryStream);
                    await writer.WriteLineAsync("Filen saknas.".AsMemory(), cancellationToken);
                }
                else
                {
                    await using (blobStream)
                    {
                        await blobStream.CopyToAsync(entryStream, cancellationToken);
                    }
                }
            }
        }
    }
}

public static class ExportServiceCollectionExtensions
{
    public static IServiceCollection AddExportService(this IServiceCollection services) =>
        services.AddTransient<ExportService>();
}