PlotDirector/PlotLine/Services/StoryIntelligenceDevelopmentResetService.cs

250 lines
11 KiB
C#

using Dapper;
using PlotLine.Data;
namespace PlotLine.Services;
public interface IStoryIntelligenceDevelopmentResetService
{
Task<StoryIntelligenceDevelopmentResetResult> ResetOwnedProjectsAndImportsAsync(int userId, CancellationToken cancellationToken = default);
}
public sealed class StoryIntelligenceDevelopmentResetService(
ISqlConnectionFactory connectionFactory,
ILogger<StoryIntelligenceDevelopmentResetService> logger) : IStoryIntelligenceDevelopmentResetService
{
public async Task<StoryIntelligenceDevelopmentResetResult> ResetOwnedProjectsAndImportsAsync(int userId, CancellationToken cancellationToken = default)
{
using var connection = connectionFactory.CreateConnection();
var command = new CommandDefinition(PrepareResetSql, new { UserID = userId }, cancellationToken: cancellationToken, commandTimeout: 120);
var summary = await connection.QuerySingleAsync<PreparedResetSummary>(command);
foreach (var projectId in summary.ProjectIdList)
{
var deleteCommand = new CommandDefinition(
"dbo.ProjectHardDelete_ForOwner",
new { ProjectID = projectId, UserID = userId },
commandType: System.Data.CommandType.StoredProcedure,
cancellationToken: cancellationToken,
commandTimeout: 120);
await connection.ExecuteAsync(deleteCommand);
}
var verificationCommand = new CommandDefinition(VerifyResetSql, new { UserID = userId }, cancellationToken: cancellationToken, commandTimeout: 120);
var verification = await connection.QuerySingleAsync<ResetVerification>(verificationCommand);
var result = new StoryIntelligenceDevelopmentResetResult(
summary.ProjectIdList.Count,
summary.ImportSessionCount,
summary.RunCount,
verification.RemainingProjects,
verification.RemainingImports,
verification.RemainingRuns,
verification.PreservedIllustrationItems);
logger.LogWarning(
"Admin Story Intelligence reset completed for user {UserId}. DeletedProjects={DeletedProjects} DeletedImports={DeletedImports} DeletedRuns={DeletedRuns} PreservedIllustrationItems={PreservedIllustrationItems}.",
userId,
result.DeletedProjects,
result.DeletedImportSessions,
result.DeletedRuns,
result.PreservedIllustrationItems);
return result;
}
private const string PrepareResetSql = """
SET NOCOUNT ON;
DECLARE @ProjectIDs TABLE (ProjectID int NOT NULL PRIMARY KEY);
DECLARE @BookIDs TABLE (BookID int NOT NULL PRIMARY KEY);
DECLARE @ImportSessionIDs TABLE (ImportSessionID int NOT NULL PRIMARY KEY);
DECLARE @RunIDs TABLE (StoryIntelligenceRunID int NOT NULL PRIMARY KEY);
INSERT @ProjectIDs (ProjectID)
SELECT DISTINCT p.ProjectID
FROM dbo.Projects p
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
WHERE pua.UserID = @UserID
AND pua.AccessRole = N'Owner'
AND pua.IsActive = 1;
INSERT @BookIDs (BookID)
SELECT b.BookID
FROM dbo.Books b
INNER JOIN @ProjectIDs p ON p.ProjectID = b.ProjectID;
INSERT @ImportSessionIDs (ImportSessionID)
SELECT StoryIntelligenceBookPipelineID
FROM dbo.StoryIntelligenceBookPipelines pipeline
WHERE EXISTS (SELECT 1 FROM @ProjectIDs p WHERE p.ProjectID = pipeline.ProjectID)
OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = pipeline.BookID);
INSERT @RunIDs (StoryIntelligenceRunID)
SELECT StoryIntelligenceRunID
FROM dbo.StoryIntelligenceRuns run
WHERE run.UserID = @UserID
AND (
run.ProjectID IS NULL
OR EXISTS (SELECT 1 FROM @ProjectIDs p WHERE p.ProjectID = run.ProjectID)
OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = run.BookID)
);
DECLARE @ImportSessionCount int = (SELECT COUNT(*) FROM @ImportSessionIDs);
DECLARE @RunCount int = (SELECT COUNT(*) FROM @RunIDs);
UPDATE dbo.StoryIntelligenceRuns
SET Status = CASE WHEN Status IN (N'Pending', N'Running') THEN N'Cancelled' ELSE Status END,
CurrentStage = N'Cancelled before admin reset',
CurrentMessage = N'Admin reset invalidated this run while preserving generated illustrations.',
CancellationRequestedUtc = COALESCE(CancellationRequestedUtc, SYSUTCDATETIME()),
CancelledUtc = COALESCE(CancelledUtc, SYSUTCDATETIME()),
CompletedUtc = COALESCE(CompletedUtc, SYSUTCDATETIME()),
UpdatedUtc = SYSUTCDATETIME()
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceRuns.StoryIntelligenceRunID)
AND Status IN (N'Pending', N'Running');
DELETE FROM dbo.StoryMemoryIllustrationAssignments
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryIllustrationAssignments.ImportSessionID);
DELETE FROM dbo.StoryMemoryIllustrationDemands
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryIllustrationDemands.ImportSessionID);
DELETE FROM dbo.StoryMemoryRelationships
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryRelationships.ImportSessionID);
DELETE FROM dbo.StoryMemoryCharacterAttributes
WHERE EXISTS
(
SELECT 1
FROM dbo.StoryMemoryCharacters c
INNER JOIN @ImportSessionIDs ids ON ids.ImportSessionID = c.ImportSessionID
WHERE c.StoryMemoryCharacterID = StoryMemoryCharacterAttributes.StoryMemoryCharacterID
);
DELETE FROM dbo.StoryMemoryCharacterAliases
WHERE EXISTS
(
SELECT 1
FROM dbo.StoryMemoryCharacters c
INNER JOIN @ImportSessionIDs ids ON ids.ImportSessionID = c.ImportSessionID
WHERE c.StoryMemoryCharacterID = StoryMemoryCharacterAliases.StoryMemoryCharacterID
);
DELETE FROM dbo.StoryMemoryAssets
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryAssets.ImportSessionID);
DELETE FROM dbo.StoryMemoryLocations
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryLocations.ImportSessionID);
DELETE FROM dbo.StoryMemoryCharacters
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryCharacters.ImportSessionID);
DELETE FROM dbo.StoryMemoryProcessedSceneResults
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryProcessedSceneResults.ImportSessionID);
DELETE FROM dbo.StoryMemoryImportPreferences
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryImportPreferences.ImportSessionID);
DELETE FROM dbo.StoryIntelligenceCharacterIllustrationAssignments
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryIntelligenceCharacterIllustrationAssignments.ImportSessionID)
OR EXISTS (SELECT 1 FROM @ProjectIDs p WHERE p.ProjectID = StoryIntelligenceCharacterIllustrationAssignments.ProjectID);
DELETE FROM dbo.StoryIntelligenceIllustrationDemands
WHERE EXISTS (SELECT 1 FROM @ProjectIDs p WHERE p.ProjectID = StoryIntelligenceIllustrationDemands.ProjectID);
DELETE FROM dbo.StoryIntelligenceBookPipelines
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryIntelligenceBookPipelines.StoryIntelligenceBookPipelineID);
DELETE FROM dbo.StoryIntelligenceSceneResults
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceSceneResults.StoryIntelligenceRunID);
DELETE FROM dbo.StoryIntelligenceChapterResults
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceChapterResults.StoryIntelligenceRunID);
DELETE FROM dbo.StoryIntelligenceImportCommits
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceImportCommits.StoryIntelligenceRunID);
DELETE FROM dbo.StoryIntelligenceRuns
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceRuns.StoryIntelligenceRunID);
UPDATE dbo.Projects
SET IsArchived = 1,
ArchivedDate = COALESCE(ArchivedDate, SYSUTCDATETIME()),
ArchivedReason = N'Admin Story Intelligence reset preserving generated illustrations',
UpdatedDate = SYSUTCDATETIME()
WHERE EXISTS (SELECT 1 FROM @ProjectIDs ids WHERE ids.ProjectID = Projects.ProjectID);
SELECT
ProjectIds = COALESCE(STRING_AGG(CONVERT(varchar(20), ProjectID), ','), ''),
ImportSessionCount = @ImportSessionCount,
RunCount = @RunCount
FROM @ProjectIDs;
""";
private const string VerifyResetSql = """
SELECT
RemainingProjects = (
SELECT COUNT(*)
FROM dbo.Projects p
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
WHERE pua.UserID = @UserID AND pua.AccessRole = N'Owner' AND pua.IsActive = 1
),
RemainingImports = (
SELECT COUNT(*)
FROM dbo.StoryIntelligenceBookPipelines pipeline
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = pipeline.ProjectID
WHERE pua.UserID = @UserID AND pua.AccessRole = N'Owner' AND pua.IsActive = 1
),
RemainingRuns = (
SELECT COUNT(*)
FROM dbo.StoryIntelligenceRuns
WHERE UserID = @UserID
),
PreservedIllustrationItems = (
SELECT COUNT(*)
FROM dbo.IllustrationLibraryItems
WHERE Category IN (N'Character', N'Location', N'Asset')
);
""";
private sealed class PreparedResetSummary
{
public string ProjectIds { get; init; } = string.Empty;
public int ImportSessionCount { get; init; }
public int RunCount { get; init; }
public IReadOnlyList<int> ProjectIdList => SplitProjectIds(ProjectIds);
}
private sealed class ResetVerification
{
public int RemainingProjects { get; init; }
public int RemainingImports { get; init; }
public int RemainingRuns { get; init; }
public int PreservedIllustrationItems { get; init; }
}
private static IReadOnlyList<int> SplitProjectIds(string projectIds)
=> string.IsNullOrWhiteSpace(projectIds)
? []
: projectIds.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(value => int.TryParse(value, out var id) ? id : 0)
.Where(id => id > 0)
.ToArray();
}
public sealed record StoryIntelligenceDevelopmentResetResult(
int DeletedProjects,
int DeletedImportSessions,
int DeletedRuns,
int RemainingProjects,
int RemainingImportSessions,
int RemainingRuns,
int PreservedIllustrationItems)
{
public bool Success => RemainingProjects == 0 && RemainingImportSessions == 0 && RemainingRuns == 0;
public string Message => Success
? $"Reset complete. Deleted {DeletedProjects:N0} project(s), {DeletedImportSessions:N0} import(s) and {DeletedRuns:N0} run(s). Preserved {PreservedIllustrationItems:N0} generated image record(s)."
: $"Reset finished with remaining data: {RemainingProjects:N0} project(s), {RemainingImportSessions:N0} import(s), {RemainingRuns:N0} run(s). Preserved {PreservedIllustrationItems:N0} generated image record(s).";
}