using System.Text.Json; using CvMatcher.Data; using CvMatcher.Data.Entities; using CvMatcher.Data.Repositories.Contracts; using CvMatcher.Models.Responses; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace CvMatcher.Data.Repositories; public sealed class EfMatcherRepository : IMatcherRepository { private readonly CvMatcherDbContext _db; private readonly ILogger _logger; public EfMatcherRepository(CvMatcherDbContext db, ILogger logger) { _db = db; _logger = logger; } public async Task InitializeAsync(CancellationToken ct) { _logger.LogInformation("Ensuring CV matcher database schema exists using EF Core"); //await _db.Database.EnsureCreatedAsync(ct); } public async Task GetMatchAsync(string cvDocumentId, string jobDocumentId, string language, CancellationToken ct) { var json = await _db.CvMatchResults .AsNoTracking() .Where(x => x.CvDocumentId == cvDocumentId && x.JobDocumentId == jobDocumentId && x.Language == language) .Select(x => x.ResultJson) .FirstOrDefaultAsync(ct); if (string.IsNullOrWhiteSpace(json)) return null; var result = JsonSerializer.Deserialize(json, new JsonSerializerOptions(JsonSerializerDefaults.Web)); if (result is not null) result.Cached = true; return result; } public async Task SaveMatchAsync(string cvDocumentId, string jobDocumentId, string language, JobMatchResponse response, CancellationToken ct) { var exists = await _db.CvMatchResults.AnyAsync( x => x.CvDocumentId == cvDocumentId && x.JobDocumentId == jobDocumentId && x.Language == language, ct); if (exists) return; try { _db.CvMatchResults.Add(new CvMatchResultEntity { Id = Guid.NewGuid().ToString("N"), CvDocumentId = cvDocumentId, JobDocumentId = jobDocumentId, Language = language, ResultJson = JsonSerializer.Serialize(response, new JsonSerializerOptions(JsonSerializerDefaults.Web)), Score = response.Score, CreatedAt = DateTime.UtcNow }); await _db.SaveChangesAsync(ct); } catch (DbUpdateException ex) when (ex.InnerException?.Message.Contains("IX_Results_CvDocumentId_JobDocumentId_Language") == true || ex.InnerException?.Message.Contains("unique") == true) { // Duplicate key violation: record was inserted between the AnyAsync check and SaveChangesAsync. // This is safe to ignore — the match result already exists in the database. } } public async Task GetChatCompletionAsync(string cacheKey, CancellationToken ct) { return await _db.CvMatcherChatCache .AsNoTracking() .Where(x => x.CacheKey == cacheKey) .Select(x => x.ResponseText) .FirstOrDefaultAsync(ct); } public async Task SaveChatCompletionAsync(string cacheKey, string model, decimal temperature, string responseText, CancellationToken ct) { var exists = await _db.CvMatcherChatCache.AnyAsync(x => x.CacheKey == cacheKey, ct); if (exists) return; _db.CvMatcherChatCache.Add(new CvMatcherChatCacheEntity { CacheKey = cacheKey, Model = model, Temperature = temperature, ResponseText = responseText, CreatedAt = DateTime.UtcNow }); await _db.SaveChangesAsync(ct); } }