using PlotLine.Data; using PlotLine.Models; namespace PlotLine.Services; public interface IWordCompanionService { Task ListProjectsAsync(); Task ListBooksAsync(int projectId); Task CreateBookAsync(int projectId, WordCompanionCreateBookRequest request); Task GetBookStructureAsync(int bookId); Task ListChaptersAsync(int bookId); Task ResolveSceneAsync(int bookId, WordCompanionResolveSceneRequest request); Task GetSceneCompanionAsync(int sceneId); Task CreateChapterAsync(int bookId, WordCompanionCreateChapterRequest request); Task CreateSceneAsync(int bookId, int chapterId, WordCompanionCreateSceneRequest request); Task SyncSceneAsync(int bookId, WordCompanionSyncSceneRequest request); Task SyncManuscriptAsync(int bookId, WordCompanionSyncManuscriptRequest request); Task UpdateSceneLinksAsync(int sceneId, WordCompanionUpdateSceneLinksRequest request); Task UpdateWordCountAsync(int sceneId, WordCompanionUpdateWordCountRequest request); Task GetManuscriptForBookAsync(int bookId); Task LinkManuscriptAsync(WordCompanionManuscriptLinkRequest request); Task AnalyseManuscriptAsync(WordCompanionManuscriptAnalyseRequest request); Task ImportManuscriptAsync(WordCompanionManuscriptImportRequest request); } public sealed class WordCompanionService( IWordCompanionRepository repository, IManuscriptDocumentRepository manuscriptDocuments, 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 CreateBookAsync(int projectId, WordCompanionCreateBookRequest request) { if (projectId <= 0 || string.IsNullOrWhiteSpace(request.Title)) { return Task.FromResult(null); } request.Title = request.Title.Trim(); return repository.CreateBookAsync(projectId, RequireUserId(), request); } public Task GetBookStructureAsync(int bookId) { return bookId <= 0 ? Task.FromResult(null) : repository.GetBookStructureAsync(bookId, RequireUserId()); } public async Task ListChaptersAsync(int bookId) { if (bookId <= 0) { return null; } var chapters = await repository.ListChaptersAsync(bookId, RequireUserId()); return new WordCompanionChaptersResponse { Chapters = chapters }; } 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 CreateChapterAsync(int bookId, WordCompanionCreateChapterRequest request) { if (bookId <= 0 || string.IsNullOrWhiteSpace(request.Title) || request.SortOrder is <= 0) { return Task.FromResult(null); } request.Title = request.Title.Trim(); return repository.CreateChapterAsync(bookId, RequireUserId(), request); } public Task CreateSceneAsync(int bookId, int chapterId, WordCompanionCreateSceneRequest request) { if (bookId <= 0 || chapterId <= 0 || string.IsNullOrWhiteSpace(request.SceneTitle)) { return Task.FromResult(null); } request.SceneTitle = request.SceneTitle.Trim(); return repository.CreateSceneAsync(bookId, chapterId, RequireUserId(), request); } 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); } public async Task GetManuscriptForBookAsync(int bookId) { if (bookId <= 0) { return null; } var document = await manuscriptDocuments.GetByBookAsync(bookId, RequireUserId()); return new WordCompanionManuscriptBookResponse { BookId = bookId, ManuscriptDocument = document is null ? null : ToDto(document) }; } public async Task LinkManuscriptAsync(WordCompanionManuscriptLinkRequest request) { if (request.BookId <= 0) { return null; } var documentGuid = request.DocumentGuid.GetValueOrDefault(); if (documentGuid == Guid.Empty) { documentGuid = Guid.NewGuid(); } ManuscriptDocumentModel? document; try { document = await manuscriptDocuments.SaveAsync(request.BookId, documentGuid, RequireUserId()); } catch (InvalidOperationException) { return null; } return document is null ? null : new WordCompanionManuscriptLinkResponse { BookId = document.BookID, ManuscriptDocument = ToDto(document), Message = "Manuscript linked." }; } public Task AnalyseManuscriptAsync(WordCompanionManuscriptAnalyseRequest request) { if (request.ProjectId <= 0 || request.BookId <= 0 || request.ChapterCount < 0 || request.SceneCount < 0 || request.WordCount < 0) { return Task.FromResult(null); } return repository.AnalyseManuscriptAsync(RequireUserId(), request); } public Task ImportManuscriptAsync(WordCompanionManuscriptImportRequest request) { if (request.ProjectId <= 0 || request.BookId <= 0 || request.BindingVersion <= 0 || request.Chapters.Count == 0 || request.Chapters.Any(chapter => string.IsNullOrWhiteSpace(chapter.Title) || chapter.SortOrder is <= 0 || chapter.WordCount < 0 || chapter.Scenes.Count == 0 || chapter.Scenes.Any(scene => string.IsNullOrWhiteSpace(scene.Title) || scene.SortOrder is <= 0 || scene.WordCount < 0))) { return Task.FromResult(null); } request.DocumentGuid = request.DocumentGuid.GetValueOrDefault() == Guid.Empty ? Guid.NewGuid() : request.DocumentGuid; foreach (var chapter in request.Chapters) { chapter.Title = chapter.Title?.Trim(); foreach (var scene in chapter.Scenes) { scene.Title = scene.Title?.Trim(); } } return repository.ImportManuscriptAsync(RequireUserId(), request); } 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); private static WordCompanionManuscriptDocumentDto ToDto(ManuscriptDocumentModel document) => new() { ManuscriptDocumentId = document.ManuscriptDocumentID, BookId = document.BookID, ProjectId = document.ProjectID, DocumentGuid = document.DocumentGuid, BindingVersion = document.BindingVersion, CreatedUtc = document.CreatedUtc, LastSyncUtc = document.LastSyncUtc, LastOpenedUtc = document.LastOpenedUtc, IsActive = document.IsActive }; }