using GmRelay.Shared.Domain; namespace GmRelay.Web.Services; public sealed record PlayerAttendanceStats( Guid PlayerId, string DisplayName, string? ExternalUsername, long TotalSessions, long ConfirmedCount, long DeclinedCount, long NoResponseCount, long WaitlistedCount, long CancellationAffectedCount, decimal AttendanceRate); public sealed record SessionAuditLogEntry( Guid Id, Guid SessionId, string ActorExternalUserId, string ActorName, string ChangeType, string? OldValue, string? NewValue, DateTime ChangedAt); public interface ISessionStore { Task> GetGroupsForUserAsync(string platform, string externalUserId); Task GetGroupAsync(Guid groupId); Task IsGroupManagerAsync(Guid groupId, string platform, string externalUserId); Task IsGroupOwnerAsync(Guid groupId, string platform, string externalUserId); Task> GetGroupManagersAsync(Guid groupId); Task> GetUpcomingSessionsAsync(Guid groupId); Task GetSessionAsync(Guid sessionId); Task GetBatchAsync(Guid batchId); Task UpdateSessionAsync(Guid sessionId, Guid groupId, string title, DateTime scheduledAt, string joinLink, int? maxPlayers); Task PromoteWaitlistedPlayerAsync(Guid sessionId, Guid groupId); Task UpdateBatchDetailsAsync(Guid batchId, Guid groupId, string title, string joinLink); Task UpdateBatchNotificationModeAsync(Guid batchId, Guid groupId, SessionNotificationMode notificationMode); Task RescheduleBatchAsync(Guid batchId, Guid groupId, DateTime firstScheduledAt, int intervalDays); Task CloneBatchAsync(Guid batchId, Guid groupId, BatchCloneInterval interval); Task> GetCampaignTemplatesAsync(Guid groupId); Task GetCampaignTemplateAsync(Guid templateId); Task CreateCampaignTemplateAsync(Guid groupId, CreateCampaignTemplateRequest request); Task DeleteCampaignTemplateAsync(Guid templateId, Guid groupId); Task CreateBatchFromTemplateAsync(Guid templateId, Guid groupId, DateTime firstScheduledAt); Task AddGroupCoGmAsync(Guid groupId, string ownerPlatform, string ownerExternalUserId, string coGmPlatform, string coGmExternalUserId, string displayName, string? externalUsername); Task RemoveGroupCoGmAsync(Guid groupId, string coGmPlatform, string coGmExternalUserId); Task> GetSessionParticipantsAsync(Guid sessionId); Task RemovePlayerFromSessionAsync(Guid sessionId, Guid groupId, Guid participantId); Task> GetGroupAttendanceStatsAsync(Guid groupId); Task LogSessionChangeAsync(Guid sessionId, string actorExternalUserId, string actorName, string changeType, string? oldValue, string? newValue); Task> GetSessionHistoryAsync(Guid sessionId); Task UpsertDiscordUserAsync(string discordId, string displayName, string? avatarUrl); // --- Identity linking (issue #35) --- Task ResolveEffectivePlayerIdAsync(string platform, string externalUserId); Task> GetLinkedIdentitiesAsync(string platform, string externalUserId); Task LinkIdentityAsync(string currentPlatform, string currentExternalUserId, string targetPlatform, string targetExternalUserId, string? currentName); Task UnlinkIdentityAsync(string currentPlatform, string currentExternalUserId, string targetPlatform, string targetExternalUserId); Task UpsertPlayerAsync(string platform, string externalUserId, string displayName, string? avatarUrl); } public sealed record LinkedIdentity( string Platform, string ExternalUserId, string DisplayName, string? ExternalUsername, string? AvatarUrl, DateTime LinkedAt);