using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.CharaData; using LightlessSync.API.Dto.User; using Microsoft.Extensions.Logging; namespace LightlessSync.PlayerData.Pairs; public sealed class PairHandlerRegistry : IDisposable { private readonly object _gate = new(); private readonly Dictionary _identToHandler = new(StringComparer.Ordinal); private readonly Dictionary> _handlerToPairs = new(); private readonly Dictionary _waitingRequests = new(StringComparer.Ordinal); private readonly IPairHandlerAdapterFactory _handlerFactory; private readonly PairManager _pairManager; private readonly PairStateCache _pairStateCache; private readonly ILogger _logger; private readonly TimeSpan _deletionGracePeriod = TimeSpan.FromMinutes(5); private readonly TimeSpan _waitForHandlerGracePeriod = TimeSpan.FromMinutes(2); public PairHandlerRegistry( IPairHandlerAdapterFactory handlerFactory, PairManager pairManager, PairStateCache pairStateCache, ILogger logger) { _handlerFactory = handlerFactory; _pairManager = pairManager; _pairStateCache = pairStateCache; _logger = logger; } public int GetVisibleUsersCount() { lock (_gate) { return _handlerToPairs.Keys.Count(handler => handler.IsVisible); } } public bool IsIdentVisible(string ident) { lock (_gate) { return _identToHandler.TryGetValue(ident, out var handler) && handler.IsVisible; } } public PairOperationResult RegisterOnlinePair(PairRegistration registration) { if (registration.CharacterIdent is null) { return PairOperationResult.Fail($"Registration for {registration.PairIdent.UserId} missing ident."); } IPairHandlerAdapter handler; lock (_gate) { handler = GetOrAddHandler(registration.CharacterIdent); handler.ScheduledForDeletion = false; if (!_handlerToPairs.TryGetValue(handler, out var set)) { set = new HashSet(); _handlerToPairs[handler] = set; } set.Add(registration.PairIdent); } ApplyPauseStateForHandler(handler); if (handler.LastReceivedCharacterData is null) { var cachedData = _pairStateCache.TryLoad(registration.CharacterIdent); if (cachedData is not null) { handler.LoadCachedCharacterData(cachedData); } } if (handler.LastReceivedCharacterData is not null && (handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0)) { handler.ApplyLastReceivedData(forced: true); } return PairOperationResult.Ok(registration.PairIdent); } public PairOperationResult DeregisterOfflinePair(PairRegistration registration, bool forceDisposal = false) { if (registration.CharacterIdent is null) { return PairOperationResult.Fail($"Deregister for {registration.PairIdent.UserId} missing ident."); } IPairHandlerAdapter? handler = null; bool shouldScheduleRemoval = false; bool shouldDisposeImmediately = false; lock (_gate) { if (!_identToHandler.TryGetValue(registration.CharacterIdent, out handler)) { return PairOperationResult.Fail($"Ident {registration.CharacterIdent} not registered."); } if (_handlerToPairs.TryGetValue(handler, out var set)) { set.Remove(registration.PairIdent); if (set.Count == 0) { if (forceDisposal) { shouldDisposeImmediately = true; } else { shouldScheduleRemoval = true; handler.ScheduledForDeletion = true; } } } } if (shouldDisposeImmediately && handler is not null) { if (TryFinalizeHandlerRemoval(handler)) { handler.Dispose(); } } else if (shouldScheduleRemoval && handler is not null) { _ = RemoveAfterGracePeriodAsync(handler); } return PairOperationResult.Ok(registration.PairIdent); } public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto) { if (registration.CharacterIdent is null) { return PairOperationResult.Fail($"Character data received without ident for {registration.PairIdent.UserId}."); } IPairHandlerAdapter? handler; lock (_gate) { _identToHandler.TryGetValue(registration.CharacterIdent, out handler); } if (handler is null) { var registerResult = RegisterOnlinePair(registration); if (!registerResult.Success) { return PairOperationResult.Fail(registerResult.Error); } lock (_gate) { _identToHandler.TryGetValue(registration.CharacterIdent, out handler); } } if (handler is null) { return PairOperationResult.Fail($"Handler not ready for {registration.PairIdent.UserId}."); } handler.ApplyData(dto.CharaData); return PairOperationResult.Ok(); } public PairOperationResult ApplyLastReceivedData(PairUniqueIdentifier pairIdent, string ident, bool forced = false) { IPairHandlerAdapter? handler; lock (_gate) { _identToHandler.TryGetValue(ident, out handler); } if (handler is null) { return PairOperationResult.Fail($"Cannot reapply data: handler for {pairIdent.UserId} not found."); } handler.ApplyLastReceivedData(forced); return PairOperationResult.Ok(); } public PairOperationResult SetUploading(PairUniqueIdentifier pairIdent, string ident, bool uploading) { IPairHandlerAdapter? handler; lock (_gate) { _identToHandler.TryGetValue(ident, out handler); } if (handler is null) { return PairOperationResult.Fail($"Cannot set uploading for {pairIdent.UserId}: handler not found."); } handler.SetUploading(uploading); return PairOperationResult.Ok(); } public PairOperationResult SetPausedState(PairUniqueIdentifier pairIdent, string ident, bool paused) { IPairHandlerAdapter? handler; lock (_gate) { _identToHandler.TryGetValue(ident, out handler); } if (handler is null) { return PairOperationResult.Fail($"Cannot update pause state for {pairIdent.UserId}: handler not found."); } _ = paused; // value reflected in pair manager already // Recalculate pause state against all registered pairs to ensure consistency across contexts. ApplyPauseStateForHandler(handler); return PairOperationResult.Ok(); } public PairOperationResult> GetPairConnections(string ident) { IPairHandlerAdapter? handler; HashSet? identifiers = null; lock (_gate) { _identToHandler.TryGetValue(ident, out handler); if (handler is not null) { _handlerToPairs.TryGetValue(handler, out identifiers); } } if (handler is null || identifiers is null) { return PairOperationResult>.Fail($"No handler registered for {ident}."); } var list = new List<(PairUniqueIdentifier, PairConnection)>(); foreach (var pairIdent in identifiers) { var result = _pairManager.GetPair(pairIdent.UserId); if (result.Success) { list.Add((pairIdent, result.Value)); } } return PairOperationResult>.Ok(list); } private void ApplyPauseStateForHandler(IPairHandlerAdapter handler) { var pairs = _pairManager.GetPairsByIdent(handler.Ident); bool paused = pairs.Any(p => p.SelfToOtherPermissions.IsPaused() || p.OtherToSelfPermissions.IsPaused()); handler.SetPaused(paused); } internal bool TryGetHandler(string ident, out IPairHandlerAdapter? handler) { lock (_gate) { var success = _identToHandler.TryGetValue(ident, out var resolved); handler = resolved; return success; } } internal IReadOnlyList GetHandlerSnapshot() { lock (_gate) { return _identToHandler.Values.Distinct().ToList(); } } internal IReadOnlyCollection GetRegisteredPairs(IPairHandlerAdapter handler) { lock (_gate) { if (_handlerToPairs.TryGetValue(handler, out var pairs)) { return pairs.ToList(); } } return Array.Empty(); } internal void ReapplyAll(bool forced = false) { var handlers = GetHandlerSnapshot(); foreach (var handler in handlers) { try { handler.ApplyLastReceivedData(forced); } catch (Exception ex) { if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug(ex, "Failed to reapply cached data for {Ident}", handler.Ident); } } } } internal void ResetAllHandlers() { List handlers; lock (_gate) { handlers = _identToHandler.Values.Distinct().ToList(); _identToHandler.Clear(); _handlerToPairs.Clear(); foreach (var pending in _waitingRequests.Values) { pending.Cancel(); pending.Dispose(); } _waitingRequests.Clear(); } foreach (var handler in handlers) { try { handler.Dispose(); } catch (Exception ex) { if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug(ex, "Failed to dispose handler for {Ident}", handler.Ident); } } } } public void Dispose() { List handlers; lock (_gate) { handlers = _identToHandler.Values.Distinct().ToList(); _identToHandler.Clear(); _handlerToPairs.Clear(); foreach (var kv in _waitingRequests.Values) { kv.Cancel(); } _waitingRequests.Clear(); } foreach (var handler in handlers) { handler.Dispose(); } } private IPairHandlerAdapter GetOrAddHandler(string ident) { if (_identToHandler.TryGetValue(ident, out var handler)) { return handler; } handler = _handlerFactory.Create(ident); _identToHandler[ident] = handler; _handlerToPairs[handler] = new HashSet(); return handler; } private void EnsureInitialized(IPairHandlerAdapter handler) { if (handler.Initialized) { return; } try { handler.Initialize(); } catch (Exception ex) { _logger.LogError(ex, "Failed to initialize handler for {Ident}", handler.Ident); } } private async Task RemoveAfterGracePeriodAsync(IPairHandlerAdapter handler) { try { await Task.Delay(_deletionGracePeriod).ConfigureAwait(false); } catch (TaskCanceledException) { return; } if (TryFinalizeHandlerRemoval(handler)) { handler.Dispose(); } } private bool TryFinalizeHandlerRemoval(IPairHandlerAdapter handler) { lock (_gate) { if (!_handlerToPairs.TryGetValue(handler, out var set) || set.Count > 0) { handler.ScheduledForDeletion = false; return false; } _handlerToPairs.Remove(handler); _identToHandler.Remove(handler.Ident); if (_waitingRequests.TryGetValue(handler.Ident, out var cts)) { cts.Cancel(); cts.Dispose(); _waitingRequests.Remove(handler.Ident); } return true; } } private async Task WaitThenApplyDataAsync(PairRegistration registration, OnlineUserCharaDataDto dto, CancellationTokenSource cts) { var token = cts.Token; try { while (!token.IsCancellationRequested) { IPairHandlerAdapter? handler; lock (_gate) { _identToHandler.TryGetValue(registration.CharacterIdent!, out handler); } if (handler is not null && handler.Initialized) { handler.ApplyData(dto.CharaData); break; } await Task.Delay(TimeSpan.FromMilliseconds(500), token).ConfigureAwait(false); } } catch (OperationCanceledException) { // expected } finally { lock (_gate) { if (_waitingRequests.TryGetValue(registration.CharacterIdent!, out var existing) && existing == cts) { _waitingRequests.Remove(registration.CharacterIdent!); } } cts.Dispose(); } } }