using System.Text.Json;
using FozFc.Api.Application.Zerozero;
using FozFc.Api.Infrastructure.Zerozero;
namespace FozFc.Api.Api.Endpoints;
///
/// Football data feeds scraped from zerozero.pt (replaces the Cloudflare worker):
/// matches, news, standings, cup progress, plus an image proxy. All public.
///
public static class ZerozeroEndpoints
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
public static IEndpointRouteBuilder MapZerozeroEndpoints(this IEndpointRouteBuilder app)
{
app.MapGet("/api/matches-feed", (HttpContext ctx, IZerozeroFeedService svc, CancellationToken ct) =>
Feed(ctx, ct, () => svc.GetMatchesAsync(ct)));
app.MapGet("/api/news-feed", (HttpContext ctx, IZerozeroFeedService svc, CancellationToken ct) =>
Feed(ctx, ct, () => svc.GetNewsAsync(ctx.Request.Query["slug"], ct)));
app.MapGet("/api/standings-liga", (HttpContext ctx, IZerozeroFeedService svc, CancellationToken ct) =>
Feed(ctx, ct, () => svc.GetStandingsLigaAsync(ct)));
app.MapGet("/api/standings-taca", (HttpContext ctx, IZerozeroFeedService svc, CancellationToken ct) =>
Feed(ctx, ct, () => svc.GetStandingsTacaAsync(ct)));
app.MapGet("/api/cup-progress", (HttpContext ctx, IZerozeroFeedService svc, CancellationToken ct) =>
Feed(ctx, ct, () => svc.GetCupProgressAsync(ct)));
// Image proxy: /api/media/r/
app.MapGet("/api/media/r/{encoded}", async (string encoded, HttpContext ctx, ZerozeroHttpClient http, CancellationToken ct) =>
{
string target;
try
{
target = ZerozeroImages.Base64UrlDecode(encoded);
var host = new Uri(target).Host.ToLowerInvariant();
if (host != "zerozero.pt" && !host.EndsWith(".zerozero.pt"))
return Results.Text("Forbidden", "text/plain", statusCode: 403);
}
catch
{
return Results.Text("Invalid", "text/plain", statusCode: 400);
}
return await ProxyMedia(ctx, http, target, ct);
});
// Image proxy: /api/media/ -> zerozero.pt/
app.MapGet("/api/media/{**path}", (string path, HttpContext ctx, ZerozeroHttpClient http, CancellationToken ct) =>
{
var target = $"{ZerozeroSite.Origin}/{path}{ctx.Request.QueryString.Value}";
return ProxyMedia(ctx, http, target, ct);
});
return app;
}
private static async Task Feed(HttpContext ctx, CancellationToken ct, Func>> fetch)
{
try
{
var payload = await fetch();
ctx.Response.Headers.CacheControl = "public, max-age=86400, stale-while-revalidate=900";
return Results.Text(JsonSerializer.Serialize(payload, JsonOptions), "application/json; charset=utf-8");
}
catch (OperationCanceledException)
{
return Results.StatusCode(499);
}
catch
{
// Frontend falls back to demo data on non-2xx.
return Results.Json(new { error = "Upstream error" }, statusCode: 502);
}
}
private static async Task ProxyMedia(HttpContext ctx, ZerozeroHttpClient http, string target, CancellationToken ct)
{
try
{
var (bytes, contentType) = await http.FetchBytesAsync(target, ct: ct);
ctx.Response.Headers.CacheControl = "public, max-age=86400";
return Results.Bytes(bytes, string.IsNullOrEmpty(contentType) ? "application/octet-stream" : contentType);
}
catch
{
return Results.StatusCode(502);
}
}
}