using LiteDB;
using LehrerApp.Sync.Models;
namespace LehrerApp.Api;
///
/// Server-seitiger Event-Speicher.
/// Speichert verschlüsselte Events pro User – versteht den Payload nicht.
/// Eine LiteDB-Datei pro User in dataPath/{userId}.db
///
public class EventStore
{
private readonly string _dataPath;
private readonly Dictionary _dbs = new();
private readonly Lock _lock = new();
public EventStore(string dataPath)
{
_dataPath = dataPath;
Directory.CreateDirectory(dataPath);
}
// ── Push: Events vom Client entgegennehmen ────────────────────────────────
public PushResponse Push(string userId, List events)
{
var db = GetDb(userId);
var col = db.GetCollection("events");
col.EnsureIndex(x => x.ServerSequenceNr);
var conflictIds = new List();
long serverSeq = GetLastSequenceNr(col);
foreach (var evt in events.OrderBy(e => e.Timestamp))
{
// Konflikt: anderes Gerät hat dieselbe Entity kürzlich geändert
var recent = col.FindOne(e =>
e.EntityType == evt.EntityType &&
e.EntityId == evt.EntityId &&
e.DeviceId != evt.DeviceId &&
e.Timestamp > evt.Timestamp.AddSeconds(-30));
if (recent is not null)
{
conflictIds.Add(evt.EventId);
continue;
}
col.Insert(new ServerEvent
{
EventId = evt.EventId,
DeviceId = evt.DeviceId,
DeviceType = evt.DeviceType,
Timestamp = evt.Timestamp,
ClientSequenceNr = evt.SequenceNr,
ServerSequenceNr = ++serverSeq,
EntityType = evt.EntityType,
EntityId = evt.EntityId,
Operation = evt.Operation,
Payload = evt.Payload, // verschlüsselt – Server liest nicht
});
}
return new PushResponse
{
Success = true,
ServerSequenceNr = serverSeq,
ConflictingEventIds = conflictIds,
};
}
// ── Pull: neue Events für Client bereitstellen ────────────────────────────
public PullResponse Pull(string userId, long since, string requestingDeviceId)
{
var db = GetDb(userId);
var col = db.GetCollection("events");
// Nur Events anderer Geräte zurückgeben
var events = col
.Find(e => e.ServerSequenceNr > since && e.DeviceId != requestingDeviceId)
.OrderBy(e => e.ServerSequenceNr)
.Take(500)
.Select(e => new SyncEvent
{
EventId = e.EventId,
DeviceId = e.DeviceId,
DeviceType = e.DeviceType,
Timestamp = e.Timestamp,
SequenceNr = e.ServerSequenceNr,
EntityType = e.EntityType,
EntityId = e.EntityId,
Operation = e.Operation,
Payload = e.Payload,
})
.ToList();
return new PullResponse
{
Events = events,
ServerSequenceNr = GetLastSequenceNr(col),
};
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
private LiteDatabase GetDb(string userId)
{
lock (_lock)
{
if (!_dbs.TryGetValue(userId, out var db))
{
// Sanitize userId für Dateinamen
var safeName = string.Concat(userId
.Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_'));
var path = Path.Combine(_dataPath, $"{safeName}.db");
db = new LiteDatabase(path);
_dbs[userId] = db;
}
return db;
}
}
private static long GetLastSequenceNr(ILiteCollection col)
{
var last = col.FindOne(Query.All(nameof(ServerEvent.ServerSequenceNr), Query.Descending));
return last?.ServerSequenceNr ?? 0;
}
}
// Internes Dokument im Server-EventStore
internal class ServerEvent
{
public Guid EventId { get; set; }
public string DeviceId { get; set; } = "";
public DeviceType DeviceType { get; set; }
public DateTime Timestamp { get; set; }
public long ClientSequenceNr { get; set; }
public long ServerSequenceNr { get; set; }
public string EntityType { get; set; } = "";
public string EntityId { get; set; } = "";
public string Operation { get; set; } = "";
public string Payload { get; set; } = ""; // AES-256 – Server liest nicht
}