using System.Security.Claims; using ChessCubing.App.Models.Social; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.SignalR.Client; namespace ChessCubing.App.Services; public sealed class SocialRealtimeService( NavigationManager navigation, AuthenticationStateProvider authenticationStateProvider) : IAsyncDisposable { private readonly NavigationManager _navigation = navigation; private readonly AuthenticationStateProvider _authenticationStateProvider = authenticationStateProvider; private readonly SemaphoreSlim _gate = new(1, 1); private HubConnection? _hubConnection; private string? _currentSubject; private HashSet _knownPresenceSubjects = new(StringComparer.Ordinal); private HashSet _onlineSubjects = new(StringComparer.Ordinal); private PlayInviteMessage? _incomingPlayInvite; private PlayInviteMessage? _outgoingPlayInvite; private PlaySessionResponse? _activePlaySession; private string? _lastInviteNotice; private int _socialVersion; private bool _started; public event Action? Changed; public PlayInviteMessage? IncomingPlayInvite => _incomingPlayInvite; public PlayInviteMessage? OutgoingPlayInvite => _outgoingPlayInvite; public string? LastInviteNotice => _lastInviteNotice; public int SocialVersion => _socialVersion; public async Task EnsureStartedAsync() { if (_started) { await SyncConnectionAsync(); return; } _started = true; _authenticationStateProvider.AuthenticationStateChanged += HandleAuthenticationStateChanged; await SyncConnectionAsync(); } public bool IsUserOnline(string subject) => _onlineSubjects.Contains(subject); public bool? GetKnownOnlineStatus(string subject) { if (_onlineSubjects.Contains(subject)) { return true; } return _knownPresenceSubjects.Contains(subject) ? false : null; } public async Task SendPlayInviteAsync(string recipientSubject, string recipientColor) { _lastInviteNotice = null; if (_hubConnection is null) { throw new InvalidOperationException("La connexion temps reel n'est pas prete."); } var invite = await _hubConnection.InvokeAsync("SendPlayInvite", recipientSubject, recipientColor); ApplyInvite(invite); RaiseChanged(); } public async Task RespondToIncomingPlayInviteAsync(bool accept) { if (_hubConnection is null || _incomingPlayInvite is null) { return; } var inviteId = _incomingPlayInvite.InviteId; await _hubConnection.InvokeAsync("RespondToPlayInvite", inviteId, accept); if (!accept) { _incomingPlayInvite = null; RaiseChanged(); } } public async Task CancelOutgoingPlayInviteAsync() { if (_hubConnection is null || _outgoingPlayInvite is null) { return; } var inviteId = _outgoingPlayInvite.InviteId; await _hubConnection.InvokeAsync("CancelPlayInvite", inviteId); } public PlaySessionResponse? TakeAcceptedPlaySession() { var session = _activePlaySession; _activePlaySession = null; return session; } public void ClearInviteNotice() { if (string.IsNullOrWhiteSpace(_lastInviteNotice)) { return; } _lastInviteNotice = null; RaiseChanged(); } public async ValueTask DisposeAsync() { _authenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged; await _gate.WaitAsync(); try { if (_hubConnection is not null) { await _hubConnection.DisposeAsync(); _hubConnection = null; } } finally { _gate.Release(); _gate.Dispose(); } } private void HandleAuthenticationStateChanged(Task authenticationStateTask) => _ = SyncConnectionAsync(); private async Task SyncConnectionAsync() { await _gate.WaitAsync(); try { var authState = await _authenticationStateProvider.GetAuthenticationStateAsync(); var user = authState.User; var subject = ResolveSubject(user); if (string.IsNullOrWhiteSpace(subject)) { await DisconnectUnsafeAsync(); return; } if (_hubConnection is not null && string.Equals(subject, _currentSubject, StringComparison.Ordinal) && _hubConnection.State is HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting) { return; } await DisconnectUnsafeAsync(); _currentSubject = subject; _hubConnection = BuildHubConnection(); RegisterHandlers(_hubConnection); await _hubConnection.StartAsync(); } finally { _gate.Release(); } } private HubConnection BuildHubConnection() => new HubConnectionBuilder() .WithUrl(_navigation.ToAbsoluteUri("/hubs/social")) .WithAutomaticReconnect() .Build(); private void RegisterHandlers(HubConnection connection) { connection.On("PresenceSnapshot", message => { _knownPresenceSubjects = message.OnlineSubjects.ToHashSet(StringComparer.Ordinal); _onlineSubjects = message.OnlineSubjects.ToHashSet(StringComparer.Ordinal); RaiseChanged(); }); connection.On("PresenceChanged", message => { _knownPresenceSubjects.Add(message.Subject); if (message.IsOnline) { _onlineSubjects.Add(message.Subject); } else { _onlineSubjects.Remove(message.Subject); } RaiseChanged(); }); connection.On("SocialChanged", async () => { Interlocked.Increment(ref _socialVersion); await RequestPresenceSnapshotAsync(); RaiseChanged(); }); connection.On("PlayInviteUpdated", message => { ApplyInvite(message); RaiseChanged(); }); connection.On("PlayInviteClosed", message => { if (_incomingPlayInvite?.InviteId == message.InviteId) { _incomingPlayInvite = null; } if (_outgoingPlayInvite?.InviteId == message.InviteId) { _outgoingPlayInvite = null; } _lastInviteNotice = message.Message; RaiseChanged(); }); connection.On("PlayInviteAccepted", session => { _incomingPlayInvite = null; _outgoingPlayInvite = null; _activePlaySession = session; _lastInviteNotice = "La partie est confirmee. Les noms ont ete pre-remplis dans l'application."; RaiseChanged(); }); connection.Reconnected += async _ => { await RequestPresenceSnapshotAsync(); RaiseChanged(); }; connection.Closed += async _ => { if (!string.IsNullOrWhiteSpace(_currentSubject)) { await SyncConnectionAsync(); } }; } private async Task RequestPresenceSnapshotAsync() { if (_hubConnection is null || _hubConnection.State != HubConnectionState.Connected) { return; } try { await _hubConnection.InvokeAsync("RequestPresenceSnapshot"); } catch { // La vue peut continuer avec le dernier etat connu si le snapshot echoue ponctuellement. } } private void ApplyInvite(PlayInviteMessage invite) { if (string.IsNullOrWhiteSpace(_currentSubject)) { return; } if (string.Equals(invite.RecipientSubject, _currentSubject, StringComparison.Ordinal)) { _incomingPlayInvite = invite; } if (string.Equals(invite.SenderSubject, _currentSubject, StringComparison.Ordinal)) { _outgoingPlayInvite = invite; } } private async Task DisconnectUnsafeAsync() { _currentSubject = null; _knownPresenceSubjects.Clear(); _onlineSubjects.Clear(); _incomingPlayInvite = null; _outgoingPlayInvite = null; _activePlaySession = null; _lastInviteNotice = null; if (_hubConnection is not null) { await _hubConnection.DisposeAsync(); _hubConnection = null; } RaiseChanged(); } private void RaiseChanged() => Changed?.Invoke(); private static string? ResolveSubject(ClaimsPrincipal user) => user.Identity?.IsAuthenticated == true ? user.FindFirst("sub")?.Value ?? user.FindFirst(ClaimTypes.NameIdentifier)?.Value : null; }