using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; using PlotLine.Hubs; using PlotLine.Models; using PlotLine.Services; namespace PlotLine.Controllers; [ApiController] [Authorize] [Route("api/word-companion")] public sealed class WordCompanionController( IWordCompanionService wordCompanion, IHubContext followHub) : ControllerBase { [HttpGet("projects")] public Task GetProjects() => wordCompanion.ListProjectsAsync(); [HttpGet("projects/{projectId:int}/books")] public async Task GetBooks(int projectId) { var response = await wordCompanion.ListBooksAsync(projectId); return response is null ? BadRequest() : Ok(response); } [HttpPost("projects/{projectId:int}/books")] public async Task CreateBook(int projectId, WordCompanionCreateBookRequest request) { var response = await wordCompanion.CreateBookAsync(projectId, request); return response is null ? BadRequest() : Ok(response); } [HttpGet("books/{bookId:int}/structure")] public async Task GetBookStructure(int bookId) { var response = await wordCompanion.GetBookStructureAsync(bookId); return response is null ? NotFound() : Ok(response); } [HttpGet("books/{bookId:int}/chapters")] public async Task GetChapters(int bookId) { var response = await wordCompanion.ListChaptersAsync(bookId); return response is null ? BadRequest() : Ok(response); } [HttpPost("books/{bookId:int}/resolve-scene")] public async Task ResolveScene(int bookId, WordCompanionResolveSceneRequest request) { var response = await wordCompanion.ResolveSceneAsync(bookId, request); if (response is null) { return BadRequest(); } return response.BookAccessible ? Ok(response) : NotFound(response); } [HttpGet("scenes/{sceneId:int}/companion")] public async Task GetSceneCompanion(int sceneId) { var response = await wordCompanion.GetSceneCompanionAsync(sceneId); return response is null ? NotFound() : Ok(response); } [HttpPost("books/{bookId:int}/chapters")] public async Task CreateChapter(int bookId, WordCompanionCreateChapterRequest request) { var response = await wordCompanion.CreateChapterAsync(bookId, request); return response is null ? BadRequest() : Ok(response); } [HttpPost("books/{bookId:int}/chapters/{chapterId:int}/scenes")] public async Task CreateScene(int bookId, int chapterId, WordCompanionCreateSceneRequest request) { var response = await wordCompanion.CreateSceneAsync(bookId, chapterId, request); return response is null ? BadRequest() : Ok(response); } [HttpPost("books/{bookId:int}/sync-scene")] public async Task SyncScene(int bookId, WordCompanionSyncSceneRequest request) { var response = await wordCompanion.SyncSceneAsync(bookId, request); return response is null ? BadRequest() : Ok(response); } [HttpPost("books/{bookId:int}/sync-manuscript")] public async Task SyncManuscript(int bookId, WordCompanionSyncManuscriptRequest request) { var response = await wordCompanion.SyncManuscriptAsync(bookId, request); return response is null ? BadRequest() : Ok(response); } [HttpPut("scenes/{sceneId:int}/links")] public async Task UpdateSceneLinks(int sceneId, WordCompanionUpdateSceneLinksRequest request) { var response = await wordCompanion.UpdateSceneLinksAsync(sceneId, request); return response is null ? BadRequest() : Ok(response); } [HttpPatch("scenes/{sceneId:int}/word-count")] public async Task UpdateWordCount(int sceneId, WordCompanionUpdateWordCountRequest request) { var response = await wordCompanion.UpdateWordCountAsync(sceneId, request); return response is null ? BadRequest() : Ok(response); } [HttpGet("runtime/book/{bookId:int}")] public async Task GetRuntimeBook(int bookId, [FromQuery] Guid documentGuid) { var response = await wordCompanion.GetRuntimeBookAsync(bookId, documentGuid); return response is null ? NotFound() : Ok(response); } [HttpPost("runtime/scene-word-count")] public async Task UpdateRuntimeSceneWordCount(WordCompanionRuntimeSceneWordCountRequest request) { var response = await wordCompanion.UpdateRuntimeSceneWordCountAsync(request); return response is null ? BadRequest() : Ok(response); } [HttpPost("runtime/current-scene")] public async Task UpdateRuntimeCurrentScene(WordCompanionRuntimeCurrentSceneRequest request) { var response = await wordCompanion.ValidateRuntimeCurrentSceneAsync(request); if (response is null) { return BadRequest(); } await BroadcastToCurrentUserAsync("wordCompanionSceneChanged", new { response.ProjectId, response.BookId, response.ChapterId, response.SceneId, response.SceneUrl }); return Ok(response); } [HttpGet("runtime/characters")] public async Task GetRuntimeCharacters([FromQuery] int projectId, [FromQuery] Guid documentGuid) { var response = await wordCompanion.ListRuntimeCharacterAliasesAsync(projectId, documentGuid); return response is null ? BadRequest() : Ok(response); } [HttpPost("runtime/character-suggestions")] public async Task CreateRuntimeCharacterSuggestions(WordCompanionRuntimeCharacterSuggestionsRequest request) { var response = await wordCompanion.CreateRuntimeCharacterSuggestionsAsync(request); return response is null ? BadRequest() : Ok(response); } [HttpGet("runtime/assets")] public async Task GetRuntimeAssets([FromQuery] int projectId, [FromQuery] Guid documentGuid) { var response = await wordCompanion.ListRuntimeAssetAliasesAsync(projectId, documentGuid); return response is null ? BadRequest() : Ok(response); } [HttpPost("runtime/asset-suggestions")] public async Task CreateRuntimeAssetSuggestions(WordCompanionRuntimeAssetSuggestionsRequest request) { var response = await wordCompanion.CreateRuntimeAssetSuggestionsAsync(request); return response is null ? BadRequest() : Ok(response); } [HttpGet("runtime/locations")] public async Task GetRuntimeLocations([FromQuery] int projectId, [FromQuery] Guid documentGuid) { var response = await wordCompanion.ListRuntimeLocationAliasesAsync(projectId, documentGuid); return response is null ? BadRequest() : Ok(response); } [HttpPost("runtime/location-suggestions")] public async Task CreateRuntimeLocationSuggestions(WordCompanionRuntimeLocationSuggestionsRequest request) { var response = await wordCompanion.CreateRuntimeLocationSuggestionsAsync(request); return response is null ? BadRequest() : Ok(response); } [HttpGet("manuscript/book/{bookId:int}")] public async Task GetManuscriptForBook(int bookId) { var response = await wordCompanion.GetManuscriptForBookAsync(bookId); return response is null ? NotFound() : Ok(response); } [HttpPost("manuscript/link")] public async Task LinkManuscript(WordCompanionManuscriptLinkRequest request) { var response = await wordCompanion.LinkManuscriptAsync(request); return response is null ? BadRequest() : Ok(response); } [HttpPost("manuscript/unlink")] public async Task UnlinkManuscript(WordCompanionManuscriptUnlinkRequest request) { var response = await wordCompanion.UnlinkManuscriptAsync(request); return response is null ? BadRequest() : Ok(response); } [HttpPost("manuscript/analyse")] public async Task AnalyseManuscript(WordCompanionManuscriptAnalyseRequest request) { var response = await wordCompanion.AnalyseManuscriptAsync(request); return response is null ? BadRequest() : Ok(response); } [HttpPost("manuscript/import")] public async Task ImportManuscript(WordCompanionManuscriptImportRequest request) { var response = await wordCompanion.ImportManuscriptAsync(request); return response is null ? BadRequest() : Ok(response); } [HttpPost("manuscript/discover-characters")] public async Task DiscoverCharacters(WordCompanionDiscoverCharactersRequest request) { var response = await wordCompanion.DiscoverCharacterCandidatesAsync(request); return response is null ? BadRequest() : Ok(response); } [HttpPost("manuscript/create-discovered-characters")] public async Task CreateDiscoveredCharacters(WordCompanionCreateDiscoveredCharactersRequest request) { var response = await wordCompanion.CreateDiscoveredCharactersAsync(request); if (response is null) { return BadRequest(); } if (response.CreatedCount > 0) { await BroadcastToCurrentUserAsync("wordCompanionCharactersCreated", new { response.ProjectId, CharactersUrl = Url.Action("Index", "Characters", new { projectId = response.ProjectId }) ?? $"/Characters?projectId={response.ProjectId}" }); } return Ok(response); } private async Task BroadcastToCurrentUserAsync(string eventName, object payload) { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); if (!string.IsNullOrWhiteSpace(userId)) { await followHub.Clients.User(userId).SendAsync(eventName, payload); } } }