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; } }