using System.Net.Http.Json; using LehrerApp.Sync.Models; namespace LehrerApp.Sync; /// /// Orchestriert Push, Pull, EventApply und Snapshot-Export. /// Automatisch alle N Minuten + manuell auslösbar. /// public class SyncEngine : IDisposable { private readonly EventQueue _queue; private readonly ConflictResolver _resolver; private readonly EventApplier? _applier; private readonly ReadableSnapshotService? _snapshotExport; private readonly HttpClient _http; private readonly SyncConfig _config; private readonly Timer _timer; public SyncStatus Status { get; private set; } = new(); public event Action? StatusChanged; public SyncEngine( EventQueue queue, ConflictResolver resolver, HttpClient http, SyncConfig config, EventApplier? applier = null, ReadableSnapshotService? snapshotExport = null) { _queue = queue; _resolver = resolver; _http = http; _config = config; _applier = applier; _snapshotExport = snapshotExport; _timer = new Timer( async _ => await SyncNowAsync(isAutomatic: true), null, TimeSpan.FromMinutes(config.AutoSyncIntervalMinutes), TimeSpan.FromMinutes(config.AutoSyncIntervalMinutes)); UpdateStatus(); } // ── Öffentliche API ─────────────────────────────────────────────────────── public async Task SyncNowAsync(bool isAutomatic = false) { if (Status.State == SyncState.Syncing) return new SyncResult { Skipped = true, Reason = "Sync bereits aktiv" }; SetState(SyncState.Syncing); try { // 1. Ausstehende Events pushen var pushResult = await PushAsync(); // 2. Neue Events vom Server holen var pullResult = await PullAsync(); // 3. Geholte Events auf LiteDB anwenden if (_applier is not null && pullResult.Events.Count > 0) _applier.ApplyAll(pullResult.Events); // 4. Nach erfolgreichem Sync: lesbaren Snapshot exportieren // (nur bei automatischem Sync oder explizit – nicht bei jedem // manuellen Push um Traffic zu sparen) if (_snapshotExport is not null && (isAutomatic || pushResult.Pushed > 0)) await _snapshotExport.ExportAndPushAsync(); _queue.SetLastSyncAt(DateTime.UtcNow); SetState(SyncState.Idle); return new SyncResult { Success = true, EventsPushed = pushResult.Pushed, EventsPulled = pullResult.Events.Count, Conflicts = pullResult.Conflicts, }; } catch (HttpRequestException) { SetState(SyncState.Offline); return new SyncResult { Success = false, Reason = "Server nicht erreichbar" }; } catch (Exception ex) { SetState(SyncState.Error, ex.Message); return new SyncResult { Success = false, Reason = ex.Message }; } } // ── Push ────────────────────────────────────────────────────────────────── private async Task<(int Pushed, int Conflicts)> PushAsync() { var pending = _queue.GetPending(maxBatch: 200); if (pending.Count == 0) return (0, 0); var response = await _http.PostAsJsonAsync("/api/sync/push", pending); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync(); if (result is null) return (0, 0); var sentIds = pending .Where(e => !result.ConflictingEventIds.Contains(e.EventId)) .Select(e => e.EventId); _queue.Acknowledge(sentIds); _queue.SetLastServerSequenceNr(result.ServerSequenceNr); return (pending.Count - result.ConflictingEventIds.Count, result.ConflictingEventIds.Count); } // ── Pull ────────────────────────────────────────────────────────────────── private async Task<(List Events, int Conflicts)> PullAsync() { var since = _queue.GetLastServerSequenceNr(); var response = await _http.GetFromJsonAsync( $"/api/sync/pull?since={since}&deviceId={_config.DeviceId}"); if (response is null || response.Events.Count == 0) return ([], 0); var conflicts = 0; foreach (var remoteEvent in response.Events) { var conflict = _resolver.TryResolve(remoteEvent, _config.DeviceId); if (conflict is not null) { _queue.AddConflict(conflict); conflicts++; } } _queue.SetLastServerSequenceNr(response.ServerSequenceNr); return (response.Events, conflicts); } // ── Hilfsmethoden ───────────────────────────────────────────────────────── private void SetState(SyncState state, string? error = null) { Status = new SyncStatus { State = state, LastSyncAt = _queue.GetLastSyncAt(), PendingEvents = _queue.PendingCount(), ConflictCount = _queue.ConflictCount(), ErrorMessage = error, }; StatusChanged?.Invoke(Status); } private void UpdateStatus() => SetState(Status.State); public void Dispose() { _timer.Dispose(); _queue.Dispose(); } } public class SyncConfig { public string ServerUrl { get; set; } = ""; public string DeviceId { get; set; } = ""; public DeviceType DeviceType { get; set; } = DeviceType.Desktop; public int AutoSyncIntervalMinutes { get; set; } = 5; } public class SyncResult { public bool Success { get; set; } public bool Skipped { get; set; } public string? Reason { get; set; } public int EventsPushed { get; set; } public int EventsPulled { get; set; } public int Conflicts { get; set; } }