using System.Collections.Concurrent; using AutoMapper; using LANCommander.Server.Data; using LANCommander.Server.Data.Models; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion; namespace LANCommander.Server.Services { public sealed class ChatService( ILogger logger, IFusionCache cache, ChatMessageService chatMessageService, ChatThreadService chatThreadService, UserService userService) : BaseService(logger) { private readonly ConcurrentDictionary _locks = new(); private readonly int _maxCachedMessages = 200; private static string ThreadCacheKey(Guid threadId) => $"Chat/Thread/{threadId}"; private static string UserThreadCacheKey(string userIdentifier) => $"Chat/User/{userIdentifier}/Threads"; public async Task StartThreadAsync() { var thread = await chatThreadService.AddAsync(new ChatThread()); return thread; } public async Task SendMessageAsync(Guid threadId, string content) { var message = await chatMessageService.AddAsync(new ChatMessage { ThreadId = threadId, Content = content, }); var cacheKey = ThreadCacheKey(threadId); var gate = _locks.GetOrAdd(cacheKey, _ => new SemaphoreSlim(1, 1)); await gate.WaitAsync(); try { var current = await cache.TryGetAsync>(cacheKey); var messages = current.HasValue ? new List(current.Value) : new List(capacity: _maxCachedMessages); messages.RemoveAll(m => m.Id == message.Id); messages.Add(message); if (messages.Count > _maxCachedMessages) messages.RemoveRange(0, messages.Count - _maxCachedMessages); await cache.SetAsync(cacheKey, messages); } finally { gate.Release(); } return message; } public async Task> GetMessagesAsync(Guid threadId) { var cacheKey = ThreadCacheKey(threadId); var messages = await cache.GetOrSetAsync(cacheKey, async _ => { var dbMessages = await chatMessageService.Query(q => { return q .OrderByDescending(m => m.CreatedOn) .Take(_maxCachedMessages); }).GetAsync(); return dbMessages.Reverse().ToList(); }); return messages; } public async Task> GetThreadsAsync(string userIdentifier) { var cacheKey = UserThreadCacheKey(userIdentifier); var threads = await cache.GetOrSetAsync(cacheKey, async _ => { var user = await userService.GetAsync(userIdentifier); var dbThreads = await chatThreadService.Query(q => { return q .Include(t => t.Participants) .OrderByDescending(t => t.CreatedOn) .Where(t => t.Participants.Any(p => p.Id == user.Id)); }).GetAsync(); return dbThreads.ToList(); }); return threads; } } }