using PlotLine.Data; using PlotLine.Models; namespace PlotLine.Services; public interface IWordCompanionService { Task ListProjectsAsync(); Task ListBooksAsync(int projectId); Task GetBookStructureAsync(int bookId); Task ResolveSceneAsync(int bookId, WordCompanionResolveSceneRequest request); Task GetSceneCompanionAsync(int sceneId); Task SyncSceneAsync(int bookId, WordCompanionSyncSceneRequest request); Task SyncManuscriptAsync(int bookId, WordCompanionSyncManuscriptRequest request); Task UpdateSceneLinksAsync(int sceneId, WordCompanionUpdateSceneLinksRequest request); Task UpdateWordCountAsync(int sceneId, WordCompanionUpdateWordCountRequest request); } public sealed class WordCompanionService(IWordCompanionRepository repository, ICurrentUserService currentUser) : IWordCompanionService { public async Task ListProjectsAsync() { var userId = RequireUserId(); return new WordCompanionProjectsResponse { Projects = await repository.ListProjectsAsync(userId) }; } public async Task ListBooksAsync(int projectId) { if (projectId <= 0) { return null; } var books = await repository.ListBooksAsync(projectId, RequireUserId()); return new WordCompanionBooksResponse { Books = books }; } public Task GetBookStructureAsync(int bookId) { return bookId <= 0 ? Task.FromResult(null) : repository.GetBookStructureAsync(bookId, RequireUserId()); } public Task ResolveSceneAsync(int bookId, WordCompanionResolveSceneRequest request) { if (bookId <= 0 || string.IsNullOrWhiteSpace(request.ChapterTitle) || string.IsNullOrWhiteSpace(request.SceneTitle)) { return Task.FromResult(null); } return ResolveSceneCoreAsync(bookId, request); } public Task GetSceneCompanionAsync(int sceneId) { return sceneId <= 0 ? Task.FromResult(null) : repository.GetSceneCompanionAsync(sceneId, RequireUserId()); } public Task SyncSceneAsync(int bookId, WordCompanionSyncSceneRequest request) { if (bookId <= 0 || string.IsNullOrWhiteSpace(request.ChapterTitle) || string.IsNullOrWhiteSpace(request.SceneTitle) || request.ActualWords < 0 || request.ChapterSortOrder is <= 0 || request.SceneSortOrder is <= 0) { return Task.FromResult(null); } request.ChapterTitle = request.ChapterTitle.Trim(); request.SceneTitle = request.SceneTitle.Trim(); return repository.SyncSceneAsync(bookId, RequireUserId(), request); } public Task SyncManuscriptAsync(int bookId, WordCompanionSyncManuscriptRequest request) { if (bookId <= 0 || request.Chapters.Any(chapter => string.IsNullOrWhiteSpace(chapter.Title) || chapter.WordCount < 0 || chapter.SortOrder is <= 0 || chapter.Scenes.Any(scene => string.IsNullOrWhiteSpace(scene.Title) || scene.WordCount < 0 || scene.SortOrder is <= 0))) { return Task.FromResult(null); } foreach (var chapter in request.Chapters) { chapter.Title = chapter.Title?.Trim(); foreach (var scene in chapter.Scenes) { scene.Title = scene.Title?.Trim(); } } return repository.SyncManuscriptAsync(bookId, RequireUserId(), request); } public Task UpdateSceneLinksAsync(int sceneId, WordCompanionUpdateSceneLinksRequest request) { if (sceneId <= 0 || request.PrimaryLocationId is <= 0 || HasInvalidIds(request.CharacterIds) || HasInvalidIds(request.AssetIds) || HasInvalidIds(request.LocationIds)) { return Task.FromResult(null); } return repository.UpdateSceneLinksAsync(sceneId, RequireUserId(), request); } public Task UpdateWordCountAsync(int sceneId, WordCompanionUpdateWordCountRequest request) { var actualWords = request.ActualWordCount ?? request.ActualWords; return sceneId <= 0 || actualWords is null or < 0 ? Task.FromResult(null) : repository.UpdateWordCountAsync(sceneId, RequireUserId(), actualWords.Value, request.LastWorkedOn); } private async Task ResolveSceneCoreAsync(int bookId, WordCompanionResolveSceneRequest request) { return await repository.ResolveSceneAsync(bookId, RequireUserId(), request.ChapterTitle!.Trim(), request.SceneTitle!.Trim()); } private int RequireUserId() { return currentUser.UserId ?? throw new InvalidOperationException("An authenticated user is required."); } private static bool HasInvalidIds(IEnumerable ids) => ids.Any(id => id <= 0); }