using GerbilManagerWebAPI.Dtos; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; namespace GerbilManagerWebAPI.Endpoints { /// /// REFS: Auflöser für getippte Kurz-Verweise (Shortlinks, git-Stil) in Freitexten. /// Tokens im Text: tier:<code>, kontakt:<code>, /// wurf:<code>, gehege:<code><code> ist ein /// GUID-Präfix (mind. 6 Hex) oder die volle GUID. Das Frontend rendert daraus /// Name + interner Link. /// /// POST /refs/resolve -> [{ type, code, id|null, name|null }] /// /// DB-AGNOSTISCH (Tests: SQLite in-memory, Prod: Postgres): pro Typ eine schlanke /// {Id,Name}-Projektion laden und im Speicher per Präfix matchen — KEIN ::text-Cast /// o.ä. Mehrdeutiges Präfix (mehrere Treffer) => id=null (nicht raten). /// public static class RefsEndpoints { /// Schlanke {Id,Name}-Projektion für den In-Memory-Präfix-Match. private readonly record struct NamedRow(Guid Id, string Name); public static IEndpointRouteBuilder MapRefsEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/refs").WithTags("Refs"); group.MapPost("/resolve", async Task>> ( ResolveRefsInput input, ApplicationContext db) => { var refs = input.Refs ?? new List(); // Welche Entitätstypen werden überhaupt angefragt? Nur diese Tabellen laden. var wantedTypes = refs .Select(r => NormalizeType(r.Type)) .Where(t => t is not null) .Distinct() .ToHashSet(); // Pro Typ einmalig eine leichte {Id,Name}-Projektion laden (DB-agnostisch: // reine Spaltenauswahl, kein ::text-Cast — läuft auf SQLite wie auf Postgres). var byType = new Dictionary>(); if (wantedTypes.Contains("tier")) byType["tier"] = await db.Gerbils.AsNoTracking() .Select(x => new NamedRow(x.Id, x.Name)).ToListAsync(); if (wantedTypes.Contains("kontakt")) byType["kontakt"] = await db.Contacts.AsNoTracking() .Select(x => new NamedRow(x.Id, x.Name)).ToListAsync(); if (wantedTypes.Contains("wurf")) byType["wurf"] = await db.Litters.AsNoTracking() .Select(x => new NamedRow(x.Id, x.Name)).ToListAsync(); if (wantedTypes.Contains("gehege")) byType["gehege"] = await db.Enclosures.AsNoTracking() .Select(x => new NamedRow(x.Id, x.Name)).ToListAsync(); var results = new List(refs.Count); foreach (var r in refs) { var type = NormalizeType(r.Type); var prefix = NormalizeCode(r.Code); if (type is null || prefix.Length < 6 || !byType.TryGetValue(type, out var rows)) { results.Add(new RefResolution(r.Type, r.Code, null, null)); continue; } // Präfix-Match auf der bindestrich-losen, klein geschriebenen GUID ("N"-Format). var matches = rows .Where(x => x.Id.ToString("N").StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) .Take(2) .ToList(); // Genau ein Treffer => auflösen; 0 (unbekannt) oder >1 (mehrdeutig) => null. if (matches.Count == 1) results.Add(new RefResolution(r.Type, r.Code, matches[0].Id, matches[0].Name)); else results.Add(new RefResolution(r.Type, r.Code, null, null)); } return TypedResults.Ok(results); }); return app; } /// Akzeptiert die vier Typ-Schlüssel (case-insensitive); sonst null. private static string? NormalizeType(string? type) => type?.Trim().ToLowerInvariant() switch { "tier" => "tier", "kontakt" => "kontakt", "wurf" => "wurf", "gehege" => "gehege", _ => null, }; /// GUID-Code bindestrich-tolerant + Hex-only normalisieren (Kleinschreibung). private static string NormalizeCode(string? code) { if (string.IsNullOrWhiteSpace(code)) return string.Empty; var chars = code.Trim().ToLowerInvariant() .Where(c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')); return new string(chars.ToArray()); } } }