using Dapper;
using PlotLine.Models;

namespace PlotLine.Data;

public interface IStoryMemoryRepository
{
    Task ClearDerivedStateAsync(int importSessionId);
    Task<IReadOnlySet<int>> ListProcessedSceneResultIdsAsync(int importSessionId, string processorVersion);
    Task MarkSceneResultProcessedAsync(int importSessionId, int sceneResultId, string processorVersion, string snapshotHash);
    Task<StoryMemoryCharacter> UpsertCharacterAsync(StoryMemoryCharacterSave request);
    Task AddCharacterAliasAsync(int characterId, string alias, string normalisedAlias, string reason, int sceneResultId, decimal confidence);
    Task AddCharacterAttributeAsync(int characterId, string attributeType, string value, decimal confidence, bool isExplicit, string evidenceSummary, int sceneResultId);
    Task<StoryMemoryLocation> UpsertLocationAsync(StoryMemoryLocationSave request);
    Task<StoryMemoryAsset> UpsertAssetAsync(StoryMemoryAssetSave request);
    Task UpsertRelationshipAsync(int importSessionId, string sourceEntityType, int sourceEntityId, string targetEntityType, int targetEntityId, string relationshipType, decimal confidence, bool isExplicit, int sceneResultId);
    Task<IReadOnlyList<StoryMemoryCharacter>> ListCharactersAsync(int importSessionId);
    Task<IReadOnlyList<StoryMemoryCharacterAttribute>> ListCharacterAttributesAsync(int importSessionId);
    Task<IReadOnlyList<StoryMemoryLocation>> ListLocationsAsync(int importSessionId);
    Task<IReadOnlyList<StoryMemoryAsset>> ListAssetsAsync(int importSessionId);
    Task<IReadOnlyList<StoryMemoryIllustrationAssignment>> ListAssignmentsAsync(int importSessionId);
    Task<StoryMemoryIllustrationAssignment?> GetAssignmentAsync(int importSessionId, string entityType, int entityId);
    Task UpsertAssignmentAsync(StoryMemoryIllustrationAssignmentSave request);
    Task InvalidateAssignmentAsync(int assignmentId, string reason);
    Task<StoryMemoryIllustrationDemand> UpsertDemandAsync(StoryMemoryIllustrationDemandSave request);
    Task<string> GetAppearancePreferenceAsync(int importSessionId);
    Task<StoryMemoryImportContext> GetImportContextAsync(int importSessionId);
    Task SetAppearancePreferenceAsync(int importSessionId, string preference);
    Task SetImportContextAsync(int importSessionId, string appearancePreference, string? primaryAgeGroup, string? storyEra, string? storyLocation);
    Task<StoryMemoryDiagnostics> GetDiagnosticsAsync(int importSessionId);
}

