using Microsoft.EntityFrameworkCore; using NovelSoftware.Application.Dtos; using NovelSoftware.Domain.Entities; namespace NovelSoftware.Application.Services; public class CharacterService(INovelDbContext db) { public async Task> ListAsync(Guid projectId, CancellationToken ct = default) { var characters = await Query() .Where(c => c.ProjectId == projectId) .OrderBy(c => c.Role) .ThenBy(c => c.Name) .ToListAsync(ct); return [.. characters.Select(c => c.ToDto())]; } public async Task GetAsync(Guid id, CancellationToken ct = default) => (await FindAsync(id, ct)).ToDto(); public async Task CreateAsync(Guid projectId, CreateCharacterRequest request, CancellationToken ct = default) { await EnsureProjectExists(projectId, ct); var character = new Character { ProjectId = projectId, Name = request.Name, Role = request.Role, Age = request.Age, Pronouns = request.Pronouns, Occupation = request.Occupation, Appearance = request.Appearance, Personality = request.Personality, Backstory = request.Backstory, Want = request.Want, Need = request.Need, InternalConflict = request.InternalConflict, ExternalConflict = request.ExternalConflict, ArcSummary = request.ArcSummary, Voice = request.Voice, Notes = request.Notes }; db.Characters.Add(character); await db.SaveChangesAsync(ct); return character.ToDto(); } public async Task UpdateAsync(Guid id, UpdateCharacterRequest request, CancellationToken ct = default) { var character = await FindAsync(id, ct); character.Name = Patch.Apply(character.Name, request.Name) ?? character.Name; character.Role = request.Role ?? character.Role; character.Age = Patch.Apply(character.Age, request.Age); character.Pronouns = Patch.Apply(character.Pronouns, request.Pronouns); character.Occupation = Patch.Apply(character.Occupation, request.Occupation); character.Appearance = Patch.Apply(character.Appearance, request.Appearance); character.Personality = Patch.Apply(character.Personality, request.Personality); character.Backstory = Patch.Apply(character.Backstory, request.Backstory); character.Want = Patch.Apply(character.Want, request.Want); character.Need = Patch.Apply(character.Need, request.Need); character.InternalConflict = Patch.Apply(character.InternalConflict, request.InternalConflict); character.ExternalConflict = Patch.Apply(character.ExternalConflict, request.ExternalConflict); character.ArcSummary = Patch.Apply(character.ArcSummary, request.ArcSummary); character.Voice = Patch.Apply(character.Voice, request.Voice); character.Notes = Patch.Apply(character.Notes, request.Notes); character.UpdatedAt = DateTimeOffset.UtcNow; await db.SaveChangesAsync(ct); return character.ToDto(); } public async Task DeleteAsync(Guid id, CancellationToken ct = default) { var character = await FindAsync(id, ct); db.Characters.Remove(character); await db.SaveChangesAsync(ct); } public async Task AddRelationshipAsync( Guid characterId, CreateRelationshipRequest request, CancellationToken ct = default) { var character = await FindAsync(characterId, ct); var related = await db.Characters .FirstOrDefaultAsync(c => c.Id == request.RelatedCharacterId, ct) ?? throw new NotFoundException(nameof(Character), request.RelatedCharacterId); if (related.ProjectId != character.ProjectId) { throw new InvalidOperationException("Characters must belong to the same project to be related."); } db.CharacterRelationships.Add(new CharacterRelationship { CharacterId = characterId, RelatedCharacterId = request.RelatedCharacterId, RelationshipType = request.RelationshipType, Description = request.Description }); await db.SaveChangesAsync(ct); return (await FindAsync(characterId, ct)).ToDto(); } public async Task RemoveRelationshipAsync(Guid relationshipId, CancellationToken ct = default) { var relationship = await db.CharacterRelationships .FirstOrDefaultAsync(r => r.Id == relationshipId, ct) ?? throw new NotFoundException(nameof(CharacterRelationship), relationshipId); db.CharacterRelationships.Remove(relationship); await db.SaveChangesAsync(ct); } private IQueryable Query() => db.Characters .Include(c => c.Relationships) .ThenInclude(r => r.RelatedCharacter); private async Task FindAsync(Guid id, CancellationToken ct) => await Query().FirstOrDefaultAsync(c => c.Id == id, ct) ?? throw new NotFoundException(nameof(Character), id); private async Task EnsureProjectExists(Guid projectId, CancellationToken ct) { if (!await db.Projects.AnyAsync(p => p.Id == projectId, ct)) { throw new NotFoundException(nameof(Project), projectId); } } }