using System.Text.Json.Nodes;
using GerbilManagerWebAPI.Models;
using Microsoft.EntityFrameworkCore;
namespace GerbilManagerWebAPI.Cms
{
///
/// Builds the full CMS snapshot consumed by the static site renderer (WEB-1). The
/// abgabetiere page's auto AbgabetiereList block is resolved here from live ForSale
/// animals — nothing private is ever exposed (publish = snapshot of public content).
///
public sealed class SiteSnapshotService
{
private readonly ApplicationContext _db;
public SiteSnapshotService(ApplicationContext db) => _db = db;
public async Task BuildAsync()
{
var site = await _db.Sites.AsNoTracking().FirstOrDefaultAsync()
?? new Site { Id = Site.SingletonId };
var pages = await _db.Pages.AsNoTracking().Include(p => p.Blocks).ToListAsync();
var pageById = pages.ToDictionary(p => p.Id);
var navOrder = new JsonArray();
foreach (var id in site.NavOrder)
if (pageById.TryGetValue(id, out var p))
navOrder.Add(p.Slug);
JsonArray? forSale = null; // resolved lazily, reused across blocks
int NavIndex(Guid id) { var i = site.NavOrder.IndexOf(id); return i < 0 ? int.MaxValue : i; }
var pagesArr = new JsonArray();
foreach (var page in pages.OrderBy(p => NavIndex(p.Id)).ThenBy(p => p.Slug))
{
var blocks = new JsonArray();
foreach (var b in page.Blocks.OrderBy(b => b.Order))
{
var data = JsonNode.Parse(string.IsNullOrWhiteSpace(b.Data) ? "{}" : b.Data) as JsonObject
?? new JsonObject();
if (b.Type == BlockType.AbgabetiereList && AsString(data["mode"]) == "auto")
{
forSale ??= await BuildForSaleAsync();
data["animals"] = forSale.DeepClone();
}
blocks.Add(new JsonObject
{
["id"] = b.Id.ToString(),
["order"] = b.Order,
["type"] = b.Type.ToString(),
["data"] = data,
});
}
pagesArr.Add(new JsonObject
{
["slug"] = page.Slug,
["title"] = page.Title,
["seoDescription"] = page.SeoDescription,
["status"] = page.Status.ToString(),
["blocks"] = blocks,
});
}
return new JsonObject
{
["site"] = new JsonObject
{
["defaultLocale"] = site.DefaultLocale,
["navOrder"] = navOrder,
},
["pages"] = pagesArr,
};
}
private async Task BuildForSaleAsync()
{
var animals = await _db.Gerbils.AsNoTracking()
.Where(g => g.Status == GerbilStatus.ForSale)
.OrderBy(g => g.Name)
.Select(g => new
{
g.Id,
g.Name,
Farbschlag = g.ColorVariety != null ? g.ColorVariety.Name : null,
// group = Becken (enclosure) name — matches how the Abgabe composer
// groups ForSale animals by enclosure-mates (god ruling). null = single.
Group = g.Enclosure != null ? g.Enclosure.Name : null,
})
.ToListAsync();
var ids = animals.Select(a => a.Id).ToList();
var photos = await _db.GerbilPhotos.AsNoTracking()
.Where(p => ids.Contains(p.GerbilId))
.OrderBy(p => p.SortOrder)
.Select(p => new { p.GerbilId, p.FileName })
.ToListAsync();
var photosByGerbil = photos
.GroupBy(p => p.GerbilId)
.ToDictionary(g => g.Key, g => g.Select(x => $"/photos/files/{x.FileName}").ToList());
var arr = new JsonArray();
foreach (var a in animals)
{
var photoArr = new JsonArray();
if (photosByGerbil.TryGetValue(a.Id, out var urls))
foreach (var u in urls) photoArr.Add(u);
arr.Add(new JsonObject
{
["name"] = a.Name,
["farbschlag"] = a.Farbschlag,
["group"] = a.Group,
["photos"] = photoArr,
["aiSaleText"] = null, // populated by WEB-5 once a Gemini key exists
});
}
return arr;
}
private static string? AsString(JsonNode? node) =>
node is JsonValue v && v.TryGetValue(out var s) ? s : null;
}
}