using System.Data;
using Dapper;
using Microsoft.Data.SqlClient;
using PlotLine.Models;

namespace PlotLine.Data;

public interface IProjectRepository
{
    Task<IReadOnlyList<Project>> ListAsync();
    Task<IReadOnlyList<Project>> ListForUserAsync(int userId);
    Task<IReadOnlyList<Project>> ListActiveForUserAsync(int userId);
    Task<IReadOnlyList<Project>> ListArchivedForUserAsync(int userId);
    Task<IReadOnlyList<Project>> ListOwnedForAccountDeletionAsync(int userId);
    Task<Project?> GetAsync(int projectId);
    Task<Project?> GetForUserAsync(int projectId, int userId);
    Task<Project?> GetArchivedForOwnerAsync(int projectId, int userId);
    Task<ProjectDashboardMetrics> GetDashboardMetricsAsync(int projectId);
    Task<IReadOnlyList<ProjectDashboardAttentionThread>> ListDashboardAttentionThreadsAsync(int projectId, int count);
    Task<IReadOnlyList<GenreMetricPreset>> ListGenreMetricPresetsAsync();
    Task<int> SaveAsync(Project project);
    Task<int> SaveAsync(Project project, int userId, string? genreMetricPresetKey);
    Task<int> SaveAsync(Project project, string? genreMetricPresetKey);
    Task ArchiveAsync(int projectId);
    Task ArchiveForUserAsync(int projectId, int userId);
    Task<bool> ArchiveForOwnerAsync(int projectId, int userId);
    Task<bool> RestoreForOwnerAsync(int projectId, int userId);
    Task<IReadOnlyList<string>> ListHardDeleteFilePathsForOwnerAsync(int projectId, int userId);
    Task HardDeleteForOwnerAsync(int projectId, int userId);
    Task<IReadOnlyList<string>> ListHardDeleteFilePathsForAccountDeletionAsync(int projectId, int userId);
    Task HardDeleteForAccountDeletionAsync(int projectId, int userId);
}

public interface IProjectCollaborationRepository
{
    Task<int?> GetOwnerUserIdAsync(int projectId);
    Task<IReadOnlyList<ProjectCollaborator>> ListCollaboratorsAsync(int projectId);
    Task<IReadOnlyList<ProjectInvitation>> ListPendingInvitationsAsync(int projectId);
    Task<bool> CollaboratorCountsForOwnerAsync(int ownerUserId, int collaboratorUserId);
    Task<bool> AddCollaboratorAsync(int projectId, int userId, int invitedByUserId);
    Task<bool> RemoveCollaboratorAsync(int projectId, int userId);
    Task<int> CreatePendingInvitationAsync(int projectId, string emailAddress, int invitedByUserId);
}

public interface IProjectActivityRepository
{
    Task<IReadOnlyList<ProjectActivity>> ListForProjectAsync(int projectId);
    Task<IReadOnlyList<ProjectActivity>> ListRecentForProjectAsync(int projectId, int count);
    Task<long> RecordAsync(int projectId, int? userId, string activityType, string entityType, int? entityId, string? entityName, string? description = null);
}

public interface IBookRepository
{
    Task<IReadOnlyList<Book>> ListByProjectAsync(int projectId);
    Task<IReadOnlyList<Book>> ListByProjectsAsync(IEnumerable<int> projectIds);
    Task<Book?> GetAsync(int bookId);
    Task<Book?> GetCoverPathsAsync(int bookId);
    Task<int> SaveAsync(Book book);
    Task UpdateCoverAsync(int bookId, string? thumbnailPath, string? standardPath);
    Task ArchiveAsync(int bookId);
}

public interface IManuscriptDocumentRepository
{
    Task<ManuscriptDocumentModel?> GetByBookAsync(int bookId, int userId);
    Task<ManuscriptDocumentModel?> GetByGuidAsync(Guid documentGuid, int userId);
    Task<ManuscriptDocumentModel?> SaveAsync(int bookId, Guid documentGuid, int userId);
    Task<ManuscriptDocumentUnlinkResult?> UnlinkBookAsync(int bookId, Guid? documentGuid, int userId);
    Task UpdateLastSyncAsync(int manuscriptDocumentId, int userId);
    Task UpdateLastOpenedAsync(int manuscriptDocumentId, int userId);
}

public interface IChapterRepository
{
    Task<IReadOnlyList<Chapter>> ListByBookAsync(int bookId);
    Task<Chapter?> GetAsync(int chapterId);
    Task<int> SaveAsync(Chapter chapter);
    Task ArchiveAsync(int chapterId);
}

public interface ISceneRepository
{
    Task<IReadOnlyList<Scene>> ListByChapterAsync(int chapterId);
    Task<IReadOnlyList<Scene>> ListByFloorPlanAsync(int floorPlanId);
    Task<Scene?> GetAsync(int sceneId);
    Task<int> SaveAsync(Scene scene);
    Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId);
    Task SaveFloorPlanLinkAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId);
    Task SavePurposesAsync(int sceneId, IEnumerable<int> purposeIds);
    Task SaveMetricValuesAsync(int sceneId, IEnumerable<SceneMetricValue> metricValues);
    Task SaveMetricValueAsync(int sceneId, int metricTypeId, int value);
    Task ArchiveAsync(int sceneId);
    Task MoveAsync(int sceneId, string direction);
    Task<IReadOnlyList<ChapterOption>> ListChapterOptionsAsync(int projectId);
    Task<SceneMovePreview?> GetMovePreviewAsync(int sceneId, int targetChapterId, string position);
    Task MoveToChapterAsync(int sceneId, int targetChapterId, string position, bool renumber);
    Task MoveRelativeAsync(int sceneId, int anchorSceneId, string position);
    Task RenumberChapterAsync(int chapterId);
}

public interface ISceneFloorPlanOccupancyRepository
{
    Task<IReadOnlyList<SceneFloorPlanOccupancy>> ListBySceneAsync(int sceneId);
    Task<IReadOnlyList<SceneFloorPlanOccupancy>> ListByFloorPlanAsync(int floorPlanId);
    Task<IReadOnlyList<FloorPlanOccupancyParticipant>> ListParticipantsByFloorPlanAsync(int floorPlanId);
    Task SaveAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId, IEnumerable<SceneFloorPlanOccupancy> occupancyRows);
}

public interface ILookupRepository
{
    Task<LookupData> GetAllAsync();
}

public interface ITimelineRepository
{
    Task<TimelineData> GetByProjectAsync(int projectId, int? bookId, bool includeMetrics, IEnumerable<int> metricTypeIds, bool includePlotLines);
    Task<IReadOnlyList<TimelineSceneOverviewRow>> GetSceneOverviewsAsync(int projectId, int? bookId);
}

public interface ITimelinePresetRepository
{
    Task<IReadOnlyList<TimelineViewPreset>> ListAsync(int projectId, int? bookId);
    Task<TimelineViewPreset?> GetAsync(int timelineViewPresetId);
    Task<int> SaveAsync(TimelineViewPreset preset);
    Task ArchiveAsync(int timelineViewPresetId);
}

public interface ITimelineSettingsRepository
{
    Task<ProjectTimelineSettings?> GetAsync(int projectId);
    Task<IReadOnlyList<SceneMetricType>> ListMetricTypesAsync(int projectId);
    Task<IReadOnlyList<ProjectTimelineMetricSetting>> ListMetricSettingsAsync(int projectId);
    Task SaveAsync(ProjectTimelineSettings settings, IEnumerable<int> metricTypeIds);
}

public interface ISceneMetricTypeRepository
{
    Task<IReadOnlyList<SceneMetricType>> ListForManagementAsync(int projectId);
    Task<SceneMetricType?> GetAsync(int metricTypeId, int projectId);
    Task<int> SaveAsync(SceneMetricType metric);
    Task<bool> SetActiveAsync(int metricTypeId, int projectId, bool isActive);
    Task<bool> MoveAsync(int metricTypeId, int projectId, string direction);
    Task AddMissingDefaultsAsync(int projectId);
    Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId);
    Task<bool> HasProjectMetricsAsync(int projectId);
}

public interface IPlotRepository
{
    Task<PlotLookupData> GetLookupsAsync();
    Task<IReadOnlyList<PlotLineItem>> ListPlotLinesAsync(int projectId);
    Task<PlotLineItem?> GetPlotLineAsync(int plotLineId);
    Task<int> SavePlotLineAsync(PlotLineItem plotLine);
    Task ArchivePlotLineAsync(int plotLineId);
    Task<IReadOnlyList<PlotThread>> ListPlotThreadsByProjectAsync(int projectId);
    Task<IReadOnlyList<PlotThread>> ListPlotThreadsByPlotLineAsync(int plotLineId);
    Task<PlotThread?> GetPlotThreadAsync(int plotThreadId);
    Task<int> SavePlotThreadAsync(PlotThread plotThread);
    Task ArchivePlotThreadAsync(int plotThreadId);
    Task<IReadOnlyList<ThreadEvent>> ListThreadEventsBySceneAsync(int sceneId);
    Task<IReadOnlyList<ThreadEvent>> ListThreadEventsByPlotThreadAsync(int plotThreadId);
    Task<int> SaveThreadEventAsync(ThreadEvent threadEvent);
    Task SaveThreadEventTargetsAsync(int threadEventId, IEnumerable<int> plotLineIds);
    Task DeleteThreadEventAsync(int threadEventId);
    Task<IReadOnlyList<SceneOption>> ListSceneOptionsAsync(int projectId);
}

public interface IAssetRepository
{
    Task<AssetLookupData> GetLookupsAsync(int? projectId = null);
    Task<IReadOnlyList<StoryAsset>> ListAssetsAsync(int projectId);
    Task<StoryAsset?> GetAssetAsync(int storyAssetId);
    Task<int> SaveAssetAsync(StoryAsset asset);
    Task<IReadOnlyList<AssetAlias>> ListAliasesAsync(int storyAssetId);
    Task<int> AddAliasAsync(int storyAssetId, string alias, int? sortOrder);
    Task UpdateAliasAsync(int assetAliasId, string alias, int? sortOrder);
    Task DeleteAliasAsync(int assetAliasId);
    Task UpdateAssetImageAsync(int storyAssetId, string? imagePath, string? thumbnailPath);
    Task ArchiveAssetAsync(int storyAssetId);
    Task<IReadOnlyList<AssetEvent>> ListEventsBySceneAsync(int sceneId);
    Task<IReadOnlyList<AssetEvent>> ListEventsByAssetAsync(int storyAssetId);
    Task<int> SaveEventAsync(AssetEvent assetEvent);
    Task DeleteEventAsync(int assetEventId);
    Task<IReadOnlyList<SceneAssetSuggestion>> ListSceneAssetSuggestionsAsync(int sceneId, int userId);
    Task<SceneAssetSuggestionReviewResult?> AcceptSceneAssetSuggestionAsync(int suggestionId, int userId);
    Task<SceneAssetSuggestionReviewResult?> RejectSceneAssetSuggestionAsync(int suggestionId, int userId);
    Task<IReadOnlyList<SceneAssetSuggestionCount>> CountSceneAssetSuggestionsByChapterAsync(int chapterId, int userId);
    Task<IReadOnlyList<AssetDependency>> ListDependenciesByAssetAsync(int storyAssetId);
    Task<int> SaveDependencyAsync(AssetDependency dependency);
    Task DeleteDependencyAsync(int assetDependencyId);
    Task<IReadOnlyList<AssetCustodyEvent>> ListCustodyBySceneAsync(int sceneId);
    Task<IReadOnlyList<AssetCustodyEvent>> ListCustodyByAssetAsync(int storyAssetId);
    Task<IReadOnlyList<AssetCustodyCharacter>> ListCustodyCharactersByProjectAsync(int projectId);
    Task<int> SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable<int> custodianCharacterIds, int custodyRoleId);
    Task DeleteCustodyEventAsync(int assetCustodyEventId);
    Task<(IReadOnlyList<StoryAsset> Assets, IReadOnlyList<AssetEvent> Events)> GetTimelineAsync(int projectId, int? bookId);
    Task<IReadOnlyList<AssetDependencyIssue>> CheckDependencyIssuesAsync(int storyAssetId, int sceneId, int? toStateId);
}

public interface ICharacterRepository
{
    Task<CharacterLookupData> GetLookupsAsync(int? projectId = null);
    Task<IReadOnlyList<Character>> ListCharactersAsync(int projectId);
    Task<Character?> GetCharacterAsync(int characterId);
    Task<int> SaveCharacterAsync(Character character);
    Task<IReadOnlyList<CharacterAlias>> ListAliasesAsync(int characterId);
    Task<int> AddAliasAsync(int characterId, string alias, int? sortOrder);
    Task UpdateAliasAsync(int characterAliasId, string alias, int? sortOrder);
    Task DeleteAliasAsync(int characterAliasId);
    Task<IReadOnlyList<CharacterSexValue>> ListSexValuesAsync(int ownerUserId);
    Task<CharacterSexValue?> GetSexValueAsync(int characterSexValueId, int ownerUserId);
    Task<int> GetOrCreateSexValueAsync(int ownerUserId, string sexName);
    Task UpdateCharacterImageAsync(int characterId, string? imagePath, string? thumbnailPath);
    Task UpdateCharacterAvatarAsync(int characterId, int? sourceCharacterImageId, string? avatarImagePath, string? avatarThumbnailPath);
    Task<IReadOnlyList<CharacterImage>> ListCharacterImagesAsync(int characterId);
    Task<CharacterImage?> GetCharacterImageAsync(int characterImageId);
    Task<int> CreateCharacterImageAsync(CharacterImage image);
    Task DeleteCharacterImageAsync(int characterImageId);
    Task ArchiveCharacterAsync(int characterId);
    Task<IReadOnlyList<SceneCharacter>> ListSceneCharactersAsync(int sceneId);
    Task<int> SaveSceneCharacterAsync(SceneCharacter sceneCharacter);
    Task DeleteSceneCharacterAsync(int sceneCharacterId);
    Task<IReadOnlyList<SceneCharacterSuggestion>> ListSceneCharacterSuggestionsAsync(int sceneId, int userId);
    Task<SceneCharacterSuggestionReviewResult?> AcceptSceneCharacterSuggestionAsync(int suggestionId, int userId);
    Task<SceneCharacterSuggestionReviewResult?> RejectSceneCharacterSuggestionAsync(int suggestionId, int userId);
    Task<IReadOnlyList<SceneCharacterSuggestionCount>> CountSceneCharacterSuggestionsByChapterAsync(int chapterId, int userId);
    Task<IReadOnlyList<CharacterAttributeEvent>> ListAttributeEventsBySceneAsync(int sceneId);
    Task<IReadOnlyList<CharacterAttributeEvent>> ListAttributeEventsByCharacterAsync(int characterId);
    Task<int> SaveAttributeEventAsync(CharacterAttributeEvent attributeEvent);
    Task<IReadOnlyList<CharacterKnowledgeItem>> ListKnowledgeBySceneAsync(int sceneId);
    Task<IReadOnlyList<CharacterKnowledgeItem>> ListKnowledgeByCharacterAsync(int characterId);
    Task<int> SaveKnowledgeAsync(CharacterKnowledgeItem knowledge);
    Task DeleteKnowledgeAsync(int characterKnowledgeId);
    Task<IReadOnlyList<CharacterRelationship>> ListRelationshipsByProjectAsync(int projectId);
    Task<IReadOnlyList<CharacterRelationship>> ListRelationshipsByCharacterAsync(int characterId);
    Task<IReadOnlyList<CharacterRelationship>> ListInitialRelationshipsByCharacterAsync(int characterId);
    Task<CharacterRelationship?> GetRelationshipAsync(int characterRelationshipId);
    Task<int> SaveRelationshipAsync(CharacterRelationship relationship);
    Task ArchiveRelationshipAsync(int characterRelationshipId);
    Task<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsBySceneAsync(int sceneId);
    Task<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsByCharacterAsync(int characterId);
    Task<int> SaveRelationshipEventAsync(RelationshipEvent relationshipEvent);
    Task DeleteRelationshipEventAsync(int relationshipEventId);
    Task<(IReadOnlyList<Character> Characters, IReadOnlyList<SceneCharacter> Appearances)> GetTimelineAsync(int projectId, int? bookId);
    Task<RelationshipMapData> GetRelationshipMapAsync(int projectId);
}

