using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Serialization; using Campaign_Tracker.Server.Seed.Models; namespace Campaign_Tracker.Server.Seed; public sealed class FileSeedDataStore : ISeedDataStore { private static readonly ConcurrentDictionary FileLocks = new( StringComparer.OrdinalIgnoreCase); private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true, Converters = { new JsonStringEnumConverter() }, }; private readonly string _path; private readonly SemaphoreSlim _sync; public FileSeedDataStore(string path) { ArgumentException.ThrowIfNullOrWhiteSpace(path); _path = Path.GetFullPath(path); _sync = FileLocks.GetOrAdd(_path, _ => new SemaphoreSlim(1, 1)); } public async Task UpsertSeedDataAsync(SeedDataSet seedData, CancellationToken cancellationToken = default) { await _sync.WaitAsync(cancellationToken); try { var snapshot = await LoadAsync(cancellationToken); InMemorySeedDataStore.UpsertMissing( snapshot.ReferenceValues, seedData.ReferenceValues, InMemorySeedDataStore.Clone); InMemorySeedDataStore.UpsertMissing( snapshot.RequiredFieldRules, seedData.RequiredFieldRules, InMemorySeedDataStore.Clone); InMemorySeedDataStore.UpsertMissing( snapshot.EscalationRules, seedData.EscalationRules, InMemorySeedDataStore.Clone); await SaveAsync(snapshot, cancellationToken); } finally { _sync.Release(); } } public async Task> GetReferenceValuesAsync(CancellationToken cancellationToken = default) { await _sync.WaitAsync(cancellationToken); try { var snapshot = await LoadAsync(cancellationToken); return snapshot.ReferenceValues.Select(InMemorySeedDataStore.Clone).ToArray(); } finally { _sync.Release(); } } public async Task> GetRequiredFieldRulesAsync(CancellationToken cancellationToken = default) { await _sync.WaitAsync(cancellationToken); try { var snapshot = await LoadAsync(cancellationToken); return snapshot.RequiredFieldRules.Select(InMemorySeedDataStore.Clone).ToArray(); } finally { _sync.Release(); } } public async Task> GetEscalationRulesAsync(CancellationToken cancellationToken = default) { await _sync.WaitAsync(cancellationToken); try { var snapshot = await LoadAsync(cancellationToken); return snapshot.EscalationRules.Select(InMemorySeedDataStore.Clone).ToArray(); } finally { _sync.Release(); } } public async Task SaveReferenceValueAsync( ReferenceValue referenceValue, CancellationToken cancellationToken = default) { await _sync.WaitAsync(cancellationToken); try { var snapshot = await LoadAsync(cancellationToken); InMemorySeedDataStore.ReplaceBySeedKey( snapshot.ReferenceValues, referenceValue, InMemorySeedDataStore.Clone); await SaveAsync(snapshot, cancellationToken); } finally { _sync.Release(); } } public async Task SaveRequiredFieldRuleAsync( RequiredFieldRule rule, CancellationToken cancellationToken = default) { await _sync.WaitAsync(cancellationToken); try { var snapshot = await LoadAsync(cancellationToken); InMemorySeedDataStore.ReplaceBySeedKey( snapshot.RequiredFieldRules, rule, InMemorySeedDataStore.Clone); await SaveAsync(snapshot, cancellationToken); } finally { _sync.Release(); } } public async Task SaveEscalationRuleAsync( EscalationRule rule, CancellationToken cancellationToken = default) { await _sync.WaitAsync(cancellationToken); try { var snapshot = await LoadAsync(cancellationToken); InMemorySeedDataStore.ReplaceBySeedKey( snapshot.EscalationRules, rule, InMemorySeedDataStore.Clone); await SaveAsync(snapshot, cancellationToken); } finally { _sync.Release(); } } private async Task LoadAsync(CancellationToken cancellationToken) { if (!File.Exists(_path)) { return new InMemorySeedDataStore.SeedDataSnapshot(); } await using var stream = File.OpenRead(_path); var snapshot = await JsonSerializer.DeserializeAsync( stream, SerializerOptions, cancellationToken); return snapshot ?? new InMemorySeedDataStore.SeedDataSnapshot(); } private async Task SaveAsync( InMemorySeedDataStore.SeedDataSnapshot snapshot, CancellationToken cancellationToken) { var directory = Path.GetDirectoryName(_path); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } var tempPath = $"{_path}.{Guid.NewGuid():N}.tmp"; await using (var stream = File.Create(tempPath)) { await JsonSerializer.SerializeAsync(stream, snapshot, SerializerOptions, cancellationToken); } File.Move(tempPath, _path, overwrite: true); } }