using System.Data; using Dapper; using PlotLine.Models; namespace PlotLine.Data; public interface IIllustrationLibraryRepository { Task> ListAsync(IllustrationLibraryFilter filter); Task GetSummaryAsync(); Task GetAsync(int illustrationLibraryItemId); Task UpsertPlannedAsync(IllustrationGenerationSpecification specification, IllustrationPromptBuildResult prompt); Task QueueAsync(int? illustrationLibraryItemId, string? category); Task ClaimNextAsync(); Task MarkGeneratedAsync(int illustrationLibraryItemId, IllustrationStoredImage image, IllustrationImageGenerationResult providerResult, string prompt, string templateVersion); Task MarkFailedAsync(int illustrationLibraryItemId, string errorMessage); Task ApproveAsync(int illustrationLibraryItemId, int userId); Task RejectAsync(int illustrationLibraryItemId, int userId, string? reason); Task CreateRegenerationAsync(int illustrationLibraryItemId, string? adminNotes); Task DisableAsync(int illustrationLibraryItemId); Task UpdateMetadataAsync(int illustrationLibraryItemId, string displayTitle, string? shortDescription, string? metadataJson, string? adminNotes); Task> GetApprovedAsync(); Task SupersedeRecoveredFailuresAsync(); } public sealed class IllustrationLibraryRepository(ISqlConnectionFactory connectionFactory) : IIllustrationLibraryRepository { public async Task> ListAsync(IllustrationLibraryFilter filter) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.IllustrationLibraryItem_List", new { Category = string.IsNullOrWhiteSpace(filter.Category) ? null : filter.Category, Status = string.IsNullOrWhiteSpace(filter.Status) ? null : filter.Status, Search = string.IsNullOrWhiteSpace(filter.Search) ? null : filter.Search }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetSummaryAsync() { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.IllustrationLibraryItem_Summary", commandType: CommandType.StoredProcedure); } public async Task GetAsync(int illustrationLibraryItemId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.IllustrationLibraryItem_Get", new { IllustrationLibraryItemID = illustrationLibraryItemId }, commandType: CommandType.StoredProcedure); } public async Task UpsertPlannedAsync(IllustrationGenerationSpecification specification, IllustrationPromptBuildResult prompt) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.IllustrationLibraryItem_UpsertPlanned", new { Category = specification.Category, StableCode = specification.Code, DisplayTitle = specification.Title, ShortDescription = specification.Description, MetadataJson = prompt.MetadataJson, SpecificationJson = prompt.SpecificationJson }, commandType: CommandType.StoredProcedure); } public async Task QueueAsync(int? illustrationLibraryItemId, string? category) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.IllustrationLibraryItem_Queue", new { IllustrationLibraryItemID = illustrationLibraryItemId, Category = category }, commandType: CommandType.StoredProcedure); } public async Task ClaimNextAsync() { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.IllustrationLibraryItem_ClaimNext", commandType: CommandType.StoredProcedure); } public async Task MarkGeneratedAsync(int illustrationLibraryItemId, IllustrationStoredImage image, IllustrationImageGenerationResult providerResult, string prompt, string templateVersion) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.IllustrationLibraryItem_MarkGenerated", new { IllustrationLibraryItemID = illustrationLibraryItemId, image.FilePath, image.ThumbnailPath, image.Width, image.Height, image.FileSizeBytes, image.MimeType, Provider = providerResult.Provider, Model = providerResult.Model, PromptText = prompt, PromptTemplateVersion = templateVersion, ProviderRequestID = providerResult.ProviderRequestID }, commandType: CommandType.StoredProcedure); } public async Task MarkFailedAsync(int illustrationLibraryItemId, string errorMessage) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.IllustrationLibraryItem_MarkFailed", new { IllustrationLibraryItemID = illustrationLibraryItemId, LastError = errorMessage }, commandType: CommandType.StoredProcedure); } public async Task ApproveAsync(int illustrationLibraryItemId, int userId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.IllustrationLibraryItem_Approve", new { IllustrationLibraryItemID = illustrationLibraryItemId, ApprovedByUserID = userId }, commandType: CommandType.StoredProcedure); } public async Task RejectAsync(int illustrationLibraryItemId, int userId, string? reason) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.IllustrationLibraryItem_Reject", new { IllustrationLibraryItemID = illustrationLibraryItemId, RejectedByUserID = userId, RejectionReason = reason }, commandType: CommandType.StoredProcedure); } public async Task CreateRegenerationAsync(int illustrationLibraryItemId, string? adminNotes) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.IllustrationLibraryItem_CreateRegeneration", new { IllustrationLibraryItemID = illustrationLibraryItemId, AdminNotes = adminNotes }, commandType: CommandType.StoredProcedure); } public async Task DisableAsync(int illustrationLibraryItemId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.IllustrationLibraryItem_Disable", new { IllustrationLibraryItemID = illustrationLibraryItemId }, commandType: CommandType.StoredProcedure); } public async Task UpdateMetadataAsync(int illustrationLibraryItemId, string displayTitle, string? shortDescription, string? metadataJson, string? adminNotes) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.IllustrationLibraryItem_UpdateMetadata", new { IllustrationLibraryItemID = illustrationLibraryItemId, DisplayTitle = displayTitle, ShortDescription = shortDescription, MetadataJson = metadataJson, AdminNotes = adminNotes }, commandType: CommandType.StoredProcedure); } public async Task> GetApprovedAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.IllustrationLibraryItem_GetApproved", commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SupersedeRecoveredFailuresAsync() { using var connection = connectionFactory.CreateConnection(); return await connection.ExecuteAsync( """ SET ANSI_NULLS ON; SET QUOTED_IDENTIFIER ON; UPDATE failed SET Status = N'Superseded', IsActive = 0, UpdatedUtc = SYSUTCDATETIME(), AdminNotes = CONCAT(ISNULL(NULLIF(failed.AdminNotes, N''), N''), CASE WHEN NULLIF(failed.AdminNotes, N'') IS NULL THEN N'' ELSE N' ' END, N'Superseded after a replacement illustration generated successfully.') FROM dbo.IllustrationLibraryItems failed WHERE failed.Status = N'Failed' AND failed.IsActive = 1 AND EXISTS ( SELECT 1 FROM dbo.IllustrationLibraryItems replacement WHERE replacement.IllustrationLibraryItemID <> failed.IllustrationLibraryItemID AND replacement.StableCode = failed.StableCode AND replacement.IsActive = 1 AND replacement.Status IN (N'Generated', N'Approved') ); """); } }