using System.Net.Http.Json;
using LehrerApp.Data;
using LehrerApp.Sync.Crypto;
using LehrerApp.Sync.Models;
namespace LehrerApp.Sync;
///
/// Device-Pairing via verschlüsseltem Snapshot + Einmal-Code.
///
/// Sender: CreateAndUploadAsync() → zeigt Code dem Nutzer
/// Empfänger: RestoreFromCodeAsync(code) → entschlüsselt Schlüssel + DB
///
public class SnapshotService(
HttpClient http, LiteDbContext db, byte[] syncKey,
DeviceType deviceType, string dbPath, string keyPath)
{
public event Action? ProgressChanged;
public async Task CreateAndUploadAsync(CancellationToken ct = default)
{
Report(SnapshotStep.Checkpointing, "Datenbank wird gesichert…");
db.Checkpoint();
Report(SnapshotStep.Reading, "Datenbank wird gelesen…");
var dbBytes = await File.ReadAllBytesAsync(dbPath, ct);
Report(SnapshotStep.Encrypting, "Verschlüsselung läuft…");
var encPayload = Convert.ToBase64String(SyncCrypto.Encrypt(dbBytes, syncKey));
// Schritt 1: Upload ohne Key → Code erhalten
Report(SnapshotStep.Uploading, "Code wird angefordert…");
using var request1 = SyncProtocol.CreateRequest(HttpMethod.Post, "/api/snapshot/upload");
request1.Content = JsonContent.Create(
new SnapshotUploadRequest { EncryptedPayload = encPayload, DeviceType = deviceType });
using var r1 = await http.SendAsync(request1, ct);
await SyncProtocol.EnsureCompatibleSuccessAsync(r1);
var init = await r1.Content.ReadFromJsonAsync(ct)
?? throw new InvalidOperationException("Leere Server-Antwort.");
// Schritt 2: Key mit Code verschlüsseln + erneut hochladen
Report(SnapshotStep.Uploading, "Schlüssel wird verschlüsselt…");
var encKey = SyncCrypto.EncryptKeyWithCode(syncKey, init.Code);
using var request2 = SyncProtocol.CreateRequest(HttpMethod.Post, "/api/snapshot/upload");
request2.Content = JsonContent.Create(
new SnapshotUploadRequest { EncryptedPayload = encPayload,
EncryptedSyncKey = encKey, DeviceType = deviceType,
Code = init.Code });
using var r2 = await http.SendAsync(request2, ct);
await SyncProtocol.EnsureCompatibleSuccessAsync(r2);
var result = await r2.Content.ReadFromJsonAsync(ct)
?? throw new InvalidOperationException("Leere Server-Antwort.");
Report(SnapshotStep.Done, $"Bereit – Code: {result.Code}");
return result;
}
public async Task RestoreFromCodeAsync(string code, string targetDbPath,
CancellationToken ct = default)
{
var sanitized = code.Trim().ToUpperInvariant();
Report(SnapshotStep.Downloading, "Snapshot wird geladen…");
using var request = SyncProtocol.CreateRequest(HttpMethod.Get, $"/api/snapshot/{sanitized}");
using var resp = await http.SendAsync(request, ct);
if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
throw new SnapshotNotFoundException($"Code '{sanitized}' nicht gefunden oder abgelaufen.");
await SyncProtocol.EnsureCompatibleSuccessAsync(resp);
var dl = await resp.Content.ReadFromJsonAsync(ct)
?? throw new InvalidOperationException("Leere Server-Antwort.");
Report(SnapshotStep.Decrypting, "Schlüssel wird entschlüsselt…");
byte[] restoredKey;
try { restoredKey = SyncCrypto.DecryptKeyWithCode(dl.EncryptedSyncKey, sanitized); }
catch (System.Security.Cryptography.CryptographicException)
{ throw new InvalidOperationException("Schlüssel-Entschlüsselung fehlgeschlagen – Code korrekt?"); }
SyncCrypto.SaveKey(restoredKey, keyPath);
Report(SnapshotStep.Decrypting, "Datenbank wird entschlüsselt…");
var dbBytes = SyncCrypto.Decrypt(Convert.FromBase64String(dl.EncryptedPayload), restoredKey);
Report(SnapshotStep.Writing, "Datenbank wird geschrieben…");
Directory.CreateDirectory(Path.GetDirectoryName(targetDbPath)!);
if (File.Exists(targetDbPath))
File.Move(targetDbPath, $"{targetDbPath}.backup-{DateTime.Now:yyyyMMdd-HHmmss}");
await File.WriteAllBytesAsync(targetDbPath, dbBytes, ct);
Report(SnapshotStep.Done, "Abgeschlossen – bitte App neu starten.");
return restoredKey;
}
private void Report(SnapshotStep step, string msg) =>
ProgressChanged?.Invoke(new(step, msg));
}
public record SnapshotProgress(SnapshotStep Step, string Message)
{
public int Percent => Step switch
{
SnapshotStep.Checkpointing => 10, SnapshotStep.Reading => 20,
SnapshotStep.Encrypting => 35, SnapshotStep.Uploading => 60,
SnapshotStep.Downloading => 35, SnapshotStep.Decrypting => 65,
SnapshotStep.Writing => 85, SnapshotStep.Done => 100,
_ => 0,
};
public bool IsComplete => Step == SnapshotStep.Done;
}
public enum SnapshotStep { Idle, Checkpointing, Reading, Encrypting,
Uploading, Downloading, Decrypting, Writing, Done }
public class SnapshotNotFoundException(string msg) : Exception(msg);