using System.Data; using System.Text.Json; using Dapper; using PlotLine.Models; namespace PlotLine.Data; public interface IOnboardingBuildRepository { Task BuildAsync(int userId, OnboardingManuscriptBuildRequest request); Task GetByPreviewAsync(int userId, Guid previewId); Task MarkMarkerSyncSentAsync(int buildId); Task MarkMarkerSyncCompletedAsync(int buildId, string markerStatus, string message); Task MarkMarkerSyncUserSaveRequiredAsync(int buildId, string message); Task MarkMarkerSyncFailedAsync(int buildId, string message, string? diagnostics = null); Task SaveStructuralChapterSourceAsync(StructuralChapterSourceSaveRequest request); Task SaveSceneManuscriptSourceAsync(SceneManuscriptSourceSaveRequest request); Task SaveSceneStructuralSummaryAsync(int sceneId, string structuralSummary); Task> ListSceneManuscriptSourcesByChapterAsync(int chapterId); Task> ListSceneManuscriptSourcesByBookAsync(int bookId); } public sealed class OnboardingBuildRepository(ISqlConnectionFactory connectionFactory) : IOnboardingBuildRepository { public async Task BuildAsync(int userId, OnboardingManuscriptBuildRequest request) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync( "dbo.Onboarding_ManuscriptBuild_Create", new { UserID = userId, request.PreviewID, request.ProjectID, request.BookID, request.DocumentGuid, BindingVersion = 1, ChaptersJson = JsonSerializer.Serialize(request.Chapters), ScenesJson = JsonSerializer.Serialize(request.Scenes), CharactersJson = JsonSerializer.Serialize(request.Characters) }, commandType: CommandType.StoredProcedure); return await ReadBuildResultAsync(result); } public async Task GetByPreviewAsync(int userId, Guid previewId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync( "dbo.Onboarding_ManuscriptBuild_GetByPreview", new { UserID = userId, PreviewID = previewId }, commandType: CommandType.StoredProcedure); return await ReadBuildResultAsync(result); } private static async Task ReadBuildResultAsync(SqlMapper.GridReader result) { var header = await result.ReadSingleOrDefaultAsync(); if (header is null) { return null; } var chapters = (await result.ReadAsync()).ToList(); var scenes = (await result.ReadAsync()).ToList(); var characters = (await result.ReadAsync()).ToList(); return new OnboardingManuscriptBuildResult { BuildID = header.BuildID, PreviewID = header.PreviewID, ProjectID = header.ProjectID, BookID = header.BookID, ManuscriptDocumentID = header.ManuscriptDocumentID, Status = header.Status, MarkerStatus = header.MarkerStatus, ChaptersCreated = header.ChaptersCreated, ScenesCreated = header.ScenesCreated, CharactersCreated = header.CharactersCreated, CharactersReused = header.CharactersReused, SceneAppearancesCreated = header.SceneAppearancesCreated, AlreadyBuilt = header.AlreadyBuilt, Message = header.Message, ChapterMappings = chapters, SceneMappings = scenes, CharacterMappings = characters }; } public async Task MarkMarkerSyncSentAsync(int buildId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Onboarding_ManuscriptBuild_MarkerSyncSent", new { BuildID = buildId }, commandType: CommandType.StoredProcedure); } public async Task MarkMarkerSyncCompletedAsync(int buildId, string markerStatus, string message) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Onboarding_ManuscriptBuild_MarkerSyncCompleted", new { BuildID = buildId, MarkerStatus = markerStatus, Message = message }, commandType: CommandType.StoredProcedure); } public async Task MarkMarkerSyncUserSaveRequiredAsync(int buildId, string message) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Onboarding_ManuscriptBuild_MarkerSyncUserSaveRequired", new { BuildID = buildId, Message = message }, commandType: CommandType.StoredProcedure); } public async Task MarkMarkerSyncFailedAsync(int buildId, string message, string? diagnostics = null) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Onboarding_ManuscriptBuild_MarkerSyncFailed", new { BuildID = buildId, Message = message, Diagnostics = diagnostics }, commandType: CommandType.StoredProcedure); } public async Task SaveStructuralChapterSourceAsync(StructuralChapterSourceSaveRequest request) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.ManuscriptStructuralChapterSource_Save", new { request.ChapterID, request.ProjectID, request.BookID, request.SourceText, request.SourceWordCount, request.SourceCharacterCount, request.ParagraphMapJson }, commandType: CommandType.StoredProcedure); } public async Task SaveSceneManuscriptSourceAsync(SceneManuscriptSourceSaveRequest request) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.SceneManuscriptSource_Save", new { request.SceneID, request.ProjectID, request.BookID, request.ChapterID, request.SourceText, request.SourceWordCount, request.SourceCharacterCount, request.SourceStartParagraph, request.SourceEndParagraph, request.WordStartParagraphIndex, request.WordEndParagraphIndex, request.SourceKind }, commandType: CommandType.StoredProcedure); } public async Task SaveSceneStructuralSummaryAsync(int sceneId, string structuralSummary) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.SceneStructuralSummary_Save", new { SceneID = sceneId, StructuralSummary = structuralSummary }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneManuscriptSourcesByBookAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.SceneManuscriptSource_ListByBook", new { BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListSceneManuscriptSourcesByChapterAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.SceneManuscriptSource_ListByChapter", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class OnboardingManuscriptBuildRequest { public Guid PreviewID { get; init; } public int ProjectID { get; init; } public int BookID { get; init; } public Guid DocumentGuid { get; init; } public IReadOnlyList Chapters { get; init; } = []; public IReadOnlyList Scenes { get; init; } = []; public IReadOnlyList Characters { get; init; } = []; } public sealed class OnboardingBuildChapterRequest { public string TemporaryChapterKey { get; init; } = string.Empty; public string Title { get; init; } = string.Empty; public int SortOrder { get; init; } public int WordCount { get; init; } } public sealed class OnboardingBuildSceneRequest { public string TemporarySceneKey { get; init; } = string.Empty; public string TemporaryChapterKey { get; init; } = string.Empty; public string Title { get; init; } = string.Empty; public string? SuggestedTitle { get; init; } public int SortOrder { get; init; } public int WordCount { get; init; } } public sealed class OnboardingBuildCharacterRequest { public string TemporaryCharacterKey { get; init; } = string.Empty; public string Name { get; init; } = string.Empty; public int? ExistingCharacterID { get; init; } } public sealed class StructuralChapterSourceSaveRequest { public int ChapterID { get; init; } public int ProjectID { get; init; } public int BookID { get; init; } public string SourceText { get; init; } = string.Empty; public int SourceWordCount { get; init; } public int SourceCharacterCount { get; init; } public string? ParagraphMapJson { get; init; } } public sealed class SceneManuscriptSourceSaveRequest { public int SceneID { get; init; } public int ProjectID { get; init; } public int BookID { get; init; } public int ChapterID { get; init; } public string SourceText { get; init; } = string.Empty; public int SourceWordCount { get; init; } public int SourceCharacterCount { get; init; } public int? SourceStartParagraph { get; init; } public int? SourceEndParagraph { get; init; } public int? WordStartParagraphIndex { get; init; } public int? WordEndParagraphIndex { get; init; } public string SourceKind { get; init; } = "StructuralImport"; } public sealed class SceneManuscriptSourceRecord { public int SceneID { get; init; } public decimal SceneNumber { get; init; } public string? SceneTitle { get; init; } public string? SceneTitleSource { get; init; } public int ProjectID { get; init; } public int BookID { get; init; } public int ChapterID { get; init; } public string SourceText { get; init; } = string.Empty; public int SourceWordCount { get; init; } public int SourceCharacterCount { get; init; } public int? SourceStartParagraph { get; init; } public int? SourceEndParagraph { get; init; } public int? WordStartParagraphIndex { get; init; } public int? WordEndParagraphIndex { get; init; } public string SourceKind { get; init; } = string.Empty; public string? StructuralSummary { get; init; } }