using PlotLine.Data; using PlotLine.Models; namespace PlotLine.Services; public interface IStoryIntelligencePipelineStateService { Task GetForBookAsync(int bookId, int userId); Task> ListForUserAsync(int userId); Task EnsureForBookAsync(int bookId, int userId); Task RecordSceneImportAsync(int projectId, int bookId, int runId); Task RecordCharacterImportAsync(int projectId, int bookId); Task RecordLocationImportAsync(int projectId, int bookId); Task> ListCommittedRunsByBookAsync(int bookId, int userId); } public sealed class StoryIntelligencePipelineStateService( IStoryIntelligencePipelineRepository pipelines, ILogger logger) : IStoryIntelligencePipelineStateService { public async Task GetForBookAsync(int bookId, int userId) => await pipelines.GetByBookForUserAsync(bookId, userId) ?? await EnsureForBookAsync(bookId, userId); public Task> ListForUserAsync(int userId) => pipelines.ListForUserAsync(userId); public async Task EnsureForBookAsync(int bookId, int userId) { var existing = await pipelines.GetByBookForUserAsync(bookId, userId); if (existing is not null) { return existing; } var committedRuns = await pipelines.ListCommittedRunsByBookAsync(bookId, userId); if (committedRuns.Count == 0) { return null; } var latestRun = committedRuns.OrderByDescending(run => run.StoryIntelligenceRunID).First(); var created = await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = latestRun.ProjectID, BookID = latestRun.BookID, CurrentStage = StoryIntelligencePipelineStages.CharacterReview, LastCompletedStage = StoryIntelligencePipelineStages.SceneImport, CurrentReviewStage = StoryIntelligencePipelineStages.CharacterReview, Status = StoryIntelligencePipelineStatuses.NeedsReview, CompletedUtc = null, LastRunID = latestRun.StoryIntelligenceRunID }); logger.LogInformation( "Created Story Intelligence pipeline state for existing imported book {BookID}. ProjectID={ProjectID} CurrentStage={CurrentStage}", bookId, created.ProjectID, created.CurrentStage); return created; } public async Task RecordSceneImportAsync(int projectId, int bookId, int runId) { await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = projectId, BookID = bookId, CurrentStage = StoryIntelligencePipelineStages.CharacterReview, LastCompletedStage = StoryIntelligencePipelineStages.SceneImport, CurrentReviewStage = StoryIntelligencePipelineStages.CharacterReview, Status = StoryIntelligencePipelineStatuses.NeedsReview, CompletedUtc = null, LastRunID = runId }); } public async Task RecordCharacterImportAsync(int projectId, int bookId) { await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = projectId, BookID = bookId, CurrentStage = StoryIntelligencePipelineStages.LocationReview, LastCompletedStage = StoryIntelligencePipelineStages.CharacterImport, CurrentReviewStage = StoryIntelligencePipelineStages.LocationReview, Status = StoryIntelligencePipelineStatuses.NeedsReview, CompletedUtc = null, LastRunID = null }); } public async Task RecordLocationImportAsync(int projectId, int bookId) { await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest { ProjectID = projectId, BookID = bookId, CurrentStage = StoryIntelligencePipelineStages.Complete, LastCompletedStage = StoryIntelligencePipelineStages.LocationImport, CurrentReviewStage = null, Status = StoryIntelligencePipelineStatuses.Complete, CompletedUtc = DateTime.UtcNow, LastRunID = null }); } public Task> ListCommittedRunsByBookAsync(int bookId, int userId) => pipelines.ListCommittedRunsByBookAsync(bookId, userId); }