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