using Dapper; using PlotLine.Models; namespace PlotLine.Data; public interface ICharacterIntelligenceRepository { Task> ListProfilesAsync(int importSessionId); Task UpsertProfileAsync(CharacterIntelligenceProfileSave request); Task AddEvidenceAsync(CharacterIntelligenceEvidenceSave request); } public sealed class CharacterIntelligenceRepository(ISqlConnectionFactory connectionFactory) : ICharacterIntelligenceRepository { public async Task> ListProfilesAsync(int importSessionId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( """ SELECT * FROM dbo.CharacterIntelligenceProfiles WHERE ImportSessionID = @ImportSessionID ORDER BY IsRecurring DESC, Confidence DESC, CanonicalName; """, new { ImportSessionID = importSessionId }); return rows.ToList(); } public async Task UpsertProfileAsync(CharacterIntelligenceProfileSave request) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( """ MERGE dbo.CharacterIntelligenceProfiles 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 CanonicalName = @CanonicalName, ProfileJson = @ProfileJson, Confidence = @Confidence, NarrativeImportance = @NarrativeImportance, IsRecurring = CASE WHEN @IsRecurring = 1 THEN 1 ELSE target.IsRecurring END, EvidenceVersion = @EvidenceVersion, LastUpdatedSceneResultID = @SceneResultID, UpdatedUtc = SYSUTCDATETIME() WHEN NOT MATCHED THEN INSERT (ImportSessionID, ProjectID, BookID, CanonicalIdentityKey, CanonicalName, ProfileJson, Confidence, NarrativeImportance, IsRecurring, EvidenceVersion, LastUpdatedSceneResultID) VALUES (@ImportSessionID, @ProjectID, @BookID, @CanonicalIdentityKey, @CanonicalName, @ProfileJson, @Confidence, @NarrativeImportance, @IsRecurring, @EvidenceVersion, @SceneResultID); SELECT * FROM dbo.CharacterIntelligenceProfiles WHERE ImportSessionID = @ImportSessionID AND CanonicalIdentityKey = @CanonicalIdentityKey; """, request); } public async Task AddEvidenceAsync(CharacterIntelligenceEvidenceSave request) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( """ INSERT dbo.CharacterIntelligenceEvidenceEvents (CharacterIntelligenceProfileID, SceneResultID, FieldName, PreviousValue, NewValue, Evidence, Confidence, IsContradiction, Source) VALUES (@CharacterIntelligenceProfileID, @SceneResultID, @FieldName, @PreviousValue, @NewValue, @Evidence, @Confidence, @IsContradiction, @Source); """, request); } }