using Microsoft.EntityFrameworkCore; using SVSim.Database.Entities.Guild; using SVSim.Database.Models.Config; using SVSim.Database.Repositories.Guild; using SVSim.Database.Repositories.Viewer; using SVSim.Database.Services; namespace SVSim.Database.Services.Guild; public sealed class GuildService : IGuildService { private readonly IGuildRepository _guilds; private readonly IGuildMemberRepository _members; private readonly IGuildInviteRepository _invites; private readonly IGuildJoinRequestRepository _joinRequests; private readonly IGuildChatMessageRepository _chatMessages; private readonly IGameConfigService _config; private readonly IGuildIdGenerator _idGen; private readonly IGuildChatService _chat; private readonly IViewerRepository _viewers; private readonly SVSimDbContext _db; public GuildService( IGuildRepository guilds, IGuildMemberRepository members, IGuildInviteRepository invites, IGuildJoinRequestRepository joinRequests, IGuildChatMessageRepository chatMessages, IGameConfigService config, IGuildIdGenerator idGen, IGuildChatService chat, IViewerRepository viewers, SVSimDbContext db) { _guilds = guilds; _members = members; _invites = invites; _joinRequests = joinRequests; _chatMessages = chatMessages; _config = config; _idGen = idGen; _chat = chat; _viewers = viewers; _db = db; } public async Task GetMyGuildAsync(long viewerId, CancellationToken ct = default) { var membership = await _members.GetMembershipAsync(viewerId, ct); if (membership is null) return null; var guild = await _guilds.GetWithMembersAsync(membership.GuildId, ct); if (guild is null || guild.BreakupAt is not null) return null; int joinReqCount = await _joinRequests.CountPendingForGuildAsync(guild.GuildId, ct); int inviteCount = await _invites.CountPendingForInviteeAsync(viewerId, ct); return new GuildFullView(guild, guild.Members, joinReqCount, inviteCount); } public Task GetActiveAsync(int guildId, CancellationToken ct = default) => _guilds.GetActiveByIdAsync(guildId, ct); public async Task> SearchAsync(string name, int activity, int joinCondition, int memberBucket, CancellationToken ct = default) { var cfg = _config.Get(); var rows = await _guilds.SearchAsync(name ?? "", activity, joinCondition, memberBucket, cfg.MaxMemberNum, cfg.SearchResultCap, ct); var guildIds = rows.Select(r => r.GuildId).ToList(); var memberCounts = await _members.CountBatchByGuildIdsAsync(guildIds, ct); var leaderIds = rows.Select(r => r.LeaderViewerId).Distinct().ToList(); var leaderNames = await _viewers.LoadDisplayNamesAsync(leaderIds, ct); return rows.Select(r => new GuildSearchEntry(r, memberCounts.GetValueOrDefault(r.GuildId, 0), leaderNames.GetValueOrDefault(r.LeaderViewerId, ""))).ToList(); } public async Task CreateAsync(long viewerId, CreateGuildRequest req, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(req.Name) || req.Name.Length > 64) return new(GuildOpResultCode.NameInvalid); if (req.Activity is < 1 or > 16) return new(GuildOpResultCode.NameInvalid); if (req.JoinCondition is < 1 or > 3) return new(GuildOpResultCode.NameInvalid); if (await _members.GetMembershipAsync(viewerId, ct) is not null) return new(GuildOpResultCode.AlreadyInGuild); if (await _guilds.NameExistsAsync(req.Name, ct)) return new(GuildOpResultCode.NameTaken); var viewer = await _db.Viewers .Include(v => v.Info.SelectedEmblem) .FirstOrDefaultAsync(v => v.Id == viewerId, ct); if (viewer is null) return new(GuildOpResultCode.PermissionDenied); var id = await _idGen.NextAsync( (cand, c) => _guilds.GetByIdAsync(cand, c).ContinueWith(t => t.Result is not null, c), ct); var now = DateTime.UtcNow; var g = new Entities.Guild.Guild { GuildId = id, Name = req.Name, Description = "", LeaderViewerId = viewerId, EmblemId = DefaultEmblemId(viewer), Activity = (GuildActivity)req.Activity, JoinCondition = (GuildJoinCondition)req.JoinCondition, CreatedAt = now, }; await _guilds.AddAsync(g, ct); await _members.AddAsync( new GuildMember { GuildId = id, ViewerId = viewerId, Role = GuildRole.Leader, JoinedAt = now }, ct); // Side-effects: clear any pending invites + cancel pending join requests for this viewer. await _invites.ConsumePendingForViewerAsync(viewerId, now, ct); await _joinRequests.CancelPendingForViewerAsync(viewerId, now, ct); // Update Viewer.GuildId pointer for quick lookups. viewer.GuildId = id; await _db.SaveChangesAsync(ct); await _chat.EmitSystemEventAsync(id, viewerId, GuildChatMessageType.CreateGuild, body: null, ct); return new(GuildOpResultCode.Ok, GuildId: id); } /// /// Returns the viewer's currently-equipped emblem id, or 100000000 (default) if none. /// private static long DefaultEmblemId(Models.Viewer viewer) { var emblemId = viewer.Info?.SelectedEmblem?.Id; return emblemId is > 0 ? emblemId.Value : 100_000_000L; } public async Task UpdateAsync(long viewerId, UpdateGuildRequest req, CancellationToken ct = default) { var m = await _members.GetMembershipAsync(viewerId, ct); if (m is null) return new(GuildOpResultCode.NotInGuild); if (m.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied); if (req.Name is not null && (string.IsNullOrWhiteSpace(req.Name) || req.Name.Length > 64)) return new(GuildOpResultCode.NameInvalid); if (req.Activity.HasValue && req.Activity.Value is < 1 or > 16) return new(GuildOpResultCode.NameInvalid); if (req.JoinCondition.HasValue && req.JoinCondition.Value is < 1 or > 3) return new(GuildOpResultCode.NameInvalid); await _guilds.UpdateActivityAndJoinConditionAsync(m.GuildId, req.Activity, req.JoinCondition, req.Name, ct); return GuildOpResult.Ok; } public async Task UpdateDescriptionAsync(long viewerId, string description, CancellationToken ct = default) { var m = await _members.GetMembershipAsync(viewerId, ct); if (m is null) return new(GuildOpResultCode.NotInGuild); if (m.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied); if (description.Length > 512) return new(GuildOpResultCode.NameInvalid); await _guilds.UpdateDescriptionAsync(m.GuildId, description, ct); await _chat.EmitSystemEventAsync(m.GuildId, viewerId, GuildChatMessageType.Description, body: null, ct); return GuildOpResult.Ok; } public async Task UpdateEmblemAsync(long viewerId, long emblemId, CancellationToken ct = default) { var m = await _members.GetMembershipAsync(viewerId, ct); if (m is null) return new(GuildOpResultCode.NotInGuild); if (m.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied); await _guilds.UpdateEmblemAsync(m.GuildId, emblemId, ct); return GuildOpResult.Ok; } public async Task BreakupAsync(long viewerId, CancellationToken ct = default) { var membership = await _members.GetMembershipAsync(viewerId, ct); if (membership is null) return new(GuildOpResultCode.NotInGuild); if (membership.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied); var now = DateTime.UtcNow; var members = await _members.ListByGuildAsync(membership.GuildId, ct); foreach (var m in members) await _viewers.ClearGuildIdAsync(m.ViewerId, ct); await _chatMessages.DeleteAllForGuildAsync(membership.GuildId, ct); await _invites.DeleteAllForGuildAsync(membership.GuildId, ct); await _joinRequests.DeleteAllForGuildAsync(membership.GuildId, ct); await _members.DeleteAllForGuildAsync(membership.GuildId, ct); await _guilds.MarkBrokenUpAsync(membership.GuildId, now, ct); return GuildOpResult.Ok; } public async Task InviteAsync(long callerViewerId, long targetViewerId, CancellationToken ct = default) { // Caller must be a guild member with Leader or SubLeader role. var callerMembership = await _members.GetMembershipAsync(callerViewerId, ct); if (callerMembership is null) return new(GuildOpResultCode.NotInGuild); if (callerMembership.Role is not (GuildRole.Leader or GuildRole.SubLeader)) return new(GuildOpResultCode.PermissionDenied); // Target must not already be in any guild. var targetMembership = await _members.GetMembershipAsync(targetViewerId, ct); if (targetMembership is not null) return new(GuildOpResultCode.AlreadyInGuild); // No duplicate pending invite to the same target from the same guild. var existing = await _invites.GetAsync(callerMembership.GuildId, targetViewerId, ct); if (existing is { Status: GuildInviteStatus.Pending }) return new(GuildOpResultCode.InviteAlreadyResolved); var now = DateTime.UtcNow; await _invites.AddAsync(new GuildInvite { GuildId = callerMembership.GuildId, InviteeViewerId = targetViewerId, InviterViewerId = callerViewerId, Status = GuildInviteStatus.Pending, CreatedAt = now, }, ct); return GuildOpResult.Ok; } public async Task CancelInviteAsync(long callerViewerId, long inviteId, CancellationToken ct = default) { // Caller must be in their guild with Leader or SubLeader role. var callerMembership = await _members.GetMembershipAsync(callerViewerId, ct); if (callerMembership is null) return new(GuildOpResultCode.NotInGuild); if (callerMembership.Role is not (GuildRole.Leader or GuildRole.SubLeader)) return new(GuildOpResultCode.PermissionDenied); var invite = await _invites.GetByIdAsync(inviteId, ct); if (invite is null) return new(GuildOpResultCode.InviteNotFound); // Caller must belong to the same guild as the invite. if (invite.GuildId != callerMembership.GuildId) return new(GuildOpResultCode.PermissionDenied); if (invite.Status != GuildInviteStatus.Pending) return new(GuildOpResultCode.InviteAlreadyResolved); await _invites.UpdateStatusAsync(invite.GuildId, invite.InviteeViewerId, GuildInviteStatus.Canceled, DateTime.UtcNow, ct); return GuildOpResult.Ok; } public async Task RejectInviteAsync(long callerViewerId, long inviteId, CancellationToken ct = default) { // Invitee rejects — no guild membership check needed. var invite = await _invites.GetByIdAsync(inviteId, ct); if (invite is null) return new(GuildOpResultCode.InviteNotFound); // Caller must be the invitee. if (invite.InviteeViewerId != callerViewerId) return new(GuildOpResultCode.PermissionDenied); if (invite.Status != GuildInviteStatus.Pending) return new(GuildOpResultCode.InviteAlreadyResolved); await _invites.UpdateStatusAsync(invite.GuildId, callerViewerId, GuildInviteStatus.Rejected, DateTime.UtcNow, ct); return GuildOpResult.Ok; } public Task JoinAsync(long viewerId, int guildId, CancellationToken ct = default) => throw new NotImplementedException(); public Task CancelJoinRequestAsync(long viewerId, int guildId, CancellationToken ct = default) => throw new NotImplementedException(); public Task AcceptJoinRequestAsync(long callerViewerId, long applicantViewerId, CancellationToken ct = default) => throw new NotImplementedException(); public Task RejectJoinRequestAsync(long callerViewerId, long applicantViewerId, CancellationToken ct = default) => throw new NotImplementedException(); public Task LeaveAsync(long viewerId, CancellationToken ct = default) => throw new NotImplementedException(); public Task RemoveAsync(long callerViewerId, long targetViewerId, CancellationToken ct = default) => throw new NotImplementedException(); public Task ChangeRoleAsync(long callerViewerId, long targetViewerId, int newRoleId, CancellationToken ct = default) => throw new NotImplementedException(); public Task> ListPendingInvitesForMeAsync(long viewerId, CancellationToken ct = default) => _invites.ListPendingForInviteeAsync(viewerId, ct); public async Task> ListOutgoingInvitesAsync(long callerViewerId, CancellationToken ct = default) { var membership = await _members.GetMembershipAsync(callerViewerId, ct); if (membership is null) return Array.Empty(); var pendingInvites = await _invites.ListPendingForGuildAsync(membership.GuildId, ct); if (pendingInvites.Count == 0) return Array.Empty(); var inviteeIds = pendingInvites.Select(i => i.InviteeViewerId).Distinct().ToList(); var names = await _viewers.LoadDisplayNamesAsync(inviteeIds, ct); // Load emblem + degree data for invitees. var viewerRows = await _db.Viewers .AsNoTracking() .Include(v => v.Info.SelectedEmblem) .Include(v => v.Info.SelectedDegree) .Where(v => inviteeIds.Contains(v.Id)) .ToDictionaryAsync(v => v.Id, ct); var result = new List(pendingInvites.Count); foreach (var invite in pendingInvites) { viewerRows.TryGetValue(invite.InviteeViewerId, out var v); result.Add(new GuildOutgoingInviteEntry( InviteId: invite.Id, InviteeViewerId: invite.InviteeViewerId, Name: names.GetValueOrDefault(invite.InviteeViewerId, ""), EmblemId: v?.Info?.SelectedEmblem?.Id is > 0 ? v.Info.SelectedEmblem.Id : 100_000_000L, CountryCode: v?.Info?.CountryCode ?? "", Rank: 1, DegreeId: v?.Info?.SelectedDegree?.Id ?? 0, CreatedAt: invite.CreatedAt)); } return result; } public async Task> ListInvitedGuildsAsync(long viewerId, CancellationToken ct = default) { // ListPendingForInviteeAsync already includes the Guild nav prop. var pendingInvites = await _invites.ListPendingForInviteeAsync(viewerId, ct); if (pendingInvites.Count == 0) return Array.Empty(); var guildIds = pendingInvites.Select(i => i.GuildId).Distinct().ToList(); var memberCounts = await _members.CountBatchByGuildIdsAsync(guildIds, ct); var leaderIds = pendingInvites.Select(i => i.Guild.LeaderViewerId).Distinct().ToList(); var leaderNames = await _viewers.LoadDisplayNamesAsync(leaderIds, ct); return pendingInvites.Select(invite => new GuildReceivedInviteEntry( InviteId: invite.Id, Guild: invite.Guild, MemberNum: memberCounts.GetValueOrDefault(invite.GuildId, 0), LeaderName: leaderNames.GetValueOrDefault(invite.Guild.LeaderViewerId, ""))).ToList(); } public Task> ListPendingJoinRequestsForMyGuildAsync(long viewerId, CancellationToken ct = default) => throw new NotImplementedException(); }