using Microsoft.EntityFrameworkCore; using Novelly.Api.Common; using Novelly.Api.Data; using Novelly.Api.Projects; using Novelly.Api.Tags; namespace Novelly.Api.Chapters; public class ChapterService(INovelDbContext db, TagService tags) { public async Task> ListAsync(Guid projectId, CancellationToken ct = default) { var chapters = await db.Chapters .Include(c => c.PovCharacter) .Include(c => c.Beats) .Include(c => c.Scenes) .Include(c => c.Tags) .Where(c => c.ProjectId == projectId) .OrderBy(c => c.Number) .ToListAsync(ct); return [.. chapters.Select(c => c.ToSummaryDto())]; } public async Task GetAsync(Guid id, CancellationToken ct = default) => (await FindAsync(id, ct)).ToDto(); public async Task CreateAsync(Guid projectId, CreateChapterRequest request, CancellationToken ct = default) { if (!await db.Projects.AnyAsync(p => p.Id == projectId, ct)) { throw new NotFoundException(nameof(Project), projectId); } var chapter = new Chapter { ProjectId = projectId, Title = request.Title, Number = request.Number ?? await NextChapterNumberAsync(projectId, ct), Summary = request.Summary, PovCharacterId = request.PovCharacterId, Setting = request.Setting, Notes = request.Notes, Status = request.Status, TargetWordCount = request.TargetWordCount }; if (request.Tags is { } names) { chapter.Tags = await tags.ResolveAsync(projectId, names, ct); } db.Chapters.Add(chapter); await db.SaveChangesAsync(ct); return (await FindAsync(chapter.Id, ct)).ToDto(); } public async Task UpdateAsync(Guid id, UpdateChapterRequest request, CancellationToken ct = default) { var chapter = await FindAsync(id, ct); chapter.Title = Patch.Apply(chapter.Title, request.Title) ?? chapter.Title; chapter.Number = request.Number ?? chapter.Number; chapter.Summary = Patch.Apply(chapter.Summary, request.Summary); chapter.PovCharacterId = request.PovCharacterId ?? chapter.PovCharacterId; chapter.Setting = Patch.Apply(chapter.Setting, request.Setting); chapter.Notes = Patch.Apply(chapter.Notes, request.Notes); chapter.Status = request.Status ?? chapter.Status; chapter.TargetWordCount = request.TargetWordCount ?? chapter.TargetWordCount; chapter.UpdatedAt = DateTimeOffset.UtcNow; if (request.Tags is { } names) { chapter.Tags = await tags.ResolveAsync(chapter.ProjectId, names, ct); } await db.SaveChangesAsync(ct); return (await FindAsync(id, ct)).ToDto(); } public async Task DeleteAsync(Guid id, CancellationToken ct = default) { var chapter = await FindAsync(id, ct); db.Chapters.Remove(chapter); await db.SaveChangesAsync(ct); } private async Task NextChapterNumberAsync(Guid projectId, CancellationToken ct) { var max = await db.Chapters .Where(c => c.ProjectId == projectId) .MaxAsync(c => (int?)c.Number, ct); return (max ?? 0) + 1; } private async Task FindAsync(Guid id, CancellationToken ct) => await db.Chapters .Include(c => c.PovCharacter) .Include(c => c.Beats).ThenInclude(b => b.Character) .Include(c => c.Beats).ThenInclude(b => b.Scene) .Include(c => c.Beats).ThenInclude(b => b.Tags) .Include(c => c.Scenes).ThenInclude(s => s.PovCharacter) .Include(c => c.Tags) .FirstOrDefaultAsync(c => c.Id == id, ct) ?? throw new NotFoundException(nameof(Chapter), id); }