using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using SameBeat.Api.Monitoring;
namespace SameBeat.Api.Hubs;
///
/// Hub stanu imprezy -- kanal na zdarzenia sterujace (kto jest na scenie, czyj jest mikrofon, co gra
/// w tle). WAZNE: obraz/wideo NIGDY nie idzie przez SignalR -- to wylacznie LiveKit (WebRTC).
/// SignalR przenosi tylko metadane/zdarzenia sterujace, zeby nie dublowac transportu mediow.
///
/// Szkielet na Etap 1 -- polaczenie i grupa "widzowie". ETAP 3 dodal pierwsze eventy sceny --
/// ScenaZmieniona, UczestnikDodanyDoSceny, UczestnikUsunietyZeSceny, UkladScenyZmieniony -- ale
/// rozglaszane sa wprost przez Clients.All z HostStageController (payload zawsze StanScenyDto:
/// numerSlotu+nick, NIGDY uczestnikId), nie przez metody tego hub'a. ETAP 4 dodal analogicznie
/// eventy muzyki -- StanMuzykiZmieniony (payload StanMuzykiDto, po kazdej akcji zegara: play/pause/
/// next/poprzedni/glosnosc) i MuzykaZmieniona (payload List<PozycjaPlaylistyDto>, po zmianie
/// biblioteki/kolejki) -- rozglaszane wprost przez Clients.All z HostMusicController, tak samo bez
/// posrednictwa metod tego hub'a. ETAP 5 dodal eventy mikrofonu -- MikrofonPrzyznany i MikrofonOdebrany
/// (payload zawsze StanMikrofonuDto: numerSlotu+nick+koniecUtc, NIGDY uczestnikId, ten sam kontrakt co
/// scena) -- rozglaszane z HostMicrophoneController PRZY AKCJI HOSTA, ale takze -- to jest wyjatek od
/// reguly "kontroler rozglasza" -- przy autorytatywnym WYGASNIECIU CZASU, ktore nie ma zadnego
/// kontrolera: robi to SerwisTimeraMikrofonu (Infrastructure/Audio, BackgroundService dzialajacy w tle)
/// przez waski adapter IRozgloszenieZdarzenImprezy (Api/Hubs/RozgloszenieZdarzenImprezy.cs), zeby
/// Infrastructure nie musialo znac tej klasy Hub. ETAP 6 dodal eventy czatu -- ChatNowaWiadomosc
/// (payload WiadomoscCzatuDto: id+nick+tresc+wyslanoUtc, NIGDY uczestnikCzatuId, ten sam kontrakt co
/// scena/mikrofon) i ChatWiadomoscUsunieta (payload {id}) -- rozglaszane wprost przez Clients.All z
/// ChatController/HostChatController, bez posrednictwa metod tego hub'a. W kolejnych etapach dojdzie
/// tu jeszcze: UzytkownikWyrzucony, UzytkownikZbanowany. ETAP 8 dodal LicznikPolaczenSignalR (Api/Monitoring)
/// -- prosty licznik w pamieci procesu, aktualizowany tutaj (OnConnectedAsync/OnDisconnectedAsync/
/// DolaczDoGrupyHosta), odczytywany przez GET /api/health (liczba ogolem) i GET /api/host/diagnostyka
/// (rozbicie na "widzowie"/"host").
///
public class StanImprezyHub : Hub
{
private const string GrupaWidzowie = "widzowie";
/// Grupa panelu hosta -- odbiera zdarzenia sterujace typu "cos sie zmienilo w poczekalni".
public const string GrupaHost = "host";
private readonly ILogger _logger;
private readonly LicznikPolaczenSignalR _licznikPolaczen;
public StanImprezyHub(ILogger logger, LicznikPolaczenSignalR licznikPolaczen)
{
_logger = logger;
_licznikPolaczen = licznikPolaczen;
}
///
/// Dolacza biezace polaczenie do grupy "host", zeby dostawac zdarzenia panelu hosta
/// (np. PoczekalniaZmieniona).
///
/// ETAP 7: [Authorize] na poziomie metody -- SignalR sprawdza Context.User (ustalony raz, przy
/// nawiazywaniu polaczenia/negocjacji, na podstawie DOKLADNIE tego samego mechanizmu co REST API:
/// JwtBearerEvents.OnMessageReceived w Program.cs wyciaga token z ciasteczka httpOnly "sb_auth").
/// Ciasteczko jest wysylane przez przegladarke automatycznie przy negocjacji SignalR I przy
/// upgrade'zie WebSocket (to zwykle zadania HTTP, nie inny mechanizm) -- warunkiem jest, zeby klient
/// laczyl sie z { withCredentials: true } (frontend to juz robi, patrz SignalRSerwis.ts) i zeby
/// origin frontendu byl dopuszczony w CORS z AllowCredentials (juz jest, patrz UstawieniaCors).
/// Nie jest potrzebny token w query string (?access_token=...) -- to obejscie dotyczy tylko
/// scenariusza "token w Authorization header", ktorego JS nie moze ustawic na natywnym WebSocket;
/// przy auth przez ciasteczko przegladarka i tak dolacza je sama do kazdego zadania HTTP/WS.
///
/// Gdy polaczajacy sie NIE jest zalogowanym hostem (brak/niewazne/wygasle ciasteczko), SignalR sam
/// odrzuca to wywolanie -- klient dostaje HubException z invoke(), polaczenie NIE jest zrywane
/// (nie jest przez to widzem odciety od grupy "widzowie"), po prostu nie trafia do grupy "host".
///
/// ETAP 8: bezpieczne do wielokrotnego wywolania na tym samym polaczeniu -- Groups.AddToGroupAsync
/// jest idempotentne (dodanie juz obecnego polaczenia do grupy nie jest bledem), a LicznikPolaczenSignalR
/// nizej sledzi polaczenia w zbiorze (ConcurrentDictionary keyed po ConnectionId), wiec powtorne
/// wywolanie (np. po automatycznym reconnect po stronie klienta, patrz komentarz w OnConnectedAsync)
/// nie zawyza licznika. Frontend MUSI sam wywolac to ponownie po kazdym odtworzeniu polaczenia
/// SignalR (np. w handlerze .onreconnected() klienta) -- serwer nie pamieta "byl hostem przed
/// rozlaczeniem", bo kazde nowe polaczenie (rowniez reconnect) dostaje nowy ConnectionId i startuje
/// wylacznie w grupie "widzowie", patrz OnConnectedAsync.
///
[Authorize]
public async Task DolaczDoGrupyHosta()
{
await Groups.AddToGroupAsync(Context.ConnectionId, GrupaHost);
_licznikPolaczen.DolaczylDoGrupyHosta(Context.ConnectionId);
}
public override async Task OnConnectedAsync()
{
_licznikPolaczen.Polaczono(Context.ConnectionId);
// OGRANICZENIE ETAP 8: SignalR na serwerze NIE rozroznia natywnie "nowego" polaczenia od
// "reconnect" (automatyczny reconnect klienta z ETAPU 1 po prostu zamyka stare polaczenie i
// otwiera nowe, z nowym ConnectionId -- z punktu widzenia OnConnectedAsync to zawsze wyglada
// identycznie). Nie zgadujemy tego po iloĊci/czasie -- log ponizej pokazuje wiec tylko fakt
// nawiazania polaczenia i aktualna liczbe polaczen ogolem, bez etykiety "nowe"/"reconnect".
_logger.LogInformation(
"Polaczenie SignalR nawiazane: {ConnectionId}. Polaczen ogolem: {LiczbaPolaczen}.",
Context.ConnectionId, _licznikPolaczen.LiczbaPolaczenOgolem);
// Kazde nowe polaczenie domyslnie trafia do grupy widzow -- w kolejnych etapach przeniesienie
// do innej grupy (np. "scena") bedzie nastepowac na podstawie roli po zalogowaniu.
await Groups.AddToGroupAsync(Context.ConnectionId, GrupaWidzowie);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? wyjatek)
{
_licznikPolaczen.Rozlaczono(Context.ConnectionId);
if (wyjatek is not null)
{
_logger.LogWarning(wyjatek,
"Polaczenie SignalR rozlaczone z bledem: {ConnectionId}. Polaczen ogolem: {LiczbaPolaczen}.",
Context.ConnectionId, _licznikPolaczen.LiczbaPolaczenOgolem);
}
else
{
_logger.LogInformation(
"Polaczenie SignalR rozlaczone: {ConnectionId}. Polaczen ogolem: {LiczbaPolaczen}.",
Context.ConnectionId, _licznikPolaczen.LiczbaPolaczenOgolem);
}
await base.OnDisconnectedAsync(wyjatek);
}
}