using System.Text;
namespace MeterVault.App.Analysis;
///
/// GET /export/analysis.csv (D-55): the analysis table of the URL keys the pages use — scope/id/
/// ids, metric, period, from, to, bucket, compare — as a CSV download.
/// Build links to it with .
///
///
/// A UI endpoint, not part of the versioned REST API: it serves the same reader a signed-in browser already sees and
/// needs no API key, like the pages themselves. Invalid input is a 400 with a short plain-text message; nothing a user
/// can type into the URL makes it a 500.
///
public static class AnalysisExportEndpoints
{
public static IEndpointRouteBuilder MapAnalysisExport(this IEndpointRouteBuilder endpoints)
{
ArgumentNullException.ThrowIfNull(endpoints);
endpoints.MapGet(AnalysisLinks.ExportPath, async (HttpContext http, AnalysisExport export, ILogger logger, CancellationToken ct) =>
{
var query = AnalysisQuery.Parse(http.Request.Query, AnalysisDefaults.Export);
AnalysisExportResult prepared;
try
{
prepared = await export.PrepareAsync(query, ct);
}
catch (ArgumentException ex)
{
// The readers reject a combination they cannot answer with an ArgumentException; that is the request's
// fault, not the server's.
logger.LogWarning(ex, "Analysis export refused {Query}", query);
return Results.Text("This combination of keys cannot be exported.", "text/plain; charset=utf-8", Encoding.UTF8, StatusCodes.Status400BadRequest);
}
if (prepared.Error is { } error)
{
return Results.Text(error, "text/plain; charset=utf-8", Encoding.UTF8, StatusCodes.Status400BadRequest);
}
return Results.Stream(
async stream =>
{
// A byte-order mark, so spreadsheet programs read the umlauts of meter names as UTF-8.
await using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), leaveOpen: true);
await AnalysisCsvWriter.WriteAsync(writer, prepared.Rows, ct);
},
"text/csv; charset=utf-8",
prepared.FileName);
})
.ExcludeFromDescription();
return endpoints;
}
}