public interface ILocationRepository
{
    Task<LocationLookupData> GetLookupsAsync();
    Task<IReadOnlyList<LocationItem>> ListByProjectAsync(int projectId);
    Task<LocationItem?> GetAsync(int locationId);
    Task<int> SaveAsync(LocationItem location);
    Task<IReadOnlyList<LocationAlias>> ListAliasesAsync(int locationId);
    Task<int> AddAliasAsync(int locationId, string alias, int? sortOrder);
    Task UpdateAliasAsync(int locationAliasId, string alias, int? sortOrder);
    Task DeleteAliasAsync(int locationAliasId);
    Task ArchiveAsync(int locationId);
    Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByProjectAsync(int projectId);
    Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByLocationAsync(int locationId);
    Task<int> SaveRelationshipAsync(LocationRelationship relationship);
    Task ArchiveRelationshipAsync(int locationRelationshipId);
    Task<IReadOnlyList<SceneAssetLocation>> ListSceneAssetLocationsAsync(int sceneId);
    Task<int> SaveSceneAssetLocationAsync(SceneAssetLocation sceneAssetLocation, bool updateCurrentLocation);
    Task DeleteSceneAssetLocationAsync(int sceneAssetLocationId);
    Task<IReadOnlyList<SceneLocationSuggestion>> ListSceneLocationSuggestionsAsync(int sceneId, int userId);
    Task<SceneLocationSuggestionReviewResult?> AcceptSceneLocationSuggestionAsync(int suggestionId, int userId);
    Task<SceneLocationSuggestionReviewResult?> RejectSceneLocationSuggestionAsync(int suggestionId, int userId);
    Task<IReadOnlyList<SceneLocationSuggestionCount>> CountSceneLocationSuggestionsByChapterAsync(int chapterId, int userId);
    Task<IReadOnlyList<Scene>> ListScenesByLocationAsync(int locationId);
    Task<IReadOnlyList<SceneCharacter>> ListCharactersByLocationAsync(int locationId);
    Task<IReadOnlyList<SceneAssetLocation>> ListAssetsByLocationAsync(int locationId);
}

public interface IFloorPlanRepository
{
    Task<IReadOnlyList<FloorPlan>> ListByProjectAsync(int projectId);
    Task<FloorPlan?> GetAsync(int floorPlanId);
    Task<(FloorPlan? FloorPlan, IReadOnlyList<FloorPlanFloor> Floors, IReadOnlyList<FloorPlanBlock> Blocks, IReadOnlyList<FloorPlanTransition> Transitions)> GetEditorAsync(int floorPlanId);
    Task<FloorPlanFloor?> GetFloorAsync(int floorPlanFloorId);
    Task<IReadOnlyList<FloorPlanBlock>> ListBlocksByFloorAsync(int floorPlanFloorId);
    Task<IReadOnlyList<FloorPlanBlock>> ListBlocksByPlanAsync(int floorPlanId);
    Task<IReadOnlyList<FloorPlanFloor>> ListFloorsByPlanAsync(int floorPlanId);
    Task<FloorPlanBlock?> GetBlockAsync(int floorPlanBlockId);
    Task<int> SavePlanAsync(FloorPlan floorPlan);
    Task DeletePlanAsync(int floorPlanId);
    Task<int> SaveFloorAsync(FloorPlanFloor floor);
    Task<bool> UpdateFloorBackgroundAsync(FloorPlanFloor floor);
    Task DeleteFloorAsync(int floorPlanFloorId);
    Task<int> SaveBlockAsync(FloorPlanBlock block);
    Task DeleteBlockAsync(int floorPlanBlockId);
    Task<int> SaveTransitionAsync(FloorPlanTransition transition);
    Task DeleteTransitionAsync(int floorPlanTransitionId);
}

public interface IWarningRepository
{
    Task<WarningLookupData> GetLookupsAsync();
    Task<ContinuityValidationSummary> ValidateProjectAsync(int projectId);
    Task<ContinuityValidationSummary> ValidateBookAsync(int bookId);
    Task<ContinuityValidationSummary> ValidateSceneAsync(int sceneId);
    Task<IReadOnlyList<ContinuityWarning>> ListAsync(int projectId, int? bookId, int? sceneId, int? warningTypeId, int? warningSeverityId, string? entityType, bool includeDismissed, bool includeIntentional);
    Task<IReadOnlyList<ContinuityWarning>> ListDashboardUnresolvedAsync(int projectId, int count);
    Task<IReadOnlyList<ContinuityWarning>> ListBySceneAsync(int sceneId);
    Task<IReadOnlyList<SceneWarningCount>> GetSceneCountsAsync(int projectId, int? bookId);
    Task DismissAsync(int continuityWarningId);
    Task MarkIntentionalAsync(int continuityWarningId);
    Task RestoreAsync(int continuityWarningId);
    Task FinaliseSceneDependencyWarningsAsync(int projectId, int? bookId);
}

public interface IContinuityWarningAcknowledgementRepository
{
    Task<IReadOnlyList<ContinuityWarningAcknowledgement>> ListByProjectAsync(int projectId);
    Task<ContinuityWarningAcknowledgement?> GetAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId);
    Task SaveAsync(ContinuityWarningAcknowledgement acknowledgement);
    Task DeleteAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId);
}

public interface ISceneDependencyRepository
{
    Task<SceneDependencyLookupData> GetLookupsAsync();
    Task<IReadOnlyList<SceneDependency>> ListBySceneAsync(int sceneId);
    Task<IReadOnlyList<SceneDependency>> ListByProjectAsync(int projectId, int? bookId);
    Task<IReadOnlyList<SceneDependencyCount>> GetCountsAsync(int projectId, int? bookId);
    Task<int> SaveAsync(SceneDependency dependency);
    Task ArchiveAsync(int sceneDependencyId);
}

public interface IAnalyticsRepository
{
    Task<AnalyticsData> GetStoryHealthAsync(int projectId, int? bookId, decimal? startChapterNumber, decimal? endChapterNumber, decimal? startSceneNumber, decimal? endSceneNumber);
}

public interface IStoryBibleRepository
{
    Task<IReadOnlyList<StoryBibleCharacterSummary>> GetCharacterSummariesAsync(int projectId);
    Task<IReadOnlyList<StoryBibleThreadSummary>> GetThreadSummariesAsync(int projectId);
    Task<IReadOnlyList<StoryBibleAssetSummary>> GetAssetSummariesAsync(int projectId);
    Task<IReadOnlyList<StoryBibleRelationshipSummary>> GetRelationshipSummariesAsync(int projectId);
    Task<IReadOnlyList<StoryBibleLocationSummary>> GetLocationSummariesAsync(int projectId);
    Task<IReadOnlyList<StoryBibleTimelineSummary>> GetTimelineSummaryAsync(int projectId);
    Task<IReadOnlyList<StoryBibleAttentionItem>> GetOpenQuestionsAsync(int projectId);
    Task<IReadOnlyList<StoryBibleRevisionItem>> GetRevisionProgressAsync(int projectId);
    Task<IReadOnlyList<StoryBibleSearchResult>> SearchAsync(int projectId, string? query);
}

public interface IWriterWorkspaceRepository
{
    Task<IReadOnlyList<SceneNoteType>> ListNoteTypesAsync();
    Task<IReadOnlyList<SceneAttachmentType>> ListAttachmentTypesAsync();
    Task<SceneWorkflow> GetWorkflowAsync(int sceneId);
    Task SaveWorkflowAsync(SceneWorkflow workflow);
    Task<IReadOnlyList<SceneNote>> ListNotesAsync(int sceneId);
    Task<int> SaveNoteAsync(SceneNote note);
    Task DeleteNoteAsync(int sceneNoteId);
    Task<IReadOnlyList<SceneChecklistItem>> ListChecklistItemsAsync(int sceneId);
    Task<int> SaveChecklistItemAsync(SceneChecklistItem item);
    Task DeleteChecklistItemAsync(int sceneChecklistItemId);
    Task<IReadOnlyList<SceneAttachment>> ListAttachmentsAsync(int sceneId);
    Task<int> SaveAttachmentAsync(SceneAttachment attachment);
    Task DeleteAttachmentAsync(int sceneAttachmentId);
    Task<ChapterGoal> GetChapterGoalAsync(int chapterId);
    Task SaveChapterGoalAsync(ChapterGoal goal);
    Task<IReadOnlyList<ChapterWorkflowScene>> GetChapterWorkflowAsync(int chapterId);
    Task<IReadOnlyList<WriterDashboardScene>> GetWriterDashboardScenesAsync(int? projectId, string queue);
    Task<IReadOnlyList<StoryBibleAttentionItem>> GetWriterDashboardRemindersAsync(int? projectId);
}

public interface IWritingScheduleRepository
{
    Task<IReadOnlyList<WritingPlan>> GetWritingPlansByUserAsync(int userId);
    Task<WritingPlan?> GetWritingPlanByIDAsync(int writingPlanId);
    Task<int> CreateWritingPlanAsync(WritingPlan plan);
    Task UpdateWritingPlanAsync(WritingPlan plan);
    Task DeleteWritingPlanAsync(int writingPlanId);
    Task SetWritingPlanActiveAsync(int writingPlanId, bool isActive);
    Task<IReadOnlyList<WritingScheduleItem>> GetScheduleItemsByPlanAsync(int writingPlanId);
    Task<IReadOnlyList<WritingScheduleItem>> GetScheduleItemsByDateRangeAsync(int writingPlanId, DateTime startDate, DateTime endDate);
    Task<int> CreateScheduleItemAsync(WritingScheduleItem item);
    Task UpdateScheduleItemAsync(WritingScheduleItem item);
    Task DeleteScheduleItemAsync(int writingScheduleItemId);
    Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc);
    Task<IReadOnlyList<WritingPlanManagementItem>> GetWritingPlansForManagementAsync(int userId);
    Task<IReadOnlyList<WritingScheduleScene>> GetSchedulingScenesAsync(int projectId, int? bookId, bool includeBlockedScenes);
    Task ReplaceScheduleItemsAsync(int writingPlanId, IReadOnlyList<WritingScheduleItem> items);
    Task<IReadOnlyList<WritingPlanSummary>> GetWritingPlanSummariesByUserAsync(int userId, int? projectId = null);
    Task<IReadOnlyList<WritingSchedulePreviewItem>> GetSchedulePreviewItemsAsync(int writingPlanId);
    Task DeleteScheduleItemsByPlanAsync(int writingPlanId);
    Task<IReadOnlyList<WritingScheduleDashboardItem>> GetActiveScheduleDashboardItemsAsync(int userId, int? projectId = null);
    Task<int> UpdatePlannedScheduleItemStatusForUserAsync(int writingScheduleItemId, int userId, string scheduleStatus, DateTime? completedDateUtc);
    Task<IReadOnlyList<WritingPlanSummary>> GetPlansRequiringRebalanceAsync(int userId, DateTime today, int? projectId = null);
    Task UpdatePlannedScheduleItemDatesAsync(int writingPlanId, IReadOnlyList<WritingScheduleItemDateChange> changes);
}

public interface IExportRepository
{
    Task<ExportPacket> GetStoryBibleAsync(int projectId);
    Task<ExportPacket> GetNarrativeTimelineAsync(int projectId);
    Task<ExportPacket> GetCharacterReferenceAsync(int characterId);
    Task<ExportPacket> GetSceneBriefAsync(int sceneId);
    Task<ExportPacket> GetChapterBriefAsync(int chapterId);
    Task<ExportPacket> GetWriterSessionBriefAsync(int sceneId);
    Task<ExportPacket> GetResearchPackAsync(int projectId);
    Task<ExportPacket> GetSearchResultsAsync(int projectId, string query);
}

public interface IDynamicsRepository
{
    Task<(Character? Character, IReadOnlyList<CharacterArcPoint> ArcPoints, IReadOnlyList<RelationshipEvent> RelationshipEvents, IReadOnlyList<CharacterKnowledgeItem> KnowledgeEvents, IReadOnlyList<DynamicsIssue> Issues)> GetCharacterArcAsync(int characterId);
    Task<(CharacterRelationship? Relationship, IReadOnlyList<RelationshipProgressionPoint> Events, IReadOnlyList<DynamicsIssue> Issues)> GetRelationshipEvolutionAsync(int characterRelationshipId);
    Task<(Character? Character, IReadOnlyList<CharacterKnowledgeItem> KnowledgeEvents, IReadOnlyList<DynamicsIssue> Issues)> GetKnowledgeTimelineAsync(int characterId);
    Task<(IReadOnlyList<CharacterPairingSummary> Pairings, IReadOnlyList<DynamicsIssue> Issues)> GetStoryDynamicsAsync(int projectId);
    Task RunContinuityChecksAsync(int projectId, int? bookId, int? sceneId);
}

public interface IScenarioRepository
{
    Task<IReadOnlyList<Scenario>> ListAsync(int projectId);
    Task<Scenario?> GetAsync(int scenarioId);
    Task<int> CreateAsync(int projectId, int? bookId, string scenarioName, string? description);
    Task<(IReadOnlyList<Book> Books, IReadOnlyList<Chapter> Chapters, IReadOnlyList<ScenarioScene> Scenes, IReadOnlyList<ScenarioWarning> Warnings)> GetTimelineAsync(int scenarioId);
    Task MoveSceneAsync(int scenarioId, int sceneId, int? anchorSceneId, int? targetChapterId, string position);
    Task<int> ValidateAsync(int scenarioId);
    Task<(IReadOnlyList<ScenarioComparisonRow> MovedScenes, IReadOnlyList<ScenarioWarning> Warnings)> CompareAsync(int scenarioId);
    Task ApplyAsync(int scenarioId);
    Task ArchiveAsync(int scenarioId);
}

public interface IArchiveRepository
{
    Task<IReadOnlyList<ArchivedItem>> ListAsync(int? projectId, string? entityType);
    Task<IReadOnlyList<ArchivedItem>> ListForUserAsync(int? projectId, string? entityType, int userId);
    Task<IReadOnlyList<Project>> ListProjectFiltersAsync();
    Task<IReadOnlyList<Project>> ListProjectFiltersForUserAsync(int userId);
    Task ArchiveAsync(string entityType, int entityId, string? reason);
    Task RestoreAsync(string entityType, int entityId);
    Task<ArchiveDeleteResult> DeleteForeverAsync(string entityType, int entityId, string confirmationText, int userId);
}

public interface IAcceptanceRepository
{
    Task<IReadOnlyList<Phase1AcceptanceChecklistItem>> GetPhase1ChecklistAsync(int? projectId);
}

