using System.Collections.Concurrent; namespace Campaign_Tracker.Server.Municipalities; public sealed class InMemoryMunicipalityContactRepository : IMunicipalityContactRepository { private static readonly Dictionary ContactTypeSortOrder = new(StringComparer.OrdinalIgnoreCase) { ["Primary"] = 0, ["Secondary"] = 1, }; private readonly ConcurrentDictionary _contacts = new(StringComparer.OrdinalIgnoreCase); private readonly object _lock = new(); private readonly TimeProvider _timeProvider; public InMemoryMunicipalityContactRepository(TimeProvider timeProvider) { _timeProvider = timeProvider; } public Task> GetByProfileIdAsync( string profileId, CancellationToken cancellationToken = default) { var result = _contacts.Values .Where(c => c.ProfileId == profileId && !c.IsDeleted) .OrderBy(c => ContactTypeSortOrder.GetValueOrDefault(c.ContactType, 99)) .ThenBy(c => c.Name, StringComparer.OrdinalIgnoreCase) .ToArray(); return Task.FromResult>(result); } public Task GetByIdAsync( string contactId, CancellationToken cancellationToken = default) { _contacts.TryGetValue(contactId, out var contact); return Task.FromResult(contact is { IsDeleted: false } ? contact : null); } public Task AddAsync( string profileId, string contactType, string name, string? roleTitle, string? phone, string? email, string actorIdentity, CancellationToken cancellationToken = default) { var validation = Validate(contactType, name); if (validation is not null) return Task.FromResult(MunicipalityContactSaveResult.Failure(validation)); var now = _timeProvider.GetUtcNow(); var contact = new MunicipalityContact( ContactId: Guid.NewGuid().ToString("N"), ProfileId: profileId, ContactType: NormalizeContactType(contactType), Name: name.Trim(), RoleTitle: NormalizeOptional(roleTitle), Phone: NormalizeOptional(phone), Email: NormalizeOptional(email), IsDeleted: false, CreatedAt: now, CreatedBy: actorIdentity, UpdatedAt: now, UpdatedBy: actorIdentity); _contacts[contact.ContactId] = contact; return Task.FromResult(MunicipalityContactSaveResult.Success(contact)); } public Task UpdateAsync( string contactId, string contactType, string name, string? roleTitle, string? phone, string? email, string actorIdentity, CancellationToken cancellationToken = default) { var validation = Validate(contactType, name); if (validation is not null) return Task.FromResult(MunicipalityContactSaveResult.Failure(validation)); var now = _timeProvider.GetUtcNow(); lock (_lock) { if (!_contacts.TryGetValue(contactId, out var existing) || existing.IsDeleted) return Task.FromResult(MunicipalityContactSaveResult.NotFound(contactId)); var updated = existing with { ContactType = NormalizeContactType(contactType), Name = name.Trim(), RoleTitle = NormalizeOptional(roleTitle), Phone = NormalizeOptional(phone), Email = NormalizeOptional(email), UpdatedAt = now, UpdatedBy = actorIdentity, }; _contacts[contactId] = updated; return Task.FromResult(MunicipalityContactSaveResult.Success(updated)); } } public Task SoftDeleteAsync( string contactId, string actorIdentity, CancellationToken cancellationToken = default) { var now = _timeProvider.GetUtcNow(); lock (_lock) { if (!_contacts.TryGetValue(contactId, out var existing) || existing.IsDeleted) return Task.FromResult(MunicipalityContactSaveResult.NotFound(contactId)); var deleted = existing with { IsDeleted = true, UpdatedAt = now, UpdatedBy = actorIdentity, }; _contacts[contactId] = deleted; return Task.FromResult(MunicipalityContactSaveResult.Success(deleted)); } } private static string? Validate(string contactType, string name) { if (string.IsNullOrWhiteSpace(name)) return "Name is required."; if (string.IsNullOrWhiteSpace(contactType)) return "Contact type is required."; if (!ContactTypeSortOrder.ContainsKey(contactType.Trim())) return "Contact type must be Primary or Secondary."; return null; } private static string NormalizeContactType(string contactType) => string.Equals(contactType.Trim(), "Primary", StringComparison.OrdinalIgnoreCase) ? "Primary" : "Secondary"; private static string? NormalizeOptional(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); }