using System.Diagnostics.CodeAnalysis; using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; namespace LightlessSync.PlayerData.Pairs; /// /// in memory state for pairs, groups, and syncshells /// public sealed class PairManager { private readonly object _gate = new(); private readonly Dictionary _pairs = new(StringComparer.Ordinal); private readonly Dictionary _groups = new(StringComparer.Ordinal); public PairConnection? LastAddedUser { get; private set; } public IReadOnlyDictionary GetAllPairs() { lock (_gate) { return new Dictionary(_pairs); } } public IReadOnlyDictionary GetAllGroups() { lock (_gate) { return new Dictionary(_groups); } } public PairConnection? GetLastAddedUser() { lock (_gate) { return LastAddedUser; } } public void ClearLastAddedUser() { lock (_gate) { LastAddedUser = null; } } public void ClearAll() { lock (_gate) { _pairs.Clear(); _groups.Clear(); LastAddedUser = null; } } public PairOperationResult GetPair(string userId) { lock (_gate) { if (_pairs.TryGetValue(userId, out var connection)) { return PairOperationResult.Ok(connection); } return PairOperationResult.Fail($"Pair {userId} not found."); } } public bool TryGetPair(string userId, [NotNullWhen(true)] out PairConnection? connection) { lock (_gate) { return _pairs.TryGetValue(userId, out connection); } } public PairOperationResult GetGroup(string groupId) { lock (_gate) { if (_groups.TryGetValue(groupId, out var shell)) { return PairOperationResult.Ok(shell); } return PairOperationResult.Fail($"Group {groupId} not found."); } } public IReadOnlyList GetDirectPairs() { lock (_gate) { return _pairs.Values.Where(p => p.IsDirectlyPaired).ToList(); } } public IReadOnlyList GetPairsByIdent(string ident) { lock (_gate) { return _pairs.Values .Where(p => p.Ident is not null && string.Equals(p.Ident, ident, StringComparison.Ordinal)) .ToList(); } } public IReadOnlyList GetOwnedOrModeratedShells(string currentUserUid) { lock (_gate) { return _groups.Values .Where(s => string.Equals(s.GroupFullInfo.Owner.UID, currentUserUid, StringComparison.OrdinalIgnoreCase) || s.GroupFullInfo.GroupUserInfo.HasFlag(GroupPairUserInfo.IsModerator)) .ToList(); } } public PairOperationResult GetPairCombinedPermissions(string userId) { lock (_gate) { if (!_pairs.TryGetValue(userId, out var connection)) { return PairOperationResult.Fail($"Pair {userId} not found."); } var combined = connection.SelfToOtherPermissions | connection.OtherToSelfPermissions; return PairOperationResult.Ok(combined); } } public PairOperationResult MarkOnline(OnlineUserIdentDto dto) { lock (_gate) { if (!_pairs.TryGetValue(dto.User.UID, out var connection)) { connection = GetOrCreatePair(dto.User); } connection.SetOnline(dto.Ident); return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), dto.Ident)); } } public PairOperationResult MarkOffline(UserData user) { lock (_gate) { if (!_pairs.TryGetValue(user.UID, out var connection)) { return PairOperationResult.Fail($"Pair {user.UID} not found."); } connection.SetOffline(); return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(user.UID), connection.Ident)); } } public PairOperationResult AddOrUpdateIndividual(UserPairDto dto, bool markAsLastAddedUser = true) { lock (_gate) { var connection = GetOrCreatePair(dto.User, out var created); connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions); connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus); if (connection.Ident is null) { return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), null)); } if (created && markAsLastAddedUser) { LastAddedUser = connection; } return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident)); } } public PairOperationResult AddOrUpdateIndividual(UserFullPairDto dto) { lock (_gate) { var connection = GetOrCreatePair(dto.User, out _); connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions); connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus); var removedGroups = connection.Groups.Keys.Where(k => !dto.Groups.Contains(k, StringComparer.Ordinal)).ToList(); foreach (var groupId in removedGroups) { connection.RemoveGroupRelationship(groupId); if (_groups.TryGetValue(groupId, out var shell)) { shell.Users.Remove(dto.User.UID); } } foreach (var groupId in dto.Groups) { connection.EnsureGroupRelationship(groupId, null); if (_groups.TryGetValue(groupId, out var shell)) { shell.Users[dto.User.UID] = connection; } } return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident)); } } public PairOperationResult RemoveIndividual(UserDto dto) { lock (_gate) { if (!_pairs.TryGetValue(dto.User.UID, out var connection)) { return PairOperationResult.Fail($"Pair {dto.User.UID} not found."); } connection.UpdateStatus(null); var registration = TryRemovePairIfNoConnection(connection); return PairOperationResult.Ok(registration); } } public PairOperationResult SetPairOtherToSelfPermissions(UserPermissionsDto dto) { lock (_gate) { if (!_pairs.TryGetValue(dto.User.UID, out var connection)) { return PairOperationResult.Fail($"Pair {dto.User.UID} not found."); } connection.UpdatePermissions(connection.SelfToOtherPermissions, dto.Permissions); return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident)); } } public PairOperationResult SetPairSelfToOtherPermissions(UserPermissionsDto dto) { lock (_gate) { if (!_pairs.TryGetValue(dto.User.UID, out var connection)) { return PairOperationResult.Fail($"Pair {dto.User.UID} not found."); } connection.UpdatePermissions(dto.Permissions, connection.OtherToSelfPermissions); return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident)); } } public PairOperationResult SetIndividualStatus(UserIndividualPairStatusDto dto) { lock (_gate) { if (!_pairs.TryGetValue(dto.User.UID, out var connection)) { return PairOperationResult.Fail($"Pair {dto.User.UID} not found."); } connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus); _ = TryRemovePairIfNoConnection(connection); return PairOperationResult.Ok(); } } public PairOperationResult AddOrUpdateGroupPair(GroupPairFullInfoDto dto) { lock (_gate) { var shell = GetOrCreateShell(dto.Group); var connection = GetOrCreatePair(dto.User); var groupInfo = shell.GroupFullInfo.GroupPairUserInfos.GetValueOrDefault(dto.User.UID, GroupPairUserInfo.None); connection.EnsureGroupRelationship(dto.Group.GID, groupInfo == GroupPairUserInfo.None ? null : groupInfo); connection.UpdatePermissions(dto.SelfToOtherPermissions, dto.OtherToSelfPermissions); shell.Users[dto.User.UID] = connection; return PairOperationResult.Ok(); } } public PairOperationResult RemoveGroupPair(GroupPairDto dto) { lock (_gate) { if (_groups.TryGetValue(dto.GID, out var shell)) { shell.Users.Remove(dto.User.UID); } PairRegistration? registration = null; if (_pairs.TryGetValue(dto.User.UID, out var connection)) { connection.RemoveGroupRelationship(dto.GID); registration = TryRemovePairIfNoConnection(connection); } return PairOperationResult.Ok(registration); } } public PairOperationResult> RemoveGroup(string groupId) { lock (_gate) { if (!_groups.Remove(groupId, out var shell)) { return PairOperationResult>.Fail($"Group {groupId} not found."); } var removed = new List(); foreach (var connection in shell.Users.Values.ToList()) { connection.RemoveGroupRelationship(groupId); var registration = TryRemovePairIfNoConnection(connection); if (registration is not null) { removed.Add(registration); } } return PairOperationResult>.Ok(removed); } } public PairOperationResult AddGroup(GroupFullInfoDto dto) { lock (_gate) { if (!_groups.TryGetValue(dto.Group.GID, out var shell)) { shell = new Syncshell(dto); _groups[dto.Group.GID] = shell; } else { shell.Update(dto); shell.Users.Clear(); } foreach (var (userId, info) in dto.GroupPairUserInfos) { if (_pairs.TryGetValue(userId, out var connection)) { connection.EnsureGroupRelationship(dto.Group.GID, info == GroupPairUserInfo.None ? null : info); shell.Users[userId] = connection; } } return PairOperationResult.Ok(); } } public PairOperationResult UpdateGroupInfo(GroupInfoDto dto) { lock (_gate) { if (!_groups.TryGetValue(dto.Group.GID, out var shell)) { return PairOperationResult.Fail($"Group {dto.Group.GID} not found."); } var updated = new GroupFullInfoDto( dto.Group, dto.Owner, dto.GroupPermissions, shell.GroupFullInfo.GroupUserPermissions, shell.GroupFullInfo.GroupUserInfo, new Dictionary(shell.GroupFullInfo.GroupPairUserInfos, StringComparer.Ordinal), 0); shell.Update(updated); return PairOperationResult.Ok(); } } public PairOperationResult UpdateGroupPairPermissions(GroupPairUserPermissionDto dto) { lock (_gate) { if (!_groups.TryGetValue(dto.Group.GID, out var shell)) { return PairOperationResult.Fail($"Group {dto.Group.GID} not found."); } var updated = shell.GroupFullInfo with { GroupUserPermissions = dto.GroupPairPermissions }; shell.Update(updated); return PairOperationResult.Ok(); } } public PairOperationResult UpdateGroupPermissions(GroupPermissionDto dto) { lock (_gate) { if (!_groups.TryGetValue(dto.Group.GID, out var shell)) { return PairOperationResult.Fail($"Group {dto.Group.GID} not found."); } var updated = shell.GroupFullInfo with { GroupPermissions = dto.Permissions }; shell.Update(updated); return PairOperationResult.Ok(); } } public PairOperationResult UpdateGroupPairStatus(GroupPairUserInfoDto dto) { lock (_gate) { if (_pairs.TryGetValue(dto.UID, out var connection)) { connection.EnsureGroupRelationship(dto.GID, dto.GroupUserInfo == GroupPairUserInfo.None ? null : dto.GroupUserInfo); } if (_groups.TryGetValue(dto.GID, out var shell)) { var infos = new Dictionary(shell.GroupFullInfo.GroupPairUserInfos, StringComparer.Ordinal) { [dto.UID] = dto.GroupUserInfo }; var updated = shell.GroupFullInfo with { GroupPairUserInfos = infos }; shell.Update(updated); } return PairOperationResult.Ok(); } } public PairOperationResult UpdateGroupStatus(GroupPairUserInfoDto dto) { lock (_gate) { if (!_groups.TryGetValue(dto.GID, out var shell)) { return PairOperationResult.Fail($"Group {dto.GID} not found."); } var updated = shell.GroupFullInfo with { GroupUserInfo = dto.GroupUserInfo }; shell.Update(updated); return PairOperationResult.Ok(); } } public PairOperationResult UpdateOtherPermissions(UserPermissionsDto dto) { lock (_gate) { if (!_pairs.TryGetValue(dto.User.UID, out var connection)) { return PairOperationResult.Fail($"Pair {dto.User.UID} not found."); } connection.UpdatePermissions(connection.SelfToOtherPermissions, dto.Permissions); return PairOperationResult.Ok(); } } public PairOperationResult UpdateSelfPermissions(UserPermissionsDto dto) { lock (_gate) { if (!_pairs.TryGetValue(dto.User.UID, out var connection)) { return PairOperationResult.Fail($"Pair {dto.User.UID} not found."); } connection.UpdatePermissions(dto.Permissions, connection.OtherToSelfPermissions); return PairOperationResult.Ok(); } } private PairConnection GetOrCreatePair(UserData user) { return GetOrCreatePair(user, out _); } private PairConnection GetOrCreatePair(UserData user, out bool created) { if (_pairs.TryGetValue(user.UID, out var connection)) { created = false; return connection; } connection = new PairConnection(user); _pairs[user.UID] = connection; created = true; return connection; } private Syncshell GetOrCreateShell(GroupData group) { if (_groups.TryGetValue(group.GID, out var shell)) { return shell; } var placeholder = new GroupFullInfoDto( group, new UserData(string.Empty), GroupPermissions.NoneSet, GroupUserPreferredPermissions.NoneSet, GroupPairUserInfo.None, new Dictionary(StringComparer.Ordinal), 0); shell = new Syncshell(placeholder); _groups[group.GID] = shell; return shell; } private PairRegistration? TryRemovePairIfNoConnection(PairConnection connection) { if (connection.HasAnyConnection) { return null; } if (connection.IsOnline) { connection.SetOffline(); } var userId = connection.User.UID; _pairs.Remove(userId); foreach (var shell in _groups.Values) { shell.Users.Remove(userId); } return new PairRegistration(new PairUniqueIdentifier(userId), connection.Ident); } public static PairConnection CreateFromFullData(UserFullPairDto dto) { var connection = new PairConnection(dto.User); connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions); connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus); foreach (var groupId in dto.Groups) { connection.EnsureGroupRelationship(groupId, null); } return connection; } public static PairConnection CreateFromPartialData(UserPairDto dto) { var connection = new PairConnection(dto.User); connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions); connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus); return connection; } public static GroupPairRelationship CreateGroupPairRelationshipFromFullInfo(string userUid, GroupFullInfoDto fullInfo) { return new GroupPairRelationship(fullInfo.Group.GID, fullInfo.GroupPairUserInfos.TryGetValue(userUid, out var info) && info != GroupPairUserInfo.None ? info : null); } }