using System.Text;
using JsonSerializer = System.Text.Json.JsonSerializer;
using LehrerApp.Core.Models;
using LehrerApp.Data;
using LehrerApp.Sync.Crypto;
using LehrerApp.Sync.Models;
using LiteDB;
namespace LehrerApp.Sync;
///
/// Wendet ein von empfangenes (und nicht durch einen Konflikt
/// verlorenes) Ereignis auf die lokale Datenbank an.
///
/// Schreibt IMMER direkt auf die rohe LiteDB-Collection, nie über eine Repository-
/// Save/Delete-Methode — sonst würde erneut feuern und die
/// gerade angewendete Änderung als neues ausgehendes Ereignis re-enqueuen (Sync-Ping-Pong).
/// Ein gemeinsames Suppress-Flag wurde bewusst verworfen: SyncEngine läuft per Timer nebenläufig
/// zum UI-Thread, ein Flag könnte während eines laufenden Pulls einen echten Nutzer-Save
/// verschlucken. Der direkte Collection-Zugriff ist zustandslos und dadurch korrekt.
///
/// Weiche Geschäftsregeln (z.B. ArchivedGroupWriteGuard, Namens-Eindeutigkeit) werden auf diesem
/// Pfad bewusst NICHT geprüft (v1-Einschränkung, siehe TODO.md 10.3) — nur harte LiteDB-Unique-
/// Constraints greifen noch und führen zum Überspringen des einzelnen Ereignisses.
///
public class EventApplier(LiteDbContext db, byte[] syncKey)
{
private static readonly Dictionary Handlers = BuildHandlers();
public void Apply(SyncEvent evt)
{
if (!Handlers.TryGetValue(evt.EntityType, out var handler)) return;
try
{
var json = evt.Payload.Length == 0 ? "" : Decrypt(evt.Payload);
handler(db, evt.Operation, evt.EntityId, json);
}
catch (LiteException)
{
// Harte Constraint-Verletzung (z.B. Unique-Index) - dieses eine Ereignis
// überspringen, statt den gesamten Sync-Lauf abzubrechen.
}
}
private string Decrypt(string payloadBase64) =>
Encoding.UTF8.GetString(SyncCrypto.Decrypt(Convert.FromBase64String(payloadBase64), syncKey));
private delegate void EntityHandler(LiteDbContext db, string operation, string entityId, string json);
private static Dictionary BuildHandlers()
{
var handlers = new Dictionary();
void Simple(Func> collection) where T : class =>
handlers[typeof(T).Name] = (context, operation, entityId, json) =>
{
if (operation == "Delete") collection(context).Delete(new Guid(entityId));
else collection(context).Upsert(JsonSerializer.Deserialize(json)!);
};
Simple(context => context.Students);
Simple(context => context.SeatingPlans);
Simple(context => context.Memberships);
Simple(context => context.GradingKeyTemplates);
Simple(context => context.Grades);
Simple(context => context.GradingSchemes);
Simple(context => context.ReportGrades);
Simple(context => context.Units);
Simple(context => context.Lessons);
Simple(context => context.Tasks);
Simple(context => context.TimeEntries);
Simple(context => context.ExamResults);
Simple(context => context.ParticipationEntries);
Simple(context => context.ParticipationAspects);
Simple(context => context.ParticipationSections);
Simple(context => context.Subjects);
Simple(context => context.ShorthandCodes);
Simple(context => context.AlternativeLessonPaths);
Simple(context => context.TimetableSlots);
Simple(context => context.SchoolHolidays);
Simple(context => context.SupervisionDuties);
Simple(context => context.SubstitutionEntries);
Simple(context => context.CompetencyDomains);
// Kaskaden-Fälle: dieselben internen LiteDbContext-Hilfsmethoden wie die jeweiligen
// Repositories, damit die Kaskade nur an einer Stelle im Code existiert.
handlers[nameof(LearningGroup)] = (context, operation, entityId, json) =>
{
if (operation == "Delete") context.CascadeDeleteGroup(new Guid(entityId));
else context.Groups.Upsert(JsonSerializer.Deserialize(json)!);
};
handlers[nameof(Exam)] = (context, operation, entityId, json) =>
{
if (operation == "Delete") context.CascadeDeleteExam(new Guid(entityId));
else context.Exams.Upsert(JsonSerializer.Deserialize(json)!);
};
handlers[nameof(ParticipationSession)] = (context, operation, entityId, json) =>
{
if (operation == "Delete") context.CascadeDeleteParticipationSession(new Guid(entityId));
else context.ParticipationSessions.Upsert(JsonSerializer.Deserialize(json)!);
};
// "Delete" ist hier das harte Löschen (samt Anhängen) - das weiche Löschen kommt als
// "Save" mit IsDeleted=true und läuft über den generischen Upsert-Zweig.
handlers[nameof(Documentation)] = (context, operation, entityId, json) =>
{
if (operation == "Delete") context.CascadeHardDeleteDocumentation(new Guid(entityId));
else context.Documentation.Upsert(JsonSerializer.Deserialize(json)!);
};
return handlers;
}
}