public sealed class StoryMemoryRepository(ISqlConnectionFactory connectionFactory) : IStoryMemoryRepository
{
    public async Task ClearDerivedStateAsync(int importSessionId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            """
            DELETE FROM dbo.StoryMemoryIllustrationDemands WHERE ImportSessionID = @ImportSessionID;
            DELETE FROM dbo.StoryMemoryIllustrationAssignments WHERE ImportSessionID = @ImportSessionID;
            DELETE FROM dbo.StoryMemoryRelationships WHERE ImportSessionID = @ImportSessionID;
            DELETE a
            FROM dbo.StoryMemoryCharacterAttributes a
            INNER JOIN dbo.StoryMemoryCharacters c ON c.StoryMemoryCharacterID = a.StoryMemoryCharacterID
            WHERE c.ImportSessionID = @ImportSessionID;
            DELETE aliasRows
            FROM dbo.StoryMemoryCharacterAliases aliasRows
            INNER JOIN dbo.StoryMemoryCharacters c ON c.StoryMemoryCharacterID = aliasRows.StoryMemoryCharacterID
            WHERE c.ImportSessionID = @ImportSessionID;
            DELETE FROM dbo.StoryMemoryAssets WHERE ImportSessionID = @ImportSessionID;
            DELETE FROM dbo.StoryMemoryLocations WHERE ImportSessionID = @ImportSessionID;
            DELETE FROM dbo.StoryMemoryCharacters WHERE ImportSessionID = @ImportSessionID;
            DELETE FROM dbo.StoryMemoryProcessedSceneResults WHERE ImportSessionID = @ImportSessionID;
            """,
            new { ImportSessionID = importSessionId });
    }

    public async Task<IReadOnlySet<int>> ListProcessedSceneResultIdsAsync(int importSessionId, string processorVersion)
    {
        using var connection = connectionFactory.CreateConnection();
        var ids = await connection.QueryAsync<int>(
            """
            SELECT SceneResultID
            FROM dbo.StoryMemoryProcessedSceneResults
            WHERE ImportSessionID = @ImportSessionID
                AND ProcessorVersion = @ProcessorVersion;
            """,
            new { ImportSessionID = importSessionId, ProcessorVersion = processorVersion });
        return ids.ToHashSet();
    }

    public async Task MarkSceneResultProcessedAsync(int importSessionId, int sceneResultId, string processorVersion, string snapshotHash)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            """
            MERGE dbo.StoryMemoryProcessedSceneResults WITH (HOLDLOCK) AS target
            USING (SELECT @ImportSessionID AS ImportSessionID, @SceneResultID AS SceneResultID, @ProcessorVersion AS ProcessorVersion) AS source
                ON target.ImportSessionID = source.ImportSessionID
                AND target.SceneResultID = source.SceneResultID
                AND target.ProcessorVersion = source.ProcessorVersion
            WHEN MATCHED THEN
                UPDATE SET SnapshotHash = @SnapshotHash, ProcessedUtc = SYSUTCDATETIME()
            WHEN NOT MATCHED THEN
                INSERT (ImportSessionID, SceneResultID, ProcessorVersion, SnapshotHash)
                VALUES (@ImportSessionID, @SceneResultID, @ProcessorVersion, @SnapshotHash);
            """,
            new { ImportSessionID = importSessionId, SceneResultID = sceneResultId, ProcessorVersion = processorVersion, SnapshotHash = snapshotHash });
    }

    public async Task<StoryMemoryCharacter> UpsertCharacterAsync(StoryMemoryCharacterSave request)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<StoryMemoryCharacter>(
            """
            MERGE dbo.StoryMemoryCharacters WITH (HOLDLOCK) AS target
            USING (SELECT @ImportSessionID AS ImportSessionID, @CanonicalIdentityKey AS CanonicalIdentityKey) AS source
                ON target.ImportSessionID = source.ImportSessionID
                AND target.CanonicalIdentityKey = source.CanonicalIdentityKey
            WHEN MATCHED THEN
                UPDATE SET
                    DisplayName = CASE WHEN LEN(@DisplayName) > LEN(target.DisplayName) THEN @DisplayName ELSE target.DisplayName END,
                    EntityContext = CASE WHEN target.EntityContext = N'Story' OR @EntityContext = N'Story' THEN N'Story' ELSE @EntityContext END,
                    SourceSectionType = CASE WHEN target.SourceSectionType = N'Story' OR @SourceSectionType = N'Story' THEN N'Story' ELSE @SourceSectionType END,
                    IsNamed = CASE WHEN @IsNamed = 1 THEN 1 ELSE target.IsNamed END,
                    IsGroupEntity = CASE WHEN @IsGroupEntity = 1 THEN 1 ELSE target.IsGroupEntity END,
                    IsResolved = CASE WHEN @IsResolved = 1 THEN 1 ELSE target.IsResolved END,
                    IsNarrator = CASE WHEN @IsNarrator = 1 THEN 1 ELSE target.IsNarrator END,
                    IsPrimaryPOV = CASE WHEN @IsPrimaryPOV = 1 THEN 1 ELSE target.IsPrimaryPOV END,
                    Significance = CASE WHEN @Significance > target.Significance THEN @Significance ELSE target.Significance END,
                    LastUpdatedSceneResultID = @SceneResultID,
                    EvidenceVersion = target.EvidenceVersion + 1,
                    UpdatedUtc = SYSUTCDATETIME()
            WHEN NOT MATCHED THEN
                INSERT (ImportSessionID, ProjectID, BookID, CanonicalIdentityKey, DisplayName, EntityContext, SourceSectionType, IsNamed, IsGroupEntity, IsResolved, IsNarrator, IsPrimaryPOV, Significance, CreatedSceneResultID, LastUpdatedSceneResultID)
                VALUES (@ImportSessionID, @ProjectID, @BookID, @CanonicalIdentityKey, @DisplayName, @EntityContext, @SourceSectionType, @IsNamed, @IsGroupEntity, @IsResolved, @IsNarrator, @IsPrimaryPOV, @Significance, @SceneResultID, @SceneResultID);

            SELECT *
            FROM dbo.StoryMemoryCharacters
            WHERE ImportSessionID = @ImportSessionID
                AND CanonicalIdentityKey = @CanonicalIdentityKey;
            """,
            request);
    }

    public async Task AddCharacterAliasAsync(int characterId, string alias, string normalisedAlias, string reason, int sceneResultId, decimal confidence)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            """
            MERGE dbo.StoryMemoryCharacterAliases WITH (HOLDLOCK) AS target
            USING (SELECT @StoryMemoryCharacterID AS StoryMemoryCharacterID, @NormalisedAlias AS NormalisedAlias, @SourceSceneResultID AS SourceSceneResultID) AS source
                ON target.StoryMemoryCharacterID = source.StoryMemoryCharacterID
                AND target.NormalisedAlias = source.NormalisedAlias
                AND ISNULL(target.SourceSceneResultID, -1) = ISNULL(source.SourceSceneResultID, -1)
            WHEN NOT MATCHED THEN
                INSERT (StoryMemoryCharacterID, Alias, NormalisedAlias, ResolutionReason, SourceSceneResultID, Confidence)
                VALUES (@StoryMemoryCharacterID, @Alias, @NormalisedAlias, @ResolutionReason, @SourceSceneResultID, @Confidence);
            """,
            new { StoryMemoryCharacterID = characterId, Alias = alias, NormalisedAlias = normalisedAlias, ResolutionReason = reason, SourceSceneResultID = sceneResultId, Confidence = confidence });
    }

    public async Task AddCharacterAttributeAsync(int characterId, string attributeType, string value, decimal confidence, bool isExplicit, string evidenceSummary, int sceneResultId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            """
            MERGE dbo.StoryMemoryCharacterAttributes WITH (HOLDLOCK) AS target
            USING (SELECT @StoryMemoryCharacterID AS StoryMemoryCharacterID, @AttributeType AS AttributeType, @NormalisedValue AS NormalisedValue, @SourceSceneResultID AS SourceSceneResultID) AS source
                ON target.StoryMemoryCharacterID = source.StoryMemoryCharacterID
                AND target.AttributeType = source.AttributeType
                AND target.NormalisedValue = source.NormalisedValue
                AND ISNULL(target.SourceSceneResultID, -1) = ISNULL(source.SourceSceneResultID, -1)
                AND target.SupersededUtc IS NULL
            WHEN MATCHED THEN
                UPDATE SET Confidence = CASE WHEN @Confidence > target.Confidence THEN @Confidence ELSE target.Confidence END,
                    IsExplicit = CASE WHEN @IsExplicit = 1 THEN 1 ELSE target.IsExplicit END,
                    EvidenceSummary = @EvidenceSummary
            WHEN NOT MATCHED THEN
                INSERT (StoryMemoryCharacterID, AttributeType, NormalisedValue, Confidence, IsExplicit, EvidenceSummary, SourceSceneResultID)
                VALUES (@StoryMemoryCharacterID, @AttributeType, @NormalisedValue, @Confidence, @IsExplicit, @EvidenceSummary, @SourceSceneResultID);
            """,
            new { StoryMemoryCharacterID = characterId, AttributeType = attributeType, NormalisedValue = value, Confidence = confidence, IsExplicit = isExplicit, EvidenceSummary = evidenceSummary, SourceSceneResultID = sceneResultId });
    }

    public async Task<StoryMemoryLocation> UpsertLocationAsync(StoryMemoryLocationSave request)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<StoryMemoryLocation>(
            """
            MERGE dbo.StoryMemoryLocations WITH (HOLDLOCK) AS target
            USING (SELECT @ImportSessionID AS ImportSessionID, @CanonicalIdentityKey AS CanonicalIdentityKey) AS source
                ON target.ImportSessionID = source.ImportSessionID
                AND target.CanonicalIdentityKey = source.CanonicalIdentityKey
            WHEN MATCHED THEN
                UPDATE SET DisplayName = @DisplayName,
                    SemanticType = @SemanticType,
                    Subtype = @Subtype,
                    InteriorExterior = @InteriorExterior,
                    LastSeenSceneResultID = @SceneResultID,
                    UpdatedUtc = SYSUTCDATETIME()
            WHEN NOT MATCHED THEN
                INSERT (ImportSessionID, ProjectID, BookID, CanonicalIdentityKey, DisplayName, SemanticType, Subtype, InteriorExterior, FirstSeenSceneResultID, LastSeenSceneResultID)
                VALUES (@ImportSessionID, @ProjectID, @BookID, @CanonicalIdentityKey, @DisplayName, @SemanticType, @Subtype, @InteriorExterior, @SceneResultID, @SceneResultID);

            SELECT *
            FROM dbo.StoryMemoryLocations
            WHERE ImportSessionID = @ImportSessionID AND CanonicalIdentityKey = @CanonicalIdentityKey;
            """,
            request);
    }

    public async Task<StoryMemoryAsset> UpsertAssetAsync(StoryMemoryAssetSave request)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<StoryMemoryAsset>(
            """
            MERGE dbo.StoryMemoryAssets WITH (HOLDLOCK) AS target
            USING (SELECT @ImportSessionID AS ImportSessionID, @CanonicalIdentityKey AS CanonicalIdentityKey) AS source
                ON target.ImportSessionID = source.ImportSessionID
                AND target.CanonicalIdentityKey = source.CanonicalIdentityKey
            WHEN MATCHED THEN
                UPDATE SET DisplayName = @DisplayName,
                    SemanticType = @SemanticType,
                    Subtype = @Subtype,
                    Colour = @Colour,
                    Era = @Era,
                    LastSeenSceneResultID = @SceneResultID,
                    UpdatedUtc = SYSUTCDATETIME()
            WHEN NOT MATCHED THEN
                INSERT (ImportSessionID, ProjectID, BookID, CanonicalIdentityKey, DisplayName, SemanticType, Subtype, Colour, Era, FirstSeenSceneResultID, LastSeenSceneResultID)
                VALUES (@ImportSessionID, @ProjectID, @BookID, @CanonicalIdentityKey, @DisplayName, @SemanticType, @Subtype, @Colour, @Era, @SceneResultID, @SceneResultID);

            SELECT *
            FROM dbo.StoryMemoryAssets
            WHERE ImportSessionID = @ImportSessionID AND CanonicalIdentityKey = @CanonicalIdentityKey;
            """,
            request);
    }

    public async Task UpsertRelationshipAsync(int importSessionId, string sourceEntityType, int sourceEntityId, string targetEntityType, int targetEntityId, string relationshipType, decimal confidence, bool isExplicit, int sceneResultId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            """
            MERGE dbo.StoryMemoryRelationships WITH (HOLDLOCK) AS target
            USING (SELECT @ImportSessionID AS ImportSessionID, @SourceEntityType AS SourceEntityType, @SourceEntityID AS SourceEntityID, @TargetEntityType AS TargetEntityType, @TargetEntityID AS TargetEntityID, @RelationshipType AS RelationshipType) AS source
                ON target.ImportSessionID = source.ImportSessionID
                AND target.SourceEntityType = source.SourceEntityType
                AND target.SourceEntityID = source.SourceEntityID
                AND target.TargetEntityType = source.TargetEntityType
                AND target.TargetEntityID = source.TargetEntityID
                AND target.RelationshipType = source.RelationshipType
                AND target.Active = 1
            WHEN MATCHED THEN
                UPDATE SET Confidence = CASE WHEN @Confidence > target.Confidence THEN @Confidence ELSE target.Confidence END,
                    IsExplicit = CASE WHEN @IsExplicit = 1 THEN 1 ELSE target.IsExplicit END,
                    LastUpdatedSceneResultID = @SceneResultID,
                    UpdatedUtc = SYSUTCDATETIME()
            WHEN NOT MATCHED THEN
                INSERT (ImportSessionID, SourceEntityType, SourceEntityID, TargetEntityType, TargetEntityID, RelationshipType, Confidence, IsExplicit, FirstSeenSceneResultID, LastUpdatedSceneResultID)
                VALUES (@ImportSessionID, @SourceEntityType, @SourceEntityID, @TargetEntityType, @TargetEntityID, @RelationshipType, @Confidence, @IsExplicit, @SceneResultID, @SceneResultID);
            """,
            new { ImportSessionID = importSessionId, SourceEntityType = sourceEntityType, SourceEntityID = sourceEntityId, TargetEntityType = targetEntityType, TargetEntityID = targetEntityId, RelationshipType = relationshipType, Confidence = confidence, IsExplicit = isExplicit, SceneResultID = sceneResultId });
    }

    public async Task<IReadOnlyList<StoryMemoryCharacter>> ListCharactersAsync(int importSessionId)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryMemoryCharacter>("SELECT * FROM dbo.StoryMemoryCharacters WHERE ImportSessionID = @ImportSessionID ORDER BY Significance DESC, DisplayName;", new { ImportSessionID = importSessionId })).ToList();
    }

    public async Task<IReadOnlyList<StoryMemoryCharacterAttribute>> ListCharacterAttributesAsync(int importSessionId)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryMemoryCharacterAttribute>(
            """
            SELECT a.*
            FROM dbo.StoryMemoryCharacterAttributes a
            INNER JOIN dbo.StoryMemoryCharacters c ON c.StoryMemoryCharacterID = a.StoryMemoryCharacterID
            WHERE c.ImportSessionID = @ImportSessionID
                AND a.SupersededUtc IS NULL;
            """,
            new { ImportSessionID = importSessionId })).ToList();
    }

    public async Task<IReadOnlyList<StoryMemoryLocation>> ListLocationsAsync(int importSessionId)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryMemoryLocation>("SELECT * FROM dbo.StoryMemoryLocations WHERE ImportSessionID = @ImportSessionID ORDER BY DisplayName;", new { ImportSessionID = importSessionId })).ToList();
    }

    public async Task<IReadOnlyList<StoryMemoryAsset>> ListAssetsAsync(int importSessionId)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryMemoryAsset>("SELECT * FROM dbo.StoryMemoryAssets WHERE ImportSessionID = @ImportSessionID ORDER BY DisplayName;", new { ImportSessionID = importSessionId })).ToList();
    }

    public async Task<IReadOnlyList<StoryMemoryIllustrationAssignment>> ListAssignmentsAsync(int importSessionId)
    {
        using var connection = connectionFactory.CreateConnection();
        return (await connection.QueryAsync<StoryMemoryIllustrationAssignment>("SELECT * FROM dbo.StoryMemoryIllustrationAssignments WHERE ImportSessionID = @ImportSessionID AND InvalidatedUtc IS NULL;", new { ImportSessionID = importSessionId })).ToList();
    }

    public async Task<StoryMemoryIllustrationAssignment?> GetAssignmentAsync(int importSessionId, string entityType, int entityId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<StoryMemoryIllustrationAssignment>(
            "SELECT * FROM dbo.StoryMemoryIllustrationAssignments WHERE ImportSessionID = @ImportSessionID AND EntityType = @EntityType AND EntityID = @EntityID AND InvalidatedUtc IS NULL;",
            new { ImportSessionID = importSessionId, EntityType = entityType, EntityID = entityId });
    }

    public async Task UpsertAssignmentAsync(StoryMemoryIllustrationAssignmentSave request)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            """
            MERGE dbo.StoryMemoryIllustrationAssignments WITH (HOLDLOCK) AS target
            USING (SELECT @ImportSessionID AS ImportSessionID, @EntityType AS EntityType, @EntityID AS EntityID) AS source
                ON target.ImportSessionID = source.ImportSessionID
                AND target.EntityType = source.EntityType
                AND target.EntityID = source.EntityID
                AND target.InvalidatedUtc IS NULL
            WHEN MATCHED THEN
                UPDATE SET IllustrationLibraryItemID = @IllustrationLibraryItemID,
                    StableCode = @StableCode,
                    AssignmentStatus = @AssignmentStatus,
                    IsFallback = @IsFallback,
                    MatchScore = @MatchScore,
                    Confidence = @Confidence,
                    AssignmentReason = @AssignmentReason,
                    EvidenceVersion = @EvidenceVersion,
                    LastValidatedUtc = SYSUTCDATETIME(),
                    UpdatedUtc = SYSUTCDATETIME()
            WHEN NOT MATCHED THEN
                INSERT (ImportSessionID, EntityType, EntityID, IllustrationLibraryItemID, StableCode, AssignmentStatus, IsFallback, MatchScore, Confidence, AssignmentReason, EvidenceVersion)
                VALUES (@ImportSessionID, @EntityType, @EntityID, @IllustrationLibraryItemID, @StableCode, @AssignmentStatus, @IsFallback, @MatchScore, @Confidence, @AssignmentReason, @EvidenceVersion);
            """,
            request);
    }

    public async Task InvalidateAssignmentAsync(int assignmentId, string reason)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            """
            UPDATE dbo.StoryMemoryIllustrationAssignments
            SET InvalidatedUtc = SYSUTCDATETIME(),
                InvalidationReason = @Reason,
                UpdatedUtc = SYSUTCDATETIME()
            WHERE StoryMemoryIllustrationAssignmentID = @AssignmentID
                AND InvalidatedUtc IS NULL;
            """,
            new { AssignmentID = assignmentId, Reason = reason });
    }

    public async Task<StoryMemoryIllustrationDemand> UpsertDemandAsync(StoryMemoryIllustrationDemandSave request)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<StoryMemoryIllustrationDemand>(
            """
            MERGE dbo.StoryMemoryIllustrationDemands WITH (HOLDLOCK) AS target
            USING (SELECT @ImportSessionID AS ImportSessionID, @EntityType AS EntityType, @ArchetypeKey AS ArchetypeKey) AS source
                ON target.ImportSessionID = source.ImportSessionID
                AND target.EntityType = source.EntityType
                AND target.ArchetypeKey = source.ArchetypeKey
            WHEN MATCHED THEN
                UPDATE SET DemandCount = DemandCount + 1,
                    WaitingEntityCount = @WaitingEntityCount,
                    Status = @Status,
                    QueuedIllustrationLibraryItemID = COALESCE(@QueuedIllustrationLibraryItemID, target.QueuedIllustrationLibraryItemID),
                    GeneratedIllustrationLibraryItemID = COALESCE(@GeneratedIllustrationLibraryItemID, target.GeneratedIllustrationLibraryItemID),
                    UpdatedUtc = SYSUTCDATETIME()
            WHEN NOT MATCHED THEN
                INSERT (ImportSessionID, ProjectID, EntityType, ArchetypeKey, WaitingEntityCount, Status, QueuedIllustrationLibraryItemID, GeneratedIllustrationLibraryItemID)
                VALUES (@ImportSessionID, @ProjectID, @EntityType, @ArchetypeKey, @WaitingEntityCount, @Status, @QueuedIllustrationLibraryItemID, @GeneratedIllustrationLibraryItemID);

            SELECT *
            FROM dbo.StoryMemoryIllustrationDemands
            WHERE ImportSessionID = @ImportSessionID AND EntityType = @EntityType AND ArchetypeKey = @ArchetypeKey;
            """,
            request);
    }

    public async Task<string> GetAppearancePreferenceAsync(int importSessionId)
        => (await GetImportContextAsync(importSessionId)).AppearancePreference;

    public async Task<StoryMemoryImportContext> GetImportContextAsync(int importSessionId)
    {
        using var connection = connectionFactory.CreateConnection();
        await EnsureImportPreferenceContextColumnsAsync(connection);
        return await connection.QuerySingleOrDefaultAsync<StoryMemoryImportContext>(
            """
            SELECT
                AppearancePreference,
                COALESCE(PrimaryAgeGroup, N'') AS PrimaryAgeGroup,
                COALESCE(StoryEra, N'') AS StoryEra,
                COALESCE(StoryLocation, N'') AS StoryLocation
            FROM dbo.StoryMemoryImportPreferences
            WHERE ImportSessionID = @ImportSessionID;
            """,
            new { ImportSessionID = importSessionId })
            ?? new StoryMemoryImportContext(StoryMemoryAppearancePreferences.Balanced, string.Empty, string.Empty, string.Empty);
    }

    public async Task SetAppearancePreferenceAsync(int importSessionId, string preference)
        => await SetImportContextAsync(importSessionId, preference, null, null, null);

    public async Task SetImportContextAsync(int importSessionId, string appearancePreference, string? primaryAgeGroup, string? storyEra, string? storyLocation)
    {
        using var connection = connectionFactory.CreateConnection();
        await EnsureImportPreferenceContextColumnsAsync(connection);
        await connection.ExecuteAsync(
            """
            MERGE dbo.StoryMemoryImportPreferences WITH (HOLDLOCK) AS target
            USING (SELECT @ImportSessionID AS ImportSessionID) AS source
                ON target.ImportSessionID = source.ImportSessionID
            WHEN MATCHED THEN
                UPDATE SET AppearancePreference = @AppearancePreference,
                    PrimaryAgeGroup = COALESCE(@PrimaryAgeGroup, PrimaryAgeGroup),
                    StoryEra = COALESCE(@StoryEra, StoryEra),
                    StoryLocation = COALESCE(@StoryLocation, StoryLocation),
                    UpdatedUtc = SYSUTCDATETIME()
            WHEN NOT MATCHED THEN
                INSERT (ImportSessionID, AppearancePreference, PrimaryAgeGroup, StoryEra, StoryLocation)
                VALUES (@ImportSessionID, @AppearancePreference, COALESCE(@PrimaryAgeGroup, N''), COALESCE(@StoryEra, N''), COALESCE(@StoryLocation, N''));
            """,
            new
            {
                ImportSessionID = importSessionId,
                AppearancePreference = appearancePreference,
                PrimaryAgeGroup = TrimContext(primaryAgeGroup, 120),
                StoryEra = TrimContext(storyEra, 120),
                StoryLocation = TrimContext(storyLocation, 160)
            });
    }

    private static async Task EnsureImportPreferenceContextColumnsAsync(System.Data.IDbConnection connection)
    {
        await connection.ExecuteAsync(
            """
            IF COL_LENGTH(N'dbo.StoryMemoryImportPreferences', N'PrimaryAgeGroup') IS NULL
                ALTER TABLE dbo.StoryMemoryImportPreferences ADD PrimaryAgeGroup nvarchar(120) NULL;
            IF COL_LENGTH(N'dbo.StoryMemoryImportPreferences', N'StoryEra') IS NULL
                ALTER TABLE dbo.StoryMemoryImportPreferences ADD StoryEra nvarchar(120) NULL;
            IF COL_LENGTH(N'dbo.StoryMemoryImportPreferences', N'StoryLocation') IS NULL
                ALTER TABLE dbo.StoryMemoryImportPreferences ADD StoryLocation nvarchar(160) NULL;
            """);
    }

    private static string? TrimContext(string? value, int maxLength)
    {
        var clean = value?.Trim();
        return string.IsNullOrWhiteSpace(clean) ? null : clean[..Math.Min(clean.Length, maxLength)];
    }

    public async Task<StoryMemoryDiagnostics> GetDiagnosticsAsync(int importSessionId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<StoryMemoryDiagnostics>(
            """
            SELECT
                @ImportSessionID AS ImportSessionID,
                (SELECT COUNT(*) FROM dbo.StoryMemoryCharacters WHERE ImportSessionID = @ImportSessionID) AS CharacterCount,
                (SELECT COUNT(*) FROM dbo.StoryMemoryLocations WHERE ImportSessionID = @ImportSessionID) AS LocationCount,
                (SELECT COUNT(*) FROM dbo.StoryMemoryAssets WHERE ImportSessionID = @ImportSessionID) AS AssetCount,
                (SELECT COUNT(*) FROM dbo.StoryMemoryCharacterAliases a INNER JOIN dbo.StoryMemoryCharacters c ON c.StoryMemoryCharacterID = a.StoryMemoryCharacterID WHERE c.ImportSessionID = @ImportSessionID) AS AliasCount,
                (SELECT COUNT(*) FROM dbo.StoryMemoryCharacterAttributes a INNER JOIN dbo.StoryMemoryCharacters c ON c.StoryMemoryCharacterID = a.StoryMemoryCharacterID WHERE c.ImportSessionID = @ImportSessionID AND a.SupersededUtc IS NULL) AS AttributeCount,
                (SELECT COUNT(*) FROM dbo.StoryMemoryRelationships WHERE ImportSessionID = @ImportSessionID AND Active = 1) AS RelationshipCount,
                (SELECT COUNT(*) FROM dbo.StoryMemoryIllustrationAssignments WHERE ImportSessionID = @ImportSessionID AND InvalidatedUtc IS NULL) AS AssignmentCount,
                (SELECT COUNT(*) FROM dbo.StoryMemoryIllustrationAssignments WHERE ImportSessionID = @ImportSessionID AND InvalidatedUtc IS NULL AND IsFallback = 1) AS FallbackCount,
                (SELECT COUNT(*) FROM dbo.StoryMemoryIllustrationDemands WHERE ImportSessionID = @ImportSessionID) AS DemandCount,
                (SELECT COUNT(*) FROM dbo.StoryMemoryProcessedSceneResults WHERE ImportSessionID = @ImportSessionID) AS ProcessedSceneCount,
                (SELECT MAX(SceneResultID) FROM dbo.StoryMemoryProcessedSceneResults WHERE ImportSessionID = @ImportSessionID) AS LastProcessedSceneResultID,
                COALESCE((SELECT AppearancePreference FROM dbo.StoryMemoryImportPreferences WHERE ImportSessionID = @ImportSessionID), N'Balanced') AS AppearancePreference;
            """,
            new { ImportSessionID = importSessionId });
    }
}
