using SameBeat.Domain.Enumy;
using SameBeat.Domain.Wyjatki;
namespace SameBeat.Domain.Encje;
///
/// Uczestnik oczekujacy na akceptacje hosta -- zanim host wpusci go na scene (ETAP 3), siedzi
/// w poczekalni z podgladem swojej kamery. Nick jest tu tez uzywany pozniej w czacie, dlatego
/// juz teraz jest sanityzowany (bez '<' i '>' -- zeby nie dalo sie wstrzyknac HTML).
///
public class UczestnikOczekujacy
{
public const int MaksymalnaDlugoscNicku = 40;
public Guid Id { get; private set; }
public Guid WydarzenieId { get; private set; }
public string Nick { get; private set; } = string.Empty;
/// Tozsamosc LiveKit -- generowana WYLACZNIE po stronie serwera, nigdy z inputu klienta.
public string LiveKitIdentity { get; private set; } = string.Empty;
public DateTime DolaczylUtc { get; private set; }
public StatusUczestnikaOczekujacego Status { get; private set; }
///
/// Hash (PBKDF2, jak haslo) sekretu sesji -- jedyny sposob, w jaki uczestnik moze pozniej
/// udowodnic "to ja" bez zakladania konta (np. przy GET /api/join/{id}/status). Sekret jawny
/// nigdy tu nie trafia i nigdy nie jest zapisywany -- zwracany jest uczestnikowi raz, w odpowiedzi
/// na dolaczenie do poczekalni.
///
public string? SekretSesjiHash { get; private set; }
// Prywatny konstruktor bez parametrow -- wymagany przez EF Core do materializacji encji z bazy.
private UczestnikOczekujacy()
{
}
private UczestnikOczekujacy(Guid id, Guid wydarzenieId, string nick, string liveKitIdentity, DateTime dolaczylUtc)
{
Id = id;
WydarzenieId = wydarzenieId;
Nick = nick;
LiveKitIdentity = liveKitIdentity;
DolaczylUtc = dolaczylUtc;
Status = StatusUczestnikaOczekujacego.Oczekuje;
}
///
/// Tworzy nowego uczestnika oczekujacego. Walidacja nicku tutaj chroni przed powstaniem encji
/// w niepoprawnym stanie niezaleznie od tego, ktory serwis aplikacji jej uzyje.
///
public static UczestnikOczekujacy Dolacz(Guid wydarzenieId, string nick)
{
if (wydarzenieId == Guid.Empty)
{
throw new NieprawidlowyStanEncjiWyjatek("Uczestnik oczekujacy musi byc powiazany z konkretnym wydarzeniem.");
}
var nickOczyszczony = OczyscNick(nick);
// Tozsamosc LiveKit generowana tu, na serwerze -- klient nie ma na nia zadnego wplywu,
// wiec nie da sie podszyc pod cudza sesje kamery ani przewidziec jej wartosci z gory.
var liveKitIdentity = "uczestnik-" + Guid.NewGuid();
return new UczestnikOczekujacy(Guid.NewGuid(), wydarzenieId, nickOczyszczony, liveKitIdentity, DateTime.UtcNow);
}
private static string OczyscNick(string nick)
{
if (string.IsNullOrWhiteSpace(nick))
{
throw new NieprawidlowyStanEncjiWyjatek("Nick jest wymagany.");
}
var przycietyNick = nick.Trim();
if (przycietyNick.Length > MaksymalnaDlugoscNicku)
{
throw new NieprawidlowyStanEncjiWyjatek($"Nick moze miec maksymalnie {MaksymalnaDlugoscNicku} znakow.");
}
// Podstawowa sanityzacja przed wstrzyknieciem HTML -- ten sam nick trafi pozniej do czatu,
// wiec odrzucamy znaki, ktorymi dalo by sie otworzyc znacznik, juz przy wejsciu do systemu.
if (przycietyNick.Contains('<') || przycietyNick.Contains('>'))
{
throw new NieprawidlowyStanEncjiWyjatek("Nick nie moze zawierac znakow '<' ani '>'.");
}
return przycietyNick;
}
public void Odrzuc() => Status = StatusUczestnikaOczekujacego.Odrzucony;
public void Zbanuj() => Status = StatusUczestnikaOczekujacego.Zbanowany;
/// Host przypisal uczestnika do slotu sceny -- od teraz moze wymienic sekret sesji na token do pokoju glownego.
public void WpuscNaScene() => Status = StatusUczestnikaOczekujacego.NaScenie;
/// Slot zostal zwolniony (host usunal ze sceny albo zmienil uklad) -- uczestnik wraca do poczekalni.
public void ZdejmijZeSceny() => Status = StatusUczestnikaOczekujacego.Oczekuje;
public void UstawSekretSesji(string sekretHash)
{
if (string.IsNullOrWhiteSpace(sekretHash))
{
throw new NieprawidlowyStanEncjiWyjatek("Hash sekretu sesji nie moze byc pusty.");
}
SekretSesjiHash = sekretHash;
}
}