public sealed class TimelineData
{
    public Project? Project { get; init; }
    public IReadOnlyList<Book> Books { get; init; } = [];
    public IReadOnlyList<Chapter> Chapters { get; init; } = [];
    public IReadOnlyList<Scene> Scenes { get; init; } = [];
    public IReadOnlyList<ScenePurposeLabel> PurposeLabels { get; init; } = [];
    public IReadOnlyList<SceneMetricType> MetricTypes { get; init; } = [];
    public IReadOnlyList<TimelineMetricPoint> MetricPoints { get; init; } = [];
    public IReadOnlyList<PlotLineItem> PlotLines { get; init; } = [];
    public IReadOnlyList<ThreadEvent> ThreadEvents { get; init; } = [];
}

public sealed class TimelineSceneOverviewRow
{
    public int SceneID { get; set; }
    public int? CharacterID { get; set; }
    public string? CharacterName { get; set; }
    public bool IsPov { get; set; }
    public int? StoryAssetID { get; set; }
    public string? AssetName { get; set; }
    public int? AssetImportance { get; set; }
    public string? AssetEventTypeName { get; set; }
    public string? LocationName { get; set; }
}

public sealed class LocationRepository(ISqlConnectionFactory connectionFactory) : ILocationRepository
{
    public async Task<LocationLookupData> GetLookupsAsync()
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.LocationLookup_All", commandType: CommandType.StoredProcedure);
        return new LocationLookupData
        {
            LocationTypes = (await result.ReadAsync<LocationType>()).ToList(),
            RelationshipTypes = (await result.ReadAsync<LocationRelationshipType>()).ToList()
        };
    }

    public async Task<IReadOnlyList<LocationItem>> ListByProjectAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<LocationItem>("dbo.Location_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<LocationItem?> GetAsync(int locationId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<LocationItem>("dbo.Location_Get", new { LocationID = locationId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SaveAsync(LocationItem location)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.Location_Save",
            new
            {
                location.LocationID,
                location.ProjectID,
                location.ParentLocationID,
                location.LocationName,
                location.LocationTypeID,
                location.Description,
                location.ShowInQuickAddBar,
                location.ExcludeFromCompanionDetection,
                location.DetectionPriority
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<LocationAlias>> ListAliasesAsync(int locationId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<LocationAlias>(
            "dbo.LocationAlias_ListByLocation",
            new { LocationID = locationId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> AddAliasAsync(int locationId, string alias, int? sortOrder)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.LocationAlias_Add",
            new { LocationID = locationId, Alias = alias, SortOrder = sortOrder },
            commandType: CommandType.StoredProcedure);
    }

    public async Task UpdateAliasAsync(int locationAliasId, string alias, int? sortOrder)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.LocationAlias_Update",
            new { LocationAliasID = locationAliasId, Alias = alias, SortOrder = sortOrder },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteAliasAsync(int locationAliasId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.LocationAlias_Delete",
            new { LocationAliasID = locationAliasId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task ArchiveAsync(int locationId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.Location_Archive", new { LocationID = locationId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByProjectAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<LocationRelationship>("dbo.LocationRelationship_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByLocationAsync(int locationId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<LocationRelationship>("dbo.LocationRelationship_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> SaveRelationshipAsync(LocationRelationship relationship)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.LocationRelationship_Save",
            new
            {
                relationship.LocationRelationshipID,
                relationship.FromLocationID,
                relationship.ToLocationID,
                relationship.LocationRelationshipTypeID,
                relationship.IsBidirectional,
                relationship.Strength,
                relationship.CanHearNormalSpeech,
                relationship.CanHearLoudNoise,
                relationship.CanSeeMovement,
                relationship.CanSeeClearly,
                relationship.CanTravelDirectly,
                relationship.Notes
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task ArchiveRelationshipAsync(int locationRelationshipId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.LocationRelationship_Archive", new { LocationRelationshipID = locationRelationshipId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<SceneAssetLocation>> ListSceneAssetLocationsAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneAssetLocation>("dbo.SceneAssetLocation_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> SaveSceneAssetLocationAsync(SceneAssetLocation sceneAssetLocation, bool updateCurrentLocation)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.SceneAssetLocation_Save",
            new
            {
                sceneAssetLocation.SceneAssetLocationID,
                sceneAssetLocation.SceneID,
                sceneAssetLocation.StoryAssetID,
                sceneAssetLocation.LocationID,
                sceneAssetLocation.Description,
                UpdateCurrentLocation = updateCurrentLocation
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteSceneAssetLocationAsync(int sceneAssetLocationId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.SceneAssetLocation_Delete", new { SceneAssetLocationID = sceneAssetLocationId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<SceneLocationSuggestion>> ListSceneLocationSuggestionsAsync(int sceneId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneLocationSuggestion>(
            "dbo.SceneLocationSuggestions_ListByScene",
            new { SceneID = sceneId, UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<SceneLocationSuggestionReviewResult?> AcceptSceneLocationSuggestionAsync(int suggestionId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<SceneLocationSuggestionReviewResult>(
            "dbo.SceneLocationSuggestion_Accept",
            new { SceneLocationSuggestionID = suggestionId, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<SceneLocationSuggestionReviewResult?> RejectSceneLocationSuggestionAsync(int suggestionId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<SceneLocationSuggestionReviewResult>(
            "dbo.SceneLocationSuggestion_Reject",
            new { SceneLocationSuggestionID = suggestionId, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<SceneLocationSuggestionCount>> CountSceneLocationSuggestionsByChapterAsync(int chapterId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneLocationSuggestionCount>(
            "dbo.SceneLocationSuggestions_CountByChapter",
            new { ChapterID = chapterId, UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<Scene>> ListScenesByLocationAsync(int locationId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Scene>("dbo.LocationScene_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<SceneCharacter>> ListCharactersByLocationAsync(int locationId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneCharacter>("dbo.LocationCharacter_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<SceneAssetLocation>> ListAssetsByLocationAsync(int locationId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneAssetLocation>("dbo.SceneAssetLocation_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }
}

public sealed class FloorPlanRepository(ISqlConnectionFactory connectionFactory) : IFloorPlanRepository
{
    public async Task<IReadOnlyList<FloorPlan>> ListByProjectAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<FloorPlan>("dbo.FloorPlan_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<FloorPlan?> GetAsync(int floorPlanId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<FloorPlan>("dbo.FloorPlan_Get", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<(FloorPlan? FloorPlan, IReadOnlyList<FloorPlanFloor> Floors, IReadOnlyList<FloorPlanBlock> Blocks, IReadOnlyList<FloorPlanTransition> Transitions)> GetEditorAsync(int floorPlanId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.FloorPlan_GetEditor", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure);
        var floorPlan = await result.ReadSingleOrDefaultAsync<FloorPlan>();
        var floors = (await result.ReadAsync<FloorPlanFloor>()).ToList();
        var blocks = (await result.ReadAsync<FloorPlanBlock>()).ToList();
        var transitions = (await result.ReadAsync<FloorPlanTransition>()).ToList();
        return (floorPlan, floors, blocks, transitions);
    }

    public async Task<FloorPlanFloor?> GetFloorAsync(int floorPlanFloorId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<FloorPlanFloor>("dbo.FloorPlanFloor_Get", new { FloorPlanFloorID = floorPlanFloorId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<FloorPlanBlock>> ListBlocksByFloorAsync(int floorPlanFloorId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<FloorPlanBlock>("dbo.FloorPlanBlock_ListByFloor", new { FloorPlanFloorID = floorPlanFloorId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<FloorPlanBlock>> ListBlocksByPlanAsync(int floorPlanId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<FloorPlanBlock>("dbo.FloorPlanBlock_ListByPlan", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<FloorPlanFloor>> ListFloorsByPlanAsync(int floorPlanId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<FloorPlanFloor>("dbo.FloorPlanFloor_ListByPlan", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<FloorPlanBlock?> GetBlockAsync(int floorPlanBlockId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<FloorPlanBlock>("dbo.FloorPlanBlock_Get", new { FloorPlanBlockID = floorPlanBlockId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SavePlanAsync(FloorPlan floorPlan)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.FloorPlan_Save",
            new { floorPlan.FloorPlanID, floorPlan.ProjectID, floorPlan.LocationID, floorPlan.Name, floorPlan.Description },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeletePlanAsync(int floorPlanId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.FloorPlan_Delete", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SaveFloorAsync(FloorPlanFloor floor)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.FloorPlanFloor_Save",
            new { floor.FloorPlanFloorID, floor.FloorPlanID, floor.LocationID, floor.Name, floor.Description, floor.SortOrder, floor.GridWidth, floor.GridHeight },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<bool> UpdateFloorBackgroundAsync(FloorPlanFloor floor)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QuerySingleAsync<int>(
            "dbo.FloorPlanFloor_UpdateBackground",
            new
            {
                floor.FloorPlanFloorID,
                floor.FloorPlanID,
                floor.BackgroundImagePath,
                floor.BackgroundImageOriginalFileName,
                floor.BackgroundOpacity,
                floor.BackgroundScale,
                floor.BackgroundOffsetX,
                floor.BackgroundOffsetY,
                floor.BackgroundLocked
            },
            commandType: CommandType.StoredProcedure);
        return rows > 0;
    }

    public async Task DeleteFloorAsync(int floorPlanFloorId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.FloorPlanFloor_Delete", new { FloorPlanFloorID = floorPlanFloorId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SaveBlockAsync(FloorPlanBlock block)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.FloorPlanBlock_Save",
            new
            {
                block.FloorPlanBlockID,
                block.FloorPlanFloorID,
                block.LocationID,
                block.X,
                block.Y,
                block.WidthCells,
                block.HeightCells,
                block.BlockType,
                block.LabelOverride,
                block.Notes
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteBlockAsync(int floorPlanBlockId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.FloorPlanBlock_Delete", new { FloorPlanBlockID = floorPlanBlockId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SaveTransitionAsync(FloorPlanTransition transition)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.FloorPlanTransition_Save",
            new
            {
                transition.FloorPlanTransitionID,
                transition.FloorPlanFloorID,
                transition.FromLocationID,
                transition.ToLocationID,
                transition.TransitionType,
                transition.Notes,
                transition.IsLocked,
                transition.DisplayLabel,
                transition.PositionWithinLocation
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteTransitionAsync(int floorPlanTransitionId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.FloorPlanTransition_Delete", new { FloorPlanTransitionID = floorPlanTransitionId }, commandType: CommandType.StoredProcedure);
    }
}

public sealed class WarningRepository(ISqlConnectionFactory connectionFactory) : IWarningRepository
{
    public async Task<WarningLookupData> GetLookupsAsync()
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.WarningLookup_All", commandType: CommandType.StoredProcedure);
        return new WarningLookupData
        {
            WarningTypes = (await result.ReadAsync<WarningType>()).ToList(),
            Severities = (await result.ReadAsync<WarningSeverity>()).ToList()
        };
    }

    public async Task<ContinuityValidationSummary> ValidateProjectAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<ContinuityValidationSummary>(
            "dbo.ContinuityValidation_ValidateProject",
            new { ProjectID = projectId, TriggeredBy = "User" },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<ContinuityValidationSummary> ValidateBookAsync(int bookId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<ContinuityValidationSummary>(
            "dbo.ContinuityValidation_ValidateBook",
            new { BookID = bookId, TriggeredBy = "User" },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<ContinuityValidationSummary> ValidateSceneAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<ContinuityValidationSummary>(
            "dbo.ContinuityValidation_ValidateScene",
            new { SceneID = sceneId, TriggeredBy = "User" },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<ContinuityWarning>> ListAsync(int projectId, int? bookId, int? sceneId, int? warningTypeId, int? warningSeverityId, string? entityType, bool includeDismissed, bool includeIntentional)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ContinuityWarning>(
            "dbo.ContinuityWarning_List",
            new
            {
                ProjectID = projectId,
                BookID = bookId,
                SceneID = sceneId,
                WarningTypeID = warningTypeId,
                WarningSeverityID = warningSeverityId,
                EntityType = string.IsNullOrWhiteSpace(entityType) ? null : entityType,
                IncludeDismissed = includeDismissed,
                IncludeIntentional = includeIntentional
            },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<ContinuityWarning>> ListDashboardUnresolvedAsync(int projectId, int count)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ContinuityWarning>(
            "dbo.ContinuityWarning_ListDashboardUnresolved",
            new { ProjectID = projectId, Count = count },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<ContinuityWarning>> ListBySceneAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ContinuityWarning>("dbo.ContinuityWarning_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<SceneWarningCount>> GetSceneCountsAsync(int projectId, int? bookId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneWarningCount>("dbo.ContinuityWarning_SceneCounts", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task DismissAsync(int continuityWarningId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.ContinuityWarning_Dismiss", new { ContinuityWarningID = continuityWarningId }, commandType: CommandType.StoredProcedure);
    }

    public async Task MarkIntentionalAsync(int continuityWarningId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.ContinuityWarning_MarkIntentional", new { ContinuityWarningID = continuityWarningId }, commandType: CommandType.StoredProcedure);
    }

    public async Task RestoreAsync(int continuityWarningId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.ContinuityWarning_Restore", new { ContinuityWarningID = continuityWarningId }, commandType: CommandType.StoredProcedure);
    }

    public async Task FinaliseSceneDependencyWarningsAsync(int projectId, int? bookId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.SceneDependency_FinaliseWarnings", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure);
    }
}

public sealed class ContinuityWarningAcknowledgementRepository(ISqlConnectionFactory connectionFactory) : IContinuityWarningAcknowledgementRepository
{
    public async Task<IReadOnlyList<ContinuityWarningAcknowledgement>> ListByProjectAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ContinuityWarningAcknowledgement>(
            "dbo.ContinuityWarningAcknowledgement_ListByProject",
            new { ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<ContinuityWarningAcknowledgement?> GetAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<ContinuityWarningAcknowledgement>(
            "dbo.ContinuityWarningAcknowledgement_Get",
            new
            {
                ProjectID = projectId,
                WarningType = warningType,
                SceneID = sceneId,
                CharacterID = characterId,
                AssetID = assetId
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task SaveAsync(ContinuityWarningAcknowledgement acknowledgement)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.ContinuityWarningAcknowledgement_Save",
            new
            {
                acknowledgement.ProjectID,
                acknowledgement.WarningType,
                acknowledgement.SceneID,
                acknowledgement.CharacterID,
                acknowledgement.AssetID,
                acknowledgement.AcknowledgedByUserID,
                acknowledgement.Notes
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.ContinuityWarningAcknowledgement_Delete",
            new
            {
                ProjectID = projectId,
                WarningType = warningType,
                SceneID = sceneId,
                CharacterID = characterId,
                AssetID = assetId
            },
            commandType: CommandType.StoredProcedure);
    }
}

public sealed class AnalyticsData
{
    public IReadOnlyList<AnalyticsMetricPoint> MetricPoints { get; init; } = [];
    public IReadOnlyList<AnalyticsRevisionSummary> RevisionSummary { get; init; } = [];
    public IReadOnlyList<AnalyticsRevisionScene> RevisionAttentionScenes { get; init; } = [];
    public IReadOnlyList<AnalyticsPurposeSummary> PurposeSummary { get; init; } = [];
    public IReadOnlyList<AnalyticsSceneGap> SceneGaps { get; init; } = [];
    public IReadOnlyList<AnalyticsThreadHealth> ThreadHealth { get; init; } = [];
    public IReadOnlyList<AnalyticsAssetHealth> AssetHealth { get; init; } = [];
    public IReadOnlyList<AnalyticsWarningSeverityCount> WarningSeverityCounts { get; init; } = [];
    public IReadOnlyList<AnalyticsWarningTypeCount> WarningTypeCounts { get; init; } = [];
    public IReadOnlyList<AnalyticsWarningHotspot> WarningHotspots { get; init; } = [];
    public IReadOnlyList<AnalyticsPovSummary> PovSummary { get; init; } = [];
    public IReadOnlyList<AnalyticsMultiPovChapter> MultiPovChapters { get; init; } = [];
    public IReadOnlyList<AnalyticsCharacterAppearance> CharacterAppearances { get; init; } = [];
    public IReadOnlyList<AnalyticsBusyScene> BusyScenes { get; init; } = [];
    public IReadOnlyList<AnalyticsLocationUsage> LocationUsage { get; init; } = [];
}

public sealed class AnalyticsRepository(ISqlConnectionFactory connectionFactory) : IAnalyticsRepository
{
    public async Task<AnalyticsData> GetStoryHealthAsync(int projectId, int? bookId, decimal? startChapterNumber, decimal? endChapterNumber, decimal? startSceneNumber, decimal? endSceneNumber)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync(
            "dbo.Analytics_StoryHealth",
            new
            {
                ProjectID = projectId,
                BookID = bookId,
                StartChapterNumber = startChapterNumber,
                EndChapterNumber = endChapterNumber,
                StartSceneNumber = startSceneNumber,
                EndSceneNumber = endSceneNumber
            },
            commandType: CommandType.StoredProcedure);

        return new AnalyticsData
        {
            MetricPoints = (await result.ReadAsync<AnalyticsMetricPoint>()).ToList(),
            RevisionSummary = (await result.ReadAsync<AnalyticsRevisionSummary>()).ToList(),
            RevisionAttentionScenes = (await result.ReadAsync<AnalyticsRevisionScene>()).ToList(),
            PurposeSummary = (await result.ReadAsync<AnalyticsPurposeSummary>()).ToList(),
            SceneGaps = (await result.ReadAsync<AnalyticsSceneGap>()).ToList(),
            ThreadHealth = (await result.ReadAsync<AnalyticsThreadHealth>()).ToList(),
            AssetHealth = (await result.ReadAsync<AnalyticsAssetHealth>()).ToList(),
            WarningSeverityCounts = (await result.ReadAsync<AnalyticsWarningSeverityCount>()).ToList(),
            WarningTypeCounts = (await result.ReadAsync<AnalyticsWarningTypeCount>()).ToList(),
            WarningHotspots = (await result.ReadAsync<AnalyticsWarningHotspot>()).ToList(),
            PovSummary = (await result.ReadAsync<AnalyticsPovSummary>()).ToList(),
            MultiPovChapters = (await result.ReadAsync<AnalyticsMultiPovChapter>()).ToList(),
            CharacterAppearances = (await result.ReadAsync<AnalyticsCharacterAppearance>()).ToList(),
            BusyScenes = (await result.ReadAsync<AnalyticsBusyScene>()).ToList(),
            LocationUsage = (await result.ReadAsync<AnalyticsLocationUsage>()).ToList()
        };
    }
}

public sealed class StoryBibleRepository(ISqlConnectionFactory connectionFactory) : IStoryBibleRepository
{
    public async Task<IReadOnlyList<StoryBibleCharacterSummary>> GetCharacterSummariesAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryBibleCharacterSummary>("dbo.StoryBible_CharacterSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
    }

    public async Task<IReadOnlyList<StoryBibleThreadSummary>> GetThreadSummariesAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryBibleThreadSummary>("dbo.StoryBible_ThreadSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
    }

    public async Task<IReadOnlyList<StoryBibleAssetSummary>> GetAssetSummariesAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryBibleAssetSummary>("dbo.StoryBible_AssetSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
    }

    public async Task<IReadOnlyList<StoryBibleRelationshipSummary>> GetRelationshipSummariesAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryBibleRelationshipSummary>("dbo.StoryBible_RelationshipSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
    }

    public async Task<IReadOnlyList<StoryBibleLocationSummary>> GetLocationSummariesAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryBibleLocationSummary>("dbo.StoryBible_LocationSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
    }

    public async Task<IReadOnlyList<StoryBibleTimelineSummary>> GetTimelineSummaryAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryBibleTimelineSummary>("dbo.StoryBible_TimelineSummary", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
    }

    public async Task<IReadOnlyList<StoryBibleAttentionItem>> GetOpenQuestionsAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryBibleAttentionItem>("dbo.StoryBible_OpenQuestions", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
    }

    public async Task<IReadOnlyList<StoryBibleRevisionItem>> GetRevisionProgressAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryBibleRevisionItem>("dbo.StoryBible_RevisionProgress", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
    }

    public async Task<IReadOnlyList<StoryBibleSearchResult>> SearchAsync(int projectId, string? query)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryBibleSearchResult>("dbo.StoryBible_Search", new { ProjectID = projectId, Query = query }, commandType: CommandType.StoredProcedure)).ToList();
    }
}

public sealed class WriterWorkspaceRepository(ISqlConnectionFactory connectionFactory) : IWriterWorkspaceRepository
{
    public async Task<IReadOnlyList<SceneNoteType>> ListNoteTypesAsync()
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneNoteType>("dbo.SceneNoteType_List", commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<SceneAttachmentType>> ListAttachmentTypesAsync()
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneAttachmentType>("dbo.SceneAttachmentType_List", commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<SceneWorkflow> GetWorkflowAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<SceneWorkflow>("dbo.SceneWorkflow_Get", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
    }

    public async Task SaveWorkflowAsync(SceneWorkflow workflow)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.SceneWorkflow_Save",
            new
            {
                workflow.SceneID,
                workflow.DraftStatus,
                workflow.EstimatedWordCount,
                workflow.ActualWordCount,
                workflow.DraftedDate,
                workflow.LastWorkedOn,
                workflow.ReadyForDraft,
                workflow.ReadyForRevision,
                workflow.ReadyForPolish,
                workflow.IsBlocked,
                workflow.BlockedReason,
                workflow.Priority
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<SceneNote>> ListNotesAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneNote>("dbo.SceneNote_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> SaveNoteAsync(SceneNote note)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.SceneNote_Save",
            new { note.SceneNoteID, note.SceneID, note.SceneNoteTypeID, note.NoteTitle, note.NoteText, note.SortOrder, note.IsPinned, note.IsResolved },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteNoteAsync(int sceneNoteId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.SceneNote_Delete", new { SceneNoteID = sceneNoteId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<SceneChecklistItem>> ListChecklistItemsAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneChecklistItem>("dbo.SceneChecklist_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> SaveChecklistItemAsync(SceneChecklistItem item)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.SceneChecklist_Save",
            new { item.SceneChecklistItemID, item.SceneID, item.ItemText, item.IsCompleted, item.SortOrder },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteChecklistItemAsync(int sceneChecklistItemId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.SceneChecklist_Delete", new { SceneChecklistItemID = sceneChecklistItemId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<SceneAttachment>> ListAttachmentsAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneAttachment>("dbo.SceneAttachment_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> SaveAttachmentAsync(SceneAttachment attachment)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.SceneAttachment_Save",
            new { attachment.SceneAttachmentID, attachment.SceneID, attachment.SceneAttachmentTypeID, attachment.Title, attachment.FilePath, attachment.ExternalUrl, attachment.Notes },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteAttachmentAsync(int sceneAttachmentId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.SceneAttachment_Delete", new { SceneAttachmentID = sceneAttachmentId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<ChapterGoal> GetChapterGoalAsync(int chapterId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<ChapterGoal>("dbo.ChapterGoal_Get", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure);
    }

    public async Task SaveChapterGoalAsync(ChapterGoal goal)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.ChapterGoal_Save",
            new { goal.ChapterID, goal.GoalSummary, goal.EmotionalGoal, goal.ReaderTakeaway, goal.MustInclude, goal.RisksOrConcerns },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<ChapterWorkflowScene>> GetChapterWorkflowAsync(int chapterId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ChapterWorkflowScene>("dbo.ChapterWorkflow_Summary", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<WriterDashboardScene>> GetWriterDashboardScenesAsync(int? projectId, string queue)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<WriterDashboardScene>("dbo.WriterDashboard_Scenes", new { ProjectID = projectId, Queue = queue }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<StoryBibleAttentionItem>> GetWriterDashboardRemindersAsync(int? projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<StoryBibleAttentionItem>("dbo.WriterDashboard_Reminders", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }
}

public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFactory) : IWritingScheduleRepository
{
    public async Task<IReadOnlyList<WritingPlan>> GetWritingPlansByUserAsync(int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<WritingPlan>("dbo.GetWritingPlansByUser", new { UserID = userId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<WritingPlan?> GetWritingPlanByIDAsync(int writingPlanId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<WritingPlan>("dbo.GetWritingPlanByID", new { WritingPlanID = writingPlanId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<int> CreateWritingPlanAsync(WritingPlan plan)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.CreateWritingPlan",
            new
            {
                plan.UserID,
                plan.ProjectID,
                plan.BookID,
                plan.PlanName,
                plan.GoalType,
                plan.StartDate,
                plan.DeadlineDate,
                plan.AvailableDaysJson,
                plan.SessionLengthMinutes,
                plan.IncludeBlockedScenes,
                plan.RebalanceMode,
                plan.IsActive
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task UpdateWritingPlanAsync(WritingPlan plan)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.UpdateWritingPlan",
            new
            {
                plan.WritingPlanID,
                plan.UserID,
                plan.ProjectID,
                plan.BookID,
                plan.PlanName,
                plan.GoalType,
                plan.StartDate,
                plan.DeadlineDate,
                plan.AvailableDaysJson,
                plan.SessionLengthMinutes,
                plan.IncludeBlockedScenes,
                plan.RebalanceMode,
                plan.IsActive
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteWritingPlanAsync(int writingPlanId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.DeleteWritingPlan", new { WritingPlanID = writingPlanId }, commandType: CommandType.StoredProcedure);
    }

    public async Task SetWritingPlanActiveAsync(int writingPlanId, bool isActive)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.SetWritingPlanActive", new { WritingPlanID = writingPlanId, IsActive = isActive }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<WritingScheduleItem>> GetScheduleItemsByPlanAsync(int writingPlanId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<WritingScheduleItem>("dbo.GetScheduleItemsByPlan", new { WritingPlanID = writingPlanId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<WritingScheduleItem>> GetScheduleItemsByDateRangeAsync(int writingPlanId, DateTime startDate, DateTime endDate)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<WritingScheduleItem>(
            "dbo.GetScheduleItemsByDateRange",
            new { WritingPlanID = writingPlanId, StartDate = startDate, EndDate = endDate },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> CreateScheduleItemAsync(WritingScheduleItem item)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.CreateScheduleItem",
            new
            {
                item.WritingPlanID,
                item.SceneID,
                item.ScheduledDate,
                item.TaskType,
                item.EstimatedMinutes,
                item.TargetWords,
                item.ScheduleStatus,
                item.CompletedDateUtc,
                item.Notes
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task UpdateScheduleItemAsync(WritingScheduleItem item)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.UpdateScheduleItem",
            new
            {
                item.WritingScheduleItemID,
                item.WritingPlanID,
                item.SceneID,
                item.ScheduledDate,
                item.TaskType,
                item.EstimatedMinutes,
                item.TargetWords,
                item.ScheduleStatus,
                item.CompletedDateUtc,
                item.Notes
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteScheduleItemAsync(int writingScheduleItemId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.DeleteScheduleItem", new { WritingScheduleItemID = writingScheduleItemId }, commandType: CommandType.StoredProcedure);
    }

    public async Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.UpdateScheduleItemStatus",
            new { WritingScheduleItemID = writingScheduleItemId, ScheduleStatus = scheduleStatus, CompletedDateUtc = completedDateUtc },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<WritingPlanManagementItem>> GetWritingPlansForManagementAsync(int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<WritingPlanManagementItem>(
            "dbo.WritingPlan_ListForManagement",
            new { UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<WritingScheduleScene>> GetSchedulingScenesAsync(int projectId, int? bookId, bool includeBlockedScenes)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<WritingScheduleScene>(
            "dbo.WritingScheduleScene_ListForPlanning",
            new { ProjectID = projectId, BookID = bookId, IncludeBlockedScenes = includeBlockedScenes },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task ReplaceScheduleItemsAsync(int writingPlanId, IReadOnlyList<WritingScheduleItem> items)
    {
        using var connection = connectionFactory.CreateConnection();
        connection.Open();
        using var transaction = connection.BeginTransaction();

        try
        {
            await connection.ExecuteAsync(
                "dbo.WritingScheduleItem_DeleteByPlan",
                new { WritingPlanID = writingPlanId },
                transaction,
                commandType: CommandType.StoredProcedure);

            foreach (var item in items)
            {
                await connection.ExecuteAsync(
                    "dbo.CreateScheduleItem",
                    new
                    {
                        item.WritingPlanID,
                        item.SceneID,
                        item.ScheduledDate,
                        item.TaskType,
                        item.EstimatedMinutes,
                        item.TargetWords,
                        item.ScheduleStatus,
                        item.CompletedDateUtc,
                        item.Notes
                    },
                    transaction,
                    commandType: CommandType.StoredProcedure);
            }

            transaction.Commit();
        }
        catch
        {
            transaction.Rollback();
            throw;
        }
    }

    public async Task<IReadOnlyList<WritingPlanSummary>> GetWritingPlanSummariesByUserAsync(int userId, int? projectId = null)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<WritingPlanSummary>(
            "dbo.WritingPlanSummary_ListByUser",
            new { UserID = userId, ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<WritingSchedulePreviewItem>> GetSchedulePreviewItemsAsync(int writingPlanId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<WritingSchedulePreviewItem>(
            "dbo.WritingSchedulePreview_ListByPlan",
            new { WritingPlanID = writingPlanId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task DeleteScheduleItemsByPlanAsync(int writingPlanId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.WritingScheduleItem_DeleteByPlan",
            new { WritingPlanID = writingPlanId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<WritingScheduleDashboardItem>> GetActiveScheduleDashboardItemsAsync(int userId, int? projectId = null)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<WritingScheduleDashboardItem>(
            "dbo.WritingScheduleDashboard_ListActive",
            new { UserID = userId, ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> UpdatePlannedScheduleItemStatusForUserAsync(int writingScheduleItemId, int userId, string scheduleStatus, DateTime? completedDateUtc)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.ExecuteAsync(
            "dbo.WritingScheduleItem_UpdatePlannedStatusForUser",
            new
            {
                WritingScheduleItemID = writingScheduleItemId,
                UserID = userId,
                ScheduleStatus = scheduleStatus,
                CompletedDateUtc = completedDateUtc
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<WritingPlanSummary>> GetPlansRequiringRebalanceAsync(int userId, DateTime today, int? projectId = null)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<WritingPlanSummary>(
            "dbo.WritingPlanSummary_ListRequiringRebalance",
            new { UserID = userId, Today = today.Date, ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task UpdatePlannedScheduleItemDatesAsync(int writingPlanId, IReadOnlyList<WritingScheduleItemDateChange> changes)
    {
        if (changes.Count == 0)
        {
            return;
        }

        using var connection = connectionFactory.CreateConnection();
        connection.Open();
        using var transaction = connection.BeginTransaction();

        try
        {
            foreach (var change in changes)
            {
                await connection.ExecuteAsync(
                    "dbo.WritingScheduleItem_UpdatePlannedDate",
                    new
                    {
                        WritingPlanID = writingPlanId,
                        change.WritingScheduleItemID,
                        ScheduledDate = change.ScheduledDate.Date
                    },
                    transaction,
                    commandType: CommandType.StoredProcedure);
            }

            transaction.Commit();
        }
        catch
        {
            transaction.Rollback();
            throw;
        }
    }
}

public sealed class ExportRepository(ISqlConnectionFactory connectionFactory) : IExportRepository
{
    public async Task<ExportPacket> GetStoryBibleAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.Export_StoryBible", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return new ExportPacket
        {
            Title = "Story Bible",
            Characters = (await result.ReadAsync<StoryBibleCharacterSummary>()).ToList(),
            PlotThreads = (await result.ReadAsync<StoryBibleThreadSummary>()).ToList(),
            Assets = (await result.ReadAsync<StoryBibleAssetSummary>()).ToList(),
            Relationships = (await result.ReadAsync<StoryBibleRelationshipSummary>()).ToList(),
            Locations = (await result.ReadAsync<StoryBibleLocationSummary>()).ToList(),
            Timeline = (await result.ReadAsync<StoryBibleTimelineSummary>()).ToList(),
            OpenQuestions = (await result.ReadAsync<StoryBibleAttentionItem>()).ToList(),
            RevisionItems = (await result.ReadAsync<StoryBibleRevisionItem>()).ToList()
        };
    }

    public async Task<ExportPacket> GetNarrativeTimelineAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<StoryBibleTimelineSummary>("dbo.Export_NarrativeTimeline", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return new ExportPacket { Title = "Narrative Timeline Summary", Timeline = rows.ToList() };
    }

    public async Task<ExportPacket> GetCharacterReferenceAsync(int characterId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.Export_CharacterReference", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
        return new ExportPacket
        {
            Title = "Character Reference Pack",
            Character = await result.ReadSingleOrDefaultAsync<Character>(),
            CharacterRelationships = (await result.ReadAsync<CharacterRelationship>()).ToList(),
            CharacterAppearances = (await result.ReadAsync<SceneCharacter>()).ToList(),
            CharacterKnowledge = (await result.ReadAsync<CharacterKnowledgeItem>()).ToList(),
            CharacterCustody = (await result.ReadAsync<AssetCustodyEvent>()).ToList(),
            Items = (await result.ReadAsync<ExportBriefItem>()).ToList()
        };
    }

    public async Task<ExportPacket> GetSceneBriefAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.Export_SceneBrief", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return new ExportPacket
        {
            Title = "Scene Brief",
            SceneHeader = await result.ReadSingleOrDefaultAsync<ExportSceneBriefHeader>(),
            Items = (await result.ReadAsync<ExportBriefItem>()).ToList()
        };
    }

    public async Task<ExportPacket> GetChapterBriefAsync(int chapterId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.Export_ChapterBrief", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure);
        return new ExportPacket
        {
            Title = "Chapter Brief",
            ChapterHeader = await result.ReadSingleOrDefaultAsync<ExportChapterBriefHeader>(),
            Timeline = (await result.ReadAsync<StoryBibleTimelineSummary>()).ToList(),
            Items = (await result.ReadAsync<ExportBriefItem>()).ToList()
        };
    }

    public async Task<ExportPacket> GetWriterSessionBriefAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.Export_WriterSessionBrief", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return new ExportPacket
        {
            Title = "Writing Session Brief",
            SceneHeader = await result.ReadSingleOrDefaultAsync<ExportSceneBriefHeader>(),
            Items = (await result.ReadAsync<ExportBriefItem>()).ToList()
        };
    }

    public async Task<ExportPacket> GetResearchPackAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ExportResearchItem>("dbo.Export_ResearchPack", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return new ExportPacket { Title = "Research Pack", ResearchItems = rows.ToList() };
    }

    public async Task<ExportPacket> GetSearchResultsAsync(int projectId, string query)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<StoryBibleSearchResult>("dbo.Export_SearchResults", new { ProjectID = projectId, Query = query }, commandType: CommandType.StoredProcedure);
        return new ExportPacket { Title = $"Search Results: {query}", SearchResults = rows.ToList() };
    }
}

public sealed class DynamicsRepository(ISqlConnectionFactory connectionFactory) : IDynamicsRepository
{
    public async Task<(Character? Character, IReadOnlyList<CharacterArcPoint> ArcPoints, IReadOnlyList<RelationshipEvent> RelationshipEvents, IReadOnlyList<CharacterKnowledgeItem> KnowledgeEvents, IReadOnlyList<DynamicsIssue> Issues)> GetCharacterArcAsync(int characterId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.Dynamics_CharacterArc", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
        return (
            await result.ReadSingleOrDefaultAsync<Character>(),
            (await result.ReadAsync<CharacterArcPoint>()).ToList(),
            (await result.ReadAsync<RelationshipEvent>()).ToList(),
            (await result.ReadAsync<CharacterKnowledgeItem>()).ToList(),
            (await result.ReadAsync<DynamicsIssue>()).ToList());
    }

    public async Task<(CharacterRelationship? Relationship, IReadOnlyList<RelationshipProgressionPoint> Events, IReadOnlyList<DynamicsIssue> Issues)> GetRelationshipEvolutionAsync(int characterRelationshipId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.Dynamics_RelationshipEvolution", new { CharacterRelationshipID = characterRelationshipId }, commandType: CommandType.StoredProcedure);
        return (
            await result.ReadSingleOrDefaultAsync<CharacterRelationship>(),
            (await result.ReadAsync<RelationshipProgressionPoint>()).ToList(),
            (await result.ReadAsync<DynamicsIssue>()).ToList());
    }

    public async Task<(Character? Character, IReadOnlyList<CharacterKnowledgeItem> KnowledgeEvents, IReadOnlyList<DynamicsIssue> Issues)> GetKnowledgeTimelineAsync(int characterId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.Dynamics_KnowledgeTimeline", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
        return (
            await result.ReadSingleOrDefaultAsync<Character>(),
            (await result.ReadAsync<CharacterKnowledgeItem>()).ToList(),
            (await result.ReadAsync<DynamicsIssue>()).ToList());
    }

    public async Task<(IReadOnlyList<CharacterPairingSummary> Pairings, IReadOnlyList<DynamicsIssue> Issues)> GetStoryDynamicsAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.Dynamics_StoryDashboard", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return ((await result.ReadAsync<CharacterPairingSummary>()).ToList(), (await result.ReadAsync<DynamicsIssue>()).ToList());
    }

    public async Task RunContinuityChecksAsync(int projectId, int? bookId, int? sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.Dynamics_RunContinuityChecks", new { ProjectID = projectId, BookID = bookId, SceneID = sceneId }, commandType: CommandType.StoredProcedure);
    }
}

public sealed class ScenarioRepository(ISqlConnectionFactory connectionFactory) : IScenarioRepository
{
    public async Task<IReadOnlyList<Scenario>> ListAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Scenario>("dbo.Scenario_List", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<Scenario?> GetAsync(int scenarioId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<Scenario>("dbo.Scenario_Get", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<int> CreateAsync(int projectId, int? bookId, string scenarioName, string? description)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.Scenario_Create",
            new { ProjectID = projectId, BookID = bookId, ScenarioName = scenarioName, Description = description },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<(IReadOnlyList<Book> Books, IReadOnlyList<Chapter> Chapters, IReadOnlyList<ScenarioScene> Scenes, IReadOnlyList<ScenarioWarning> Warnings)> GetTimelineAsync(int scenarioId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.Scenario_Timeline", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure);
        return (
            (await result.ReadAsync<Book>()).ToList(),
            (await result.ReadAsync<Chapter>()).ToList(),
            (await result.ReadAsync<ScenarioScene>()).ToList(),
            (await result.ReadAsync<ScenarioWarning>()).ToList());
    }

    public async Task MoveSceneAsync(int scenarioId, int sceneId, int? anchorSceneId, int? targetChapterId, string position)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.Scenario_MoveScene",
            new { ScenarioID = scenarioId, SceneID = sceneId, AnchorSceneID = anchorSceneId, TargetChapterID = targetChapterId, Position = position },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<int> ValidateAsync(int scenarioId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>("dbo.Scenario_Validate", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<(IReadOnlyList<ScenarioComparisonRow> MovedScenes, IReadOnlyList<ScenarioWarning> Warnings)> CompareAsync(int scenarioId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.Scenario_Compare", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure);
        return ((await result.ReadAsync<ScenarioComparisonRow>()).ToList(), (await result.ReadAsync<ScenarioWarning>()).ToList());
    }

    public async Task ApplyAsync(int scenarioId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.Scenario_ApplyToMain", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure);
    }

    public async Task ArchiveAsync(int scenarioId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.Scenario_Archive", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure);
    }
}

public sealed class ArchiveRepository(ISqlConnectionFactory connectionFactory) : IArchiveRepository
{
    public async Task<IReadOnlyList<ArchivedItem>> ListAsync(int? projectId, string? entityType)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ArchivedItem>(
            "dbo.Archive_List",
            new { ProjectID = projectId, EntityType = entityType },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<ArchivedItem>> ListForUserAsync(int? projectId, string? entityType, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ArchivedItem>(
            "dbo.Archive_ListForUser",
            new { ProjectID = projectId, EntityType = entityType, UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<Project>> ListProjectFiltersAsync()
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Project>(
            "dbo.Archive_ProjectFilterList",
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<Project>> ListProjectFiltersForUserAsync(int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Project>(
            "dbo.Archive_ProjectFilterListForUser",
            new { UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task ArchiveAsync(string entityType, int entityId, string? reason)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.Archive_Entity",
            new { EntityType = entityType, EntityID = entityId, ArchivedReason = reason },
            commandType: CommandType.StoredProcedure);
    }

    public async Task RestoreAsync(string entityType, int entityId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.Archive_Restore",
            new { EntityType = entityType, EntityID = entityId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<ArchiveDeleteResult> DeleteForeverAsync(string entityType, int entityId, string confirmationText, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<ArchiveDeleteResult>(
            "dbo.Archive_DeleteForever",
            new { EntityType = entityType, EntityID = entityId, ConfirmationText = confirmationText, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }
}

public sealed class AcceptanceRepository(ISqlConnectionFactory connectionFactory) : IAcceptanceRepository
{
    public async Task<IReadOnlyList<Phase1AcceptanceChecklistItem>> GetPhase1ChecklistAsync(int? projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Phase1AcceptanceChecklistItem>(
            "dbo.Acceptance_Phase1Checklist",
            new { ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }
}

public sealed class SceneDependencyRepository(ISqlConnectionFactory connectionFactory) : ISceneDependencyRepository
{
    public async Task<SceneDependencyLookupData> GetLookupsAsync()
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneDependencyType>("dbo.SceneDependencyLookup_All", commandType: CommandType.StoredProcedure);
        return new SceneDependencyLookupData { DependencyTypes = rows.ToList() };
    }

    public async Task<IReadOnlyList<SceneDependency>> ListBySceneAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneDependency>("dbo.SceneDependency_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<SceneDependency>> ListByProjectAsync(int projectId, int? bookId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneDependency>("dbo.SceneDependency_ListByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<SceneDependencyCount>> GetCountsAsync(int projectId, int? bookId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneDependencyCount>("dbo.SceneDependency_Counts", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> SaveAsync(SceneDependency dependency)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.SceneDependency_Save",
            new
            {
                dependency.SceneDependencyID,
                dependency.ProjectID,
                dependency.SourceSceneID,
                dependency.TargetSceneID,
                dependency.SceneDependencyTypeID,
                dependency.Description,
                dependency.IsHardDependency
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task ArchiveAsync(int sceneDependencyId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.SceneDependency_Archive", new { SceneDependencyID = sceneDependencyId }, commandType: CommandType.StoredProcedure);
    }
}

public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) : IProjectRepository
{
    public async Task<IReadOnlyList<Project>> ListAsync()
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Project>("dbo.Project_List", commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<Project>> ListForUserAsync(int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Project>("dbo.Project_ListForUser", new { UserID = userId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<Project>> ListActiveForUserAsync(int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Project>("dbo.Project_ListActiveForUser", new { UserID = userId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<Project>> ListArchivedForUserAsync(int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Project>("dbo.Project_ListArchivedForUser", new { UserID = userId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<Project>> ListOwnedForAccountDeletionAsync(int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Project>(
            "dbo.Project_ListOwnedForAccountDeletion",
            new { UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<Project?> GetAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<Project>("dbo.Project_Get", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<Project?> GetForUserAsync(int projectId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<Project>("dbo.Project_GetForUser", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<Project?> GetArchivedForOwnerAsync(int projectId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<Project>("dbo.Project_GetArchivedForOwner", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<ProjectDashboardMetrics> GetDashboardMetricsAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<ProjectDashboardMetrics>(
            "dbo.ProjectDashboard_Metrics",
            new { ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<ProjectDashboardAttentionThread>> ListDashboardAttentionThreadsAsync(int projectId, int count)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ProjectDashboardAttentionThread>(
            "dbo.ProjectDashboard_AttentionThreads",
            new { ProjectID = projectId, Count = count },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<GenreMetricPreset>> ListGenreMetricPresetsAsync()
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<GenreMetricPreset>("dbo.GenreMetricPreset_List", commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public Task<int> SaveAsync(Project project) => SaveAsync(project, null);

    public Task<int> SaveAsync(Project project, int userId, string? genreMetricPresetKey)
        => SaveInternalAsync(project, userId, genreMetricPresetKey);

    public async Task<int> SaveAsync(Project project, string? genreMetricPresetKey)
        => await SaveInternalAsync(project, null, genreMetricPresetKey);

    private async Task<int> SaveInternalAsync(Project project, int? userId, string? genreMetricPresetKey)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.Project_Save",
            new { project.ProjectID, project.ProjectName, project.Description, GenreMetricPresetKey = genreMetricPresetKey, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task ArchiveAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.Project_Archive", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
    }

    public async Task ArchiveForUserAsync(int projectId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.Project_ArchiveForUser", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<bool> ArchiveForOwnerAsync(int projectId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rowsAffected = await connection.QuerySingleAsync<int>(
            "dbo.Project_ArchiveForOwner",
            new { ProjectID = projectId, UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rowsAffected > 0;
    }

    public async Task<bool> RestoreForOwnerAsync(int projectId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rowsAffected = await connection.QuerySingleAsync<int>(
            "dbo.Project_RestoreForOwner",
            new { ProjectID = projectId, UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rowsAffected > 0;
    }

    public async Task<IReadOnlyList<string>> ListHardDeleteFilePathsForOwnerAsync(int projectId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<string>(
            "dbo.ProjectHardDelete_ListFilePathsForOwner",
            new { ProjectID = projectId, UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rows.Where(path => !string.IsNullOrWhiteSpace(path)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
    }

    public async Task HardDeleteForOwnerAsync(int projectId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.ProjectHardDelete_ForOwner",
            new { ProjectID = projectId, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<string>> ListHardDeleteFilePathsForAccountDeletionAsync(int projectId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<string>(
            "dbo.ProjectHardDelete_ListFilePathsForAccountDeletion",
            new { ProjectID = projectId, UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task HardDeleteForAccountDeletionAsync(int projectId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.ProjectHardDelete_ForAccountDeletion",
            new { ProjectID = projectId, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }
}

public sealed class ProjectCollaborationRepository(ISqlConnectionFactory connectionFactory) : IProjectCollaborationRepository
{
    public async Task<int?> GetOwnerUserIdAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<int?>(
            "dbo.ProjectCollaboration_GetOwner",
            new { ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<ProjectCollaborator>> ListCollaboratorsAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ProjectCollaborator>(
            "dbo.ProjectCollaboration_ListCollaborators",
            new { ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<ProjectInvitation>> ListPendingInvitationsAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ProjectInvitation>(
            "dbo.ProjectCollaboration_ListPendingInvitations",
            new { ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<bool> CollaboratorCountsForOwnerAsync(int ownerUserId, int collaboratorUserId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<bool>(
            "dbo.ProjectCollaboration_CollaboratorCountsForOwner",
            new { OwnerUserID = ownerUserId, CollaboratorUserID = collaboratorUserId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<bool> AddCollaboratorAsync(int projectId, int userId, int invitedByUserId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<bool>(
            "dbo.ProjectCollaboration_AddCollaborator",
            new { ProjectID = projectId, UserID = userId, InvitedByUserID = invitedByUserId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<bool> RemoveCollaboratorAsync(int projectId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<bool>(
            "dbo.ProjectCollaboration_RemoveCollaborator",
            new { ProjectID = projectId, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<int> CreatePendingInvitationAsync(int projectId, string emailAddress, int invitedByUserId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.ProjectCollaboration_CreatePendingInvitation",
            new { ProjectID = projectId, EmailAddress = emailAddress, InvitedByUserID = invitedByUserId },
            commandType: CommandType.StoredProcedure);
    }
}

public sealed class ProjectActivityRepository(ISqlConnectionFactory connectionFactory) : IProjectActivityRepository
{
    public async Task<IReadOnlyList<ProjectActivity>> ListForProjectAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ProjectActivity>(
            "dbo.ProjectActivity_ListForProject",
            new { ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<ProjectActivity>> ListRecentForProjectAsync(int projectId, int count)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ProjectActivity>(
            "dbo.ProjectActivity_ListRecent",
            new { ProjectID = projectId, Count = count },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<long> RecordAsync(int projectId, int? userId, string activityType, string entityType, int? entityId, string? entityName, string? description = null)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<long>(
            "dbo.ProjectAudit_Record",
            new
            {
                ProjectID = projectId,
                UserID = userId,
                ActivityType = activityType,
                EntityType = entityType,
                EntityID = entityId,
                EntityName = entityName,
                Description = description
            },
            commandType: CommandType.StoredProcedure);
    }
}

public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IBookRepository
{
    public async Task<IReadOnlyList<Book>> ListByProjectAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Book>("dbo.Book_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<Book>> ListByProjectsAsync(IEnumerable<int> projectIds)
    {
        var ids = projectIds.Distinct().ToArray();
        if (ids.Length == 0)
        {
            return [];
        }

        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Book>(
            "dbo.Book_ListByProjects",
            new { ProjectIDs = string.Join(',', ids) },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<Book?> GetAsync(int bookId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<Book>("dbo.Book_Get", new { BookID = bookId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<Book?> GetCoverPathsAsync(int bookId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<Book>(
            "dbo.Book_GetCoverPaths",
            new { BookID = bookId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SaveAsync(Book book)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.Book_Save",
            new
            {
                book.BookID,
                book.ProjectID,
                book.BookTitle,
                book.Subtitle,
                book.Tagline,
                book.ShortDescription,
                book.Status,
                book.BookNumber,
                book.Description,
                book.TargetWordCount,
                book.WritingStartDate,
                book.TargetCompletionDate,
                book.PublicationDate,
                book.UsesExplicitScenes
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task UpdateCoverAsync(int bookId, string? thumbnailPath, string? standardPath)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.Book_UpdateCover",
            new { BookID = bookId, CoverThumbnailPath = thumbnailPath, CoverStandardPath = standardPath },
            commandType: CommandType.StoredProcedure);
    }

    public async Task ArchiveAsync(int bookId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.Book_Archive", new { BookID = bookId }, commandType: CommandType.StoredProcedure);
    }
}

public sealed class ManuscriptDocumentRepository(ISqlConnectionFactory connectionFactory) : IManuscriptDocumentRepository
{
    public async Task<ManuscriptDocumentModel?> GetByBookAsync(int bookId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<ManuscriptDocumentModel>(
            "dbo.ManuscriptDocument_GetByBook",
            new { BookID = bookId, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<ManuscriptDocumentModel?> GetByGuidAsync(Guid documentGuid, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<ManuscriptDocumentModel>(
            "dbo.ManuscriptDocument_GetByGuid",
            new { DocumentGuid = documentGuid, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<ManuscriptDocumentModel?> SaveAsync(int bookId, Guid documentGuid, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        try
        {
            return await connection.QuerySingleOrDefaultAsync<ManuscriptDocumentModel>(
                "dbo.ManuscriptDocument_Save",
                new { BookID = bookId, DocumentGuid = documentGuid, UserID = userId },
                commandType: CommandType.StoredProcedure);
        }
        catch (SqlException ex) when (ex.Number == 51000)
        {
            throw new InvalidOperationException(ex.Message, ex);
        }
    }

    public async Task<ManuscriptDocumentUnlinkResult?> UnlinkBookAsync(int bookId, Guid? documentGuid, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<ManuscriptDocumentUnlinkResult>(
            "dbo.ManuscriptDocument_UnlinkBook",
            new { BookID = bookId, DocumentGuid = documentGuid, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task UpdateLastSyncAsync(int manuscriptDocumentId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.ManuscriptDocument_UpdateLastSync",
            new { ManuscriptDocumentID = manuscriptDocumentId, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task UpdateLastOpenedAsync(int manuscriptDocumentId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.ManuscriptDocument_UpdateLastOpened",
            new { ManuscriptDocumentID = manuscriptDocumentId, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }
}

public sealed class ChapterRepository(ISqlConnectionFactory connectionFactory) : IChapterRepository
{
    public async Task<IReadOnlyList<Chapter>> ListByBookAsync(int bookId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Chapter>("dbo.Chapter_ListByBook", new { BookID = bookId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<Chapter?> GetAsync(int chapterId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<Chapter>("dbo.Chapter_Get", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SaveAsync(Chapter chapter)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.Chapter_Save",
            new { chapter.ChapterID, chapter.BookID, chapter.ChapterNumber, chapter.ChapterTitle, chapter.Summary, chapter.ChapterPurposeID, chapter.RevisionStatusID },
            commandType: CommandType.StoredProcedure);
    }

    public async Task ArchiveAsync(int chapterId)
    {
        using var connection = connectionFactory.CreateConnection();
        try
        {
            await connection.ExecuteAsync("dbo.Chapter_Archive", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure);
        }
        catch (SqlException ex) when (ex.Number == 51000)
        {
            throw new InvalidOperationException(ex.Message, ex);
        }
    }
}

public sealed class SceneRepository(ISqlConnectionFactory connectionFactory) : ISceneRepository
{
    public async Task<IReadOnlyList<Scene>> ListByChapterAsync(int chapterId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Scene>("dbo.Scene_ListByChapter", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<Scene>> ListByFloorPlanAsync(int floorPlanId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Scene>("dbo.Scene_ListByFloorPlan", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<Scene?> GetAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.Scene_Get", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        var scene = await result.ReadSingleOrDefaultAsync<Scene>();
        if (scene is null)
        {
            return null;
        }

        scene.SelectedPurposeTypeIds = (await result.ReadAsync<int>()).ToList();
        scene.Metrics = (await result.ReadAsync<SceneMetricValue>()).ToList();
        return scene;
    }

    public async Task<int> SaveAsync(Scene scene)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.Scene_Save",
            new
            {
                scene.SceneID,
                scene.ChapterID,
                scene.SceneNumber,
                scene.SceneTitle,
                scene.Summary,
                scene.TimeModeID,
                scene.StartDateTime,
                scene.EndDateTime,
                scene.DurationAmount,
                scene.DurationUnitID,
                scene.RelativeTimeText,
                scene.TimeConfidenceID,
                scene.ScenePurposeNotes,
                scene.SceneOutcomeNotes,
                scene.RevisionStatusID,
                scene.PrimaryLocationID,
                scene.FloorPlanID,
                scene.InitialFloorPlanFloorID
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.Scene_UpdatePovCharacter",
            new { SceneID = sceneId, POVCharacterID = povCharacterId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task SaveFloorPlanLinkAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.Scene_FloorPlanLink_Save",
            new { SceneID = sceneId, FloorPlanID = floorPlanId, InitialFloorPlanFloorID = initialFloorPlanFloorId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task SavePurposesAsync(int sceneId, IEnumerable<int> purposeIds)
    {
        using var connection = connectionFactory.CreateConnection();
        var cleanPurposeIds = purposeIds
            .Where(id => id > 0)
            .Distinct()
            .ToList();

        await connection.ExecuteAsync(
            "dbo.Scene_SavePurposes",
            new { SceneID = sceneId, PurposeIds = string.Join(',', cleanPurposeIds) },
            commandType: CommandType.StoredProcedure);
    }

    public async Task SaveMetricValuesAsync(int sceneId, IEnumerable<SceneMetricValue> metricValues)
    {
        using var connection = connectionFactory.CreateConnection();
        foreach (var metric in metricValues)
        {
            await connection.ExecuteAsync(
                "dbo.SceneMetric_SaveValue",
                new { SceneID = sceneId, metric.MetricTypeID, metric.Value, metric.Notes },
                commandType: CommandType.StoredProcedure);
        }
    }

    public async Task SaveMetricValueAsync(int sceneId, int metricTypeId, int value)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.SceneMetric_SaveValue",
            new { SceneID = sceneId, MetricTypeID = metricTypeId, Value = value, Notes = (string?)null },
            commandType: CommandType.StoredProcedure);
    }

    public async Task ArchiveAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        try
        {
            await connection.ExecuteAsync("dbo.Scene_Archive", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        }
        catch (SqlException ex) when (ex.Number == 51000)
        {
            throw new InvalidOperationException(ex.Message, ex);
        }
    }

    public async Task MoveAsync(int sceneId, string direction)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.Scene_Move", new { SceneID = sceneId, Direction = direction }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<ChapterOption>> ListChapterOptionsAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ChapterOption>("dbo.ChapterOption_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<SceneMovePreview?> GetMovePreviewAsync(int sceneId, int targetChapterId, string position)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<SceneMovePreview>(
            "dbo.SceneMovePreview_Get",
            new { SceneID = sceneId, TargetChapterID = targetChapterId, Position = position },
            commandType: CommandType.StoredProcedure);
    }

    public async Task MoveToChapterAsync(int sceneId, int targetChapterId, string position, bool renumber)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.Scene_MoveToChapter",
            new { SceneID = sceneId, TargetChapterID = targetChapterId, Position = position, Renumber = renumber },
            commandType: CommandType.StoredProcedure);
    }

    public async Task MoveRelativeAsync(int sceneId, int anchorSceneId, string position)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.Scene_MoveRelative",
            new { SceneID = sceneId, AnchorSceneID = anchorSceneId, Position = position },
            commandType: CommandType.StoredProcedure);
    }

    public async Task RenumberChapterAsync(int chapterId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.Scene_RenumberChapter", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure);
    }
}

public sealed class SceneFloorPlanOccupancyRepository(ISqlConnectionFactory connectionFactory) : ISceneFloorPlanOccupancyRepository
{
    public async Task<IReadOnlyList<SceneFloorPlanOccupancy>> ListBySceneAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneFloorPlanOccupancy>(
            "dbo.SceneFloorPlanOccupancy_ListByScene",
            new { SceneID = sceneId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<SceneFloorPlanOccupancy>> ListByFloorPlanAsync(int floorPlanId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneFloorPlanOccupancy>(
            "dbo.SceneFloorPlanOccupancy_ListByFloorPlan",
            new { FloorPlanID = floorPlanId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<FloorPlanOccupancyParticipant>> ListParticipantsByFloorPlanAsync(int floorPlanId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<FloorPlanOccupancyParticipant>(
            "dbo.SceneFloorPlanParticipant_ListByFloorPlan",
            new { FloorPlanID = floorPlanId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task SaveAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId, IEnumerable<SceneFloorPlanOccupancy> occupancyRows)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.Scene_FloorPlanLink_Save",
            new { SceneID = sceneId, FloorPlanID = floorPlanId, InitialFloorPlanFloorID = initialFloorPlanFloorId },
            commandType: CommandType.StoredProcedure);

        foreach (var row in occupancyRows)
        {
            await connection.ExecuteAsync(
                "dbo.SceneFloorPlanOccupancy_Save",
                new
                {
                    SceneID = sceneId,
                    FloorPlanID = floorPlanId,
                    row.CharacterID,
                    row.StoryAssetID,
                    LocationID = row.LocationID == 0 ? (int?)null : row.LocationID
                },
                commandType: CommandType.StoredProcedure);
        }
    }
}

public sealed class LookupRepository(ISqlConnectionFactory connectionFactory) : ILookupRepository
{
    public async Task<LookupData> GetAllAsync()
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.Lookup_All", commandType: CommandType.StoredProcedure);
        return new LookupData
        {
            RevisionStatuses = (await result.ReadAsync<RevisionStatus>()).ToList(),
            TimeModes = (await result.ReadAsync<TimeMode>()).ToList(),
            DurationUnits = (await result.ReadAsync<DurationUnit>()).ToList(),
            TimeConfidences = (await result.ReadAsync<TimeConfidence>()).ToList(),
            ScenePurposeTypes = (await result.ReadAsync<ScenePurposeType>()).ToList(),
            ChapterPurposeTypes = (await result.ReadAsync<ChapterPurposeType>()).ToList()
        };
    }
}

public sealed class TimelineRepository(ISqlConnectionFactory connectionFactory) : ITimelineRepository
{
    public async Task<TimelineData> GetByProjectAsync(int projectId, int? bookId, bool includeMetrics, IEnumerable<int> metricTypeIds, bool includePlotLines)
    {
        using var connection = connectionFactory.CreateConnection();
        var selectedMetricIds = string.Join(',', metricTypeIds.Where(id => id > 0).Distinct());
        using var result = await connection.QueryMultipleAsync(
            "dbo.Timeline_GetByProject",
            new
            {
                ProjectID = projectId,
                BookID = bookId,
                IncludeMetrics = includeMetrics,
                SelectedMetricTypeIDs = selectedMetricIds,
                IncludePlotLines = includePlotLines
            },
            commandType: CommandType.StoredProcedure);

        var project = await result.ReadSingleOrDefaultAsync<Project>();
        var books = (await result.ReadAsync<Book>()).ToList();
        var chapters = (await result.ReadAsync<Chapter>()).ToList();
        var scenes = (await result.ReadAsync<Scene>()).ToList();
        var purposeLabels = (await result.ReadAsync<ScenePurposeLabel>()).ToList();
        var metricTypes = (await result.ReadAsync<SceneMetricType>()).ToList();
        var metricPoints = (await result.ReadAsync<TimelineMetricPoint>()).ToList();
        var plotLines = (await result.ReadAsync<PlotLineItem>()).ToList();
        var threadEvents = (await result.ReadAsync<ThreadEvent>()).ToList();
        await PopulateThreadEventTargetsAsync(connection, threadEvents);

        return new TimelineData
        {
            Project = project,
            Books = books,
            Chapters = chapters,
            Scenes = scenes,
            PurposeLabels = purposeLabels,
            MetricTypes = metricTypes,
            MetricPoints = metricPoints,
            PlotLines = plotLines,
            ThreadEvents = threadEvents
        };
    }

    private static async Task PopulateThreadEventTargetsAsync(IDbConnection connection, IReadOnlyList<ThreadEvent> threadEvents)
    {
        var threadEventIds = threadEvents.Select(x => x.ThreadEventID).ToList();
        if (threadEventIds.Count == 0)
        {
            return;
        }

        var targetRows = await connection.QueryAsync<ThreadEventTargetRow>(
            "dbo.ThreadEventTarget_ListByEvents",
            new { ThreadEventIDs = string.Join(',', threadEventIds) },
            commandType: CommandType.StoredProcedure);
        var targetsByEvent = targetRows
            .GroupBy(x => x.PlotEventID)
            .ToDictionary(x => x.Key, x => (IReadOnlyList<int>)x.Select(row => row.PlotLineID).ToList());

        foreach (var threadEvent in threadEvents)
        {
            if (targetsByEvent.TryGetValue(threadEvent.ThreadEventID, out var targets))
            {
                threadEvent.TargetPlotLineIDs = targets;
            }
        }
    }

    private sealed class ThreadEventTargetRow
    {
        public int PlotEventID { get; set; }
        public int PlotLineID { get; set; }
    }

    public async Task<IReadOnlyList<TimelineSceneOverviewRow>> GetSceneOverviewsAsync(int projectId, int? bookId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<TimelineSceneOverviewRow>(
            "dbo.Timeline_SceneOverviews",
            new { ProjectID = projectId, BookID = bookId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }
}

public sealed class TimelineSettingsRepository(ISqlConnectionFactory connectionFactory) : ITimelineSettingsRepository
{
    public async Task<ProjectTimelineSettings?> GetAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<ProjectTimelineSettings>(
            "dbo.ProjectTimelineSettings_Get",
            new { ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<SceneMetricType>> ListMetricTypesAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneMetricType>(
            "dbo.ProjectTimelineSettings_MetricTypes",
            new { ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<ProjectTimelineMetricSetting>> ListMetricSettingsAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<ProjectTimelineMetricSetting>(
            "dbo.ProjectTimelineMetricSettings_List",
            new { ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task SaveAsync(ProjectTimelineSettings settings, IEnumerable<int> metricTypeIds)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.ProjectTimelineSettings_Save",
            new
            {
                settings.ProjectID,
                settings.ShowSceneCards,
                settings.ShowMetricShape,
                settings.ShowPlotLines,
                settings.ShowStoryAssets,
                settings.ShowCharacterAppearances,
                settings.ShowWarnings,
                MetricTypeIDs = string.Join(',', metricTypeIds.Where(id => id > 0).Distinct())
            },
            commandType: CommandType.StoredProcedure);
    }
}

public sealed class SceneMetricTypeRepository(ISqlConnectionFactory connectionFactory) : ISceneMetricTypeRepository
{
    public async Task<IReadOnlyList<SceneMetricType>> ListForManagementAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneMetricType>(
            "dbo.SceneMetricType_ListForProjectManagement",
            new { ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<SceneMetricType?> GetAsync(int metricTypeId, int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<SceneMetricType>(
            "dbo.SceneMetricType_GetForProject",
            new { MetricTypeID = metricTypeId, ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SaveAsync(SceneMetricType metric)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.SceneMetricType_SaveForProject",
            new
            {
                metric.MetricTypeID,
                metric.ProjectID,
                metric.MetricName,
                metric.Description,
                metric.MinValue,
                metric.MaxValue,
                metric.DefaultValue,
                metric.SortOrder,
                metric.IsActive
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<bool> SetActiveAsync(int metricTypeId, int projectId, bool isActive)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<bool>(
            "dbo.SceneMetricType_SetActiveForProject",
            new { MetricTypeID = metricTypeId, ProjectID = projectId, IsActive = isActive },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<bool> MoveAsync(int metricTypeId, int projectId, string direction)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<bool>(
            "dbo.SceneMetricType_MoveForProject",
            new { MetricTypeID = metricTypeId, ProjectID = projectId, Direction = direction },
            commandType: CommandType.StoredProcedure);
    }

    public async Task AddMissingDefaultsAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.SceneMetricType_AddMissingDefaultsForProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<bool>(
            "dbo.SceneMetricType_RemoveUnusedForProject",
            new { MetricTypeID = metricTypeId, ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<bool> HasProjectMetricsAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<bool>("dbo.SceneMetricType_ProjectHasMetrics", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
    }
}

public sealed class TimelinePresetRepository(ISqlConnectionFactory connectionFactory) : ITimelinePresetRepository
{
    public async Task<IReadOnlyList<TimelineViewPreset>> ListAsync(int projectId, int? bookId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<TimelineViewPreset>(
            "dbo.TimelineViewPreset_List",
            new { ProjectID = projectId, BookID = bookId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<TimelineViewPreset?> GetAsync(int timelineViewPresetId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<TimelineViewPreset>(
            "dbo.TimelineViewPreset_Get",
            new { TimelineViewPresetID = timelineViewPresetId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SaveAsync(TimelineViewPreset preset)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.TimelineViewPreset_Save",
            new
            {
                preset.TimelineViewPresetID,
                preset.ProjectID,
                preset.BookID,
                preset.PresetName,
                preset.FilterJson,
                preset.VisibilityJson,
                preset.FocusType,
                preset.FocusID
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task ArchiveAsync(int timelineViewPresetId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.TimelineViewPreset_Archive",
            new { TimelineViewPresetID = timelineViewPresetId },
            commandType: CommandType.StoredProcedure);
    }
}

public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IPlotRepository
{
    public async Task<PlotLookupData> GetLookupsAsync()
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.PlotLookup_All", commandType: CommandType.StoredProcedure);
        return new PlotLookupData
        {
            PlotLineTypes = (await result.ReadAsync<PlotLineType>()).ToList(),
            PlotImportance = (await result.ReadAsync<PlotImportance>()).ToList(),
            ThreadTypes = (await result.ReadAsync<ThreadType>()).ToList(),
            ThreadStatuses = (await result.ReadAsync<ThreadStatus>()).ToList(),
            ThreadEventTypes = (await result.ReadAsync<ThreadEventType>()).ToList(),
            PlotEventTypes = (await result.ReadAsync<PlotEventType>()).ToList()
        };
    }

    public async Task<IReadOnlyList<PlotLineItem>> ListPlotLinesAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<PlotLineItem>("dbo.PlotLine_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<PlotLineItem?> GetPlotLineAsync(int plotLineId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<PlotLineItem>("dbo.PlotLine_Get", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SavePlotLineAsync(PlotLineItem plotLine)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.PlotLine_Save",
            new
            {
                plotLine.PlotLineID,
                plotLine.ProjectID,
                plotLine.BookID,
                plotLine.PlotLineName,
                plotLine.PlotLineTypeID,
                plotLine.PlotImportanceID,
                plotLine.Description,
                plotLine.ParentPlotLineID,
                plotLine.EmergesFromPlotLineID,
                plotLine.SortOrder,
                plotLine.Colour,
                plotLine.IsVisibleOnTimeline
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task ArchivePlotLineAsync(int plotLineId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.PlotLine_Archive", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<PlotThread>> ListPlotThreadsByProjectAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<PlotThread>("dbo.PlotThread_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<PlotThread>> ListPlotThreadsByPlotLineAsync(int plotLineId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<PlotThread>("dbo.PlotThread_ListByPlotLine", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<PlotThread?> GetPlotThreadAsync(int plotThreadId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<PlotThread>("dbo.PlotThread_Get", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SavePlotThreadAsync(PlotThread plotThread)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.PlotThread_Save",
            new
            {
                plotThread.PlotThreadID,
                plotThread.PlotLineID,
                plotThread.ThreadTitle,
                plotThread.ThreadTypeID,
                plotThread.ThreadStatusID,
                plotThread.Importance,
                plotThread.Summary,
                plotThread.IntroducedSceneID,
                plotThread.PlannedResolutionSceneID,
                plotThread.ActualResolutionSceneID
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task ArchivePlotThreadAsync(int plotThreadId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.PlotThread_Archive", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<ThreadEvent>> ListThreadEventsBySceneAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = (await connection.QueryAsync<ThreadEvent>("dbo.ThreadEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure)).ToList();
        await PopulateThreadEventTargetsAsync(connection, rows);
        return rows;
    }

    public async Task<IReadOnlyList<ThreadEvent>> ListThreadEventsByPlotThreadAsync(int plotThreadId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = (await connection.QueryAsync<ThreadEvent>("dbo.ThreadEvent_ListByPlotThread", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure)).ToList();
        await PopulateThreadEventTargetsAsync(connection, rows);
        return rows;
    }

    private static async Task PopulateThreadEventTargetsAsync(IDbConnection connection, IReadOnlyList<ThreadEvent> threadEvents)
    {
        var threadEventIds = threadEvents.Select(x => x.ThreadEventID).ToList();
        if (threadEventIds.Count == 0)
        {
            return;
        }

        var targetRows = await connection.QueryAsync<ThreadEventTargetRow>(
            "dbo.ThreadEventTarget_ListByEvents",
            new { ThreadEventIDs = string.Join(',', threadEventIds) },
            commandType: CommandType.StoredProcedure);
        var targetsByEvent = targetRows
            .GroupBy(x => x.PlotEventID)
            .ToDictionary(x => x.Key, x => (IReadOnlyList<int>)x.Select(row => row.PlotLineID).ToList());

        foreach (var threadEvent in threadEvents)
        {
            if (targetsByEvent.TryGetValue(threadEvent.ThreadEventID, out var targets))
            {
                threadEvent.TargetPlotLineIDs = targets;
            }
        }
    }

    private sealed class ThreadEventTargetRow
    {
        public int PlotEventID { get; set; }
        public int PlotLineID { get; set; }
    }

    public async Task<int> SaveThreadEventAsync(ThreadEvent threadEvent)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.ThreadEvent_Save",
            new
            {
                threadEvent.ThreadEventID,
                threadEvent.PlotThreadID,
                threadEvent.SceneID,
                threadEvent.EventTypeID,
                threadEvent.PlotEventTypeID,
                threadEvent.TargetPlotLineID,
                threadEvent.EventTitle,
                threadEvent.EventDescription
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task SaveThreadEventTargetsAsync(int threadEventId, IEnumerable<int> plotLineIds)
    {
        using var connection = connectionFactory.CreateConnection();
        var targetIds = plotLineIds.Where(id => id > 0).Distinct().ToList();
        await connection.ExecuteAsync(
            "dbo.ThreadEventTarget_SaveForEvent",
            new { ThreadEventID = threadEventId, PlotLineIDs = string.Join(',', targetIds) },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteThreadEventAsync(int threadEventId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.ThreadEvent_Delete", new { ThreadEventID = threadEventId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<SceneOption>> ListSceneOptionsAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneOption>("dbo.SceneOption_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }
}

public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : IAssetRepository
{
    public async Task<AssetLookupData> GetLookupsAsync(int? projectId = null)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.AssetLookup_All", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return new AssetLookupData
        {
            AssetKinds = (await result.ReadAsync<AssetKind>()).ToList(),
            AssetStates = (await result.ReadAsync<AssetState>()).ToList(),
            AssetEventTypes = (await result.ReadAsync<AssetEventType>()).ToList(),
            AssetDependencyTypes = (await result.ReadAsync<AssetDependencyType>()).ToList(),
            AssetCustodyEventTypes = (await result.ReadAsync<AssetCustodyEventType>()).ToList(),
            CustodyRoles = (await result.ReadAsync<CustodyRole>()).ToList()
        };
    }

    public async Task<IReadOnlyList<StoryAsset>> ListAssetsAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<StoryAsset>("dbo.StoryAsset_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<StoryAsset?> GetAssetAsync(int storyAssetId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<StoryAsset>("dbo.StoryAsset_Get", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SaveAssetAsync(StoryAsset asset)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.StoryAsset_Save",
            new
            {
                asset.StoryAssetID,
                asset.ProjectID,
                asset.AssetName,
                asset.AssetKindID,
                asset.Description,
                asset.Importance,
                asset.CurrentStateID,
                asset.CurrentLocationID,
                asset.IsResolved,
                asset.ShowInQuickAddBar,
                asset.ImagePath,
                asset.ThumbnailPath
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<AssetAlias>> ListAliasesAsync(int storyAssetId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<AssetAlias>(
            "dbo.AssetAlias_ListByAsset",
            new { StoryAssetID = storyAssetId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> AddAliasAsync(int storyAssetId, string alias, int? sortOrder)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.AssetAlias_Add",
            new { StoryAssetID = storyAssetId, Alias = alias, SortOrder = sortOrder },
            commandType: CommandType.StoredProcedure);
    }

    public async Task UpdateAliasAsync(int assetAliasId, string alias, int? sortOrder)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.AssetAlias_Update",
            new { AssetAliasID = assetAliasId, Alias = alias, SortOrder = sortOrder },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteAliasAsync(int assetAliasId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.AssetAlias_Delete",
            new { AssetAliasID = assetAliasId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task UpdateAssetImageAsync(int storyAssetId, string? imagePath, string? thumbnailPath)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.StoryAsset_UpdateImage",
            new { StoryAssetID = storyAssetId, ImagePath = imagePath, ThumbnailPath = thumbnailPath },
            commandType: CommandType.StoredProcedure);
    }

    public async Task ArchiveAssetAsync(int storyAssetId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.StoryAsset_Archive", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<AssetEvent>> ListEventsBySceneAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<AssetEvent>("dbo.AssetEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<AssetEvent>> ListEventsByAssetAsync(int storyAssetId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<AssetEvent>("dbo.AssetEvent_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> SaveEventAsync(AssetEvent assetEvent)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.AssetEvent_Save",
            new
            {
                assetEvent.AssetEventID,
                assetEvent.StoryAssetID,
                assetEvent.SceneID,
                assetEvent.AssetEventTypeID,
                assetEvent.FromStateID,
                assetEvent.ToStateID,
                assetEvent.EventTitle,
                assetEvent.EventDescription
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteEventAsync(int assetEventId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.AssetEvent_Delete", new { AssetEventID = assetEventId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<SceneAssetSuggestion>> ListSceneAssetSuggestionsAsync(int sceneId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneAssetSuggestion>(
            "dbo.SceneAssetSuggestions_ListByScene",
            new { SceneID = sceneId, UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<SceneAssetSuggestionReviewResult?> AcceptSceneAssetSuggestionAsync(int suggestionId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<SceneAssetSuggestionReviewResult>(
            "dbo.SceneAssetSuggestion_Accept",
            new { SceneAssetSuggestionID = suggestionId, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<SceneAssetSuggestionReviewResult?> RejectSceneAssetSuggestionAsync(int suggestionId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<SceneAssetSuggestionReviewResult>(
            "dbo.SceneAssetSuggestion_Reject",
            new { SceneAssetSuggestionID = suggestionId, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<SceneAssetSuggestionCount>> CountSceneAssetSuggestionsByChapterAsync(int chapterId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneAssetSuggestionCount>(
            "dbo.SceneAssetSuggestions_CountByChapter",
            new { ChapterID = chapterId, UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<AssetDependency>> ListDependenciesByAssetAsync(int storyAssetId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<AssetDependency>("dbo.AssetDependency_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> SaveDependencyAsync(AssetDependency dependency)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.AssetDependency_Save",
            new
            {
                dependency.AssetDependencyID,
                dependency.SourceAssetID,
                dependency.TargetAssetID,
                dependency.AssetDependencyTypeID,
                dependency.SourceRequiredStateID,
                dependency.TargetBlockedStateID,
                dependency.Description
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteDependencyAsync(int assetDependencyId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.AssetDependency_Delete", new { AssetDependencyID = assetDependencyId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<AssetCustodyEvent>> ListCustodyBySceneAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<AssetCustodyEvent>("dbo.AssetCustodyEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return await WithValidCustodianSummariesAsync(connection, rows.ToList());
    }

    public async Task<IReadOnlyList<AssetCustodyEvent>> ListCustodyByAssetAsync(int storyAssetId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<AssetCustodyEvent>("dbo.AssetCustodyEvent_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure);
        return await WithValidCustodianSummariesAsync(connection, rows.ToList());
    }

    public async Task<IReadOnlyList<AssetCustodyCharacter>> ListCustodyCharactersByProjectAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<AssetCustodyCharacter>(
            "dbo.AssetCustodyCharacter_ListByProject",
            new { ProjectID = projectId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    private static async Task<IReadOnlyList<AssetCustodyEvent>> WithValidCustodianSummariesAsync(IDbConnection connection, List<AssetCustodyEvent> rows)
    {
        if (rows.Count == 0)
        {
            return rows;
        }

        var eventIds = rows.Select(x => x.AssetCustodyEventID).Distinct().ToList();
        var validCustodians = await connection.QueryAsync<AssetCustodySummaryRow>(
            "dbo.AssetCustodySummary_ListByEvents",
            new { AssetCustodyEventIDs = string.Join(',', eventIds) },
            commandType: CommandType.StoredProcedure);

        var summaries = validCustodians
            .GroupBy(x => x.AssetCustodyEventID)
            .ToDictionary(
                x => x.Key,
                x => string.Join(", ", x
                    .Select(row => string.IsNullOrWhiteSpace(row.RoleName)
                        ? row.CharacterName
                        : $"{row.CharacterName} ({row.RoleName})")
                    .Where(label => !string.IsNullOrWhiteSpace(label))
                    .Distinct(StringComparer.OrdinalIgnoreCase)));

        foreach (var row in rows)
        {
            row.CustodianSummary = summaries.GetValueOrDefault(row.AssetCustodyEventID);
        }

        return rows.Where(x => !string.IsNullOrWhiteSpace(x.CustodianSummary)).ToList();
    }

    private sealed class AssetCustodySummaryRow
    {
        public int AssetCustodyEventID { get; set; }
        public string CharacterName { get; set; } = string.Empty;
        public string? RoleName { get; set; }
    }

    public async Task<int> SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable<int> custodianCharacterIds, int custodyRoleId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.AssetCustodyEvent_Save",
            new
            {
                custodyEvent.AssetCustodyEventID,
                custodyEvent.StoryAssetID,
                custodyEvent.SceneID,
                custodyEvent.AssetCustodyEventTypeID,
                custodyEvent.Description,
                CustodianNames = custodianNames,
                CustodianCharacterIds = string.Join(',', custodianCharacterIds.Where(id => id > 0).Distinct()),
                CustodyRoleID = custodyRoleId
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteCustodyEventAsync(int assetCustodyEventId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.AssetCustodyEvent_Delete", new { AssetCustodyEventID = assetCustodyEventId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<(IReadOnlyList<StoryAsset> Assets, IReadOnlyList<AssetEvent> Events)> GetTimelineAsync(int projectId, int? bookId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.AssetTimeline_GetByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure);
        var assets = (await result.ReadAsync<StoryAsset>()).ToList();
        var events = (await result.ReadAsync<AssetEvent>()).ToList();
        return (assets, events);
    }

    public async Task<IReadOnlyList<AssetDependencyIssue>> CheckDependencyIssuesAsync(int storyAssetId, int sceneId, int? toStateId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<AssetDependencyIssue>(
            "dbo.AssetDependency_CheckForEvent",
            new { StoryAssetID = storyAssetId, SceneID = sceneId, ToStateID = toStateId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }
}

public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory) : ICharacterRepository
{
    public async Task<CharacterLookupData> GetLookupsAsync(int? projectId = null)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.CharacterLookup_All", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return new CharacterLookupData
        {
            RoleTypes = (await result.ReadAsync<CharacterRoleInSceneType>()).ToList(),
            PresenceTypes = (await result.ReadAsync<PresenceType>()).ToList(),
            AttributeTypes = (await result.ReadAsync<CharacterAttributeType>()).ToList(),
            KnowledgeStates = (await result.ReadAsync<KnowledgeState>()).ToList(),
            RelationshipTypes = (await result.ReadAsync<RelationshipType>()).ToList(),
            RelationshipStates = (await result.ReadAsync<RelationshipState>()).ToList(),
            RelationshipCategories = (await result.ReadAsync<RelationshipCategory>()).ToList()
        };
    }

    public async Task<IReadOnlyList<Character>> ListCharactersAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<Character>("dbo.Character_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<Character?> GetCharacterAsync(int characterId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<Character>("dbo.Character_Get", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SaveCharacterAsync(Character character)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.Character_Save",
            new
            {
                character.CharacterID,
                character.ProjectID,
                character.CharacterName,
                character.ShortName,
                character.SexValueID,
                character.Sex,
                character.BirthDate,
                character.AgeAtSeriesStart,
                character.AgeReferenceSceneID,
                character.Height,
                character.EyeColour,
                character.CharacterImportance,
                character.ShowInQuickAddBar,
                character.DefaultDescription,
                character.ImagePath,
                character.ThumbnailPath
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<CharacterAlias>> ListAliasesAsync(int characterId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<CharacterAlias>(
            "dbo.CharacterAlias_ListByCharacter",
            new { CharacterID = characterId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> AddAliasAsync(int characterId, string alias, int? sortOrder)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.CharacterAlias_Add",
            new { CharacterID = characterId, Alias = alias, SortOrder = sortOrder },
            commandType: CommandType.StoredProcedure);
    }

    public async Task UpdateAliasAsync(int characterAliasId, string alias, int? sortOrder)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.CharacterAlias_Update",
            new { CharacterAliasID = characterAliasId, Alias = alias, SortOrder = sortOrder },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteAliasAsync(int characterAliasId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.CharacterAlias_Delete",
            new { CharacterAliasID = characterAliasId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<CharacterSexValue>> ListSexValuesAsync(int ownerUserId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<CharacterSexValue>(
            "dbo.CharacterSexValue_ListForOwner",
            new { OwnerUserID = ownerUserId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<CharacterSexValue?> GetSexValueAsync(int characterSexValueId, int ownerUserId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<CharacterSexValue>(
            "dbo.CharacterSexValue_Get",
            new { CharacterSexValueID = characterSexValueId, OwnerUserID = ownerUserId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<int> GetOrCreateSexValueAsync(int ownerUserId, string sexName)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.CharacterSexValue_GetOrCreate",
            new { OwnerUserID = ownerUserId, SexName = sexName },
            commandType: CommandType.StoredProcedure);
    }

    public async Task UpdateCharacterImageAsync(int characterId, string? imagePath, string? thumbnailPath)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.Character_UpdateImage",
            new { CharacterID = characterId, ImagePath = imagePath, ThumbnailPath = thumbnailPath },
            commandType: CommandType.StoredProcedure);
    }

    public async Task UpdateCharacterAvatarAsync(int characterId, int? sourceCharacterImageId, string? avatarImagePath, string? avatarThumbnailPath)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.Character_UpdateAvatar",
            new
            {
                CharacterID = characterId,
                AvatarSourceCharacterImageID = sourceCharacterImageId,
                AvatarImagePath = avatarImagePath,
                AvatarThumbnailPath = avatarThumbnailPath
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<CharacterImage>> ListCharacterImagesAsync(int characterId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<CharacterImage>(
            "dbo.CharacterImage_ListByCharacter",
            new { CharacterID = characterId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<CharacterImage?> GetCharacterImageAsync(int characterImageId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<CharacterImage>(
            "dbo.CharacterImage_Get",
            new { CharacterImageID = characterImageId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<int> CreateCharacterImageAsync(CharacterImage image)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.CharacterImage_Create",
            new
            {
                image.CharacterID,
                image.ProjectID,
                image.ImagePath,
                image.ThumbnailPath,
                image.Caption,
                InputSortOrder = image.SortOrder == 0 ? (int?)null : image.SortOrder
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteCharacterImageAsync(int characterImageId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.CharacterImage_Delete",
            new { CharacterImageID = characterImageId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task ArchiveCharacterAsync(int characterId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.Character_Archive", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<SceneCharacter>> ListSceneCharactersAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneCharacter>("dbo.SceneCharacter_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> SaveSceneCharacterAsync(SceneCharacter sceneCharacter)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.SceneCharacter_Save",
            new
            {
                sceneCharacter.SceneCharacterID,
                sceneCharacter.SceneID,
                sceneCharacter.CharacterID,
                sceneCharacter.RoleInSceneTypeID,
                sceneCharacter.PresenceTypeID,
                sceneCharacter.LocationID,
                sceneCharacter.EntryLocationID,
                sceneCharacter.ExitLocationID,
                sceneCharacter.AppearanceNotes,
                sceneCharacter.OutfitDescription,
                sceneCharacter.PhysicalCondition,
                sceneCharacter.EmotionalState,
                sceneCharacter.KnowledgeNotes
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteSceneCharacterAsync(int sceneCharacterId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.SceneCharacter_Delete", new { SceneCharacterID = sceneCharacterId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<SceneCharacterSuggestion>> ListSceneCharacterSuggestionsAsync(int sceneId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneCharacterSuggestion>(
            "dbo.SceneCharacterSuggestions_ListByScene",
            new { SceneID = sceneId, UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<SceneCharacterSuggestionReviewResult?> AcceptSceneCharacterSuggestionAsync(int suggestionId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<SceneCharacterSuggestionReviewResult>(
            "dbo.SceneCharacterSuggestion_Accept",
            new { SceneCharacterSuggestionID = suggestionId, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<SceneCharacterSuggestionReviewResult?> RejectSceneCharacterSuggestionAsync(int suggestionId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<SceneCharacterSuggestionReviewResult>(
            "dbo.SceneCharacterSuggestion_Reject",
            new { SceneCharacterSuggestionID = suggestionId, UserID = userId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<SceneCharacterSuggestionCount>> CountSceneCharacterSuggestionsByChapterAsync(int chapterId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<SceneCharacterSuggestionCount>(
            "dbo.SceneCharacterSuggestions_CountByChapter",
            new { ChapterID = chapterId, UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<CharacterAttributeEvent>> ListAttributeEventsByCharacterAsync(int characterId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<CharacterAttributeEvent>("dbo.CharacterAttributeEvent_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<CharacterAttributeEvent>> ListAttributeEventsBySceneAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<CharacterAttributeEvent>("dbo.CharacterAttributeEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> SaveAttributeEventAsync(CharacterAttributeEvent attributeEvent)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.CharacterAttributeEvent_Save",
            new
            {
                attributeEvent.CharacterAttributeEventID,
                attributeEvent.CharacterID,
                attributeEvent.CharacterAttributeTypeID,
                attributeEvent.SceneID,
                attributeEvent.AttributeValue,
                attributeEvent.Description
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<CharacterKnowledgeItem>> ListKnowledgeBySceneAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<CharacterKnowledgeItem>("dbo.CharacterKnowledge_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<CharacterKnowledgeItem>> ListKnowledgeByCharacterAsync(int characterId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<CharacterKnowledgeItem>("dbo.CharacterKnowledge_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> SaveKnowledgeAsync(CharacterKnowledgeItem knowledge)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.CharacterKnowledge_Save",
            new
            {
                knowledge.CharacterKnowledgeID,
                knowledge.CharacterID,
                knowledge.StoryAssetID,
                knowledge.PlotThreadID,
                knowledge.SceneID,
                knowledge.KnowledgeStateID,
                knowledge.SourceEventID,
                knowledge.Description
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteKnowledgeAsync(int characterKnowledgeId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.CharacterKnowledge_Delete", new { CharacterKnowledgeID = characterKnowledgeId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<CharacterRelationship>> ListRelationshipsByProjectAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<CharacterRelationship>("dbo.Relationship_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<CharacterRelationship>> ListRelationshipsByCharacterAsync(int characterId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<CharacterRelationship>("dbo.Relationship_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<CharacterRelationship?> GetRelationshipAsync(int characterRelationshipId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<CharacterRelationship>(
            "dbo.Relationship_Get",
            new { CharacterRelationshipID = characterRelationshipId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SaveRelationshipAsync(CharacterRelationship relationship)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.Relationship_Save",
            new
            {
                relationship.CharacterRelationshipID,
                relationship.ProjectID,
                relationship.CharacterAID,
                relationship.CharacterBID,
                relationship.RelationshipTypeID,
                relationship.IsPermanent,
                relationship.StartSceneID,
                relationship.EndSceneID,
                relationship.IsKnownToReader,
                relationship.Notes,
                relationship.IsInitialRelationship,
                relationship.InitialBookID,
                relationship.ReaderInitiallyKnows,
                relationship.InitialRelationshipStateID,
                relationship.InitialIntensity,
                relationship.IsReciprocal
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<CharacterRelationship>> ListInitialRelationshipsByCharacterAsync(int characterId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<CharacterRelationship>("dbo.Relationship_ListInitialByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task ArchiveRelationshipAsync(int characterRelationshipId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.Relationship_Archive", new { CharacterRelationshipID = characterRelationshipId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsBySceneAsync(int sceneId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<RelationshipEvent>("dbo.RelationshipEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsByCharacterAsync(int characterId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<RelationshipEvent>("dbo.RelationshipEvent_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<int> SaveRelationshipEventAsync(RelationshipEvent relationshipEvent)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.RelationshipEvent_Save",
            new
            {
                relationshipEvent.RelationshipEventID,
                relationshipEvent.CharacterRelationshipID,
                relationshipEvent.SceneID,
                relationshipEvent.RelationshipStateID,
                relationshipEvent.Intensity,
                relationshipEvent.IsKnownToOtherCharacter,
                relationshipEvent.Description
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task DeleteRelationshipEventAsync(int relationshipEventId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync("dbo.RelationshipEvent_Delete", new { RelationshipEventID = relationshipEventId }, commandType: CommandType.StoredProcedure);
    }

    public async Task<(IReadOnlyList<Character> Characters, IReadOnlyList<SceneCharacter> Appearances)> GetTimelineAsync(int projectId, int? bookId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.CharacterTimeline_GetByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure);
        var characters = (await result.ReadAsync<Character>()).ToList();
        var appearances = (await result.ReadAsync<SceneCharacter>()).ToList();
        return (characters, appearances);
    }

    public async Task<RelationshipMapData> GetRelationshipMapAsync(int projectId)
    {
        using var connection = connectionFactory.CreateConnection();
        using var result = await connection.QueryMultipleAsync("dbo.RelationshipMap_GetByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
        return new RelationshipMapData
        {
            Characters = (await result.ReadAsync<Character>()).ToList(),
            Books = (await result.ReadAsync<Book>()).ToList(),
            Chapters = (await result.ReadAsync<Chapter>()).ToList(),
            Scenes = (await result.ReadAsync<Scene>()).ToList(),
            Relationships = (await result.ReadAsync<CharacterRelationship>()).ToList(),
            RelationshipEvents = (await result.ReadAsync<RelationshipEvent>()).ToList(),
            RelationshipCategories = (await result.ReadAsync<RelationshipCategory>()).ToList()
        };
    }
}
