using System.Text.Json; using Api.Responses; using Api.Services.Contracts; using Microsoft.Data.SqlClient; namespace Api.Services; public sealed class SqlMatcherRepository : IMatcherRepository { private readonly string _connectionString; public SqlMatcherRepository(IConfiguration configuration) { _connectionString = configuration.GetConnectionString("CvMatcherDb") ?? throw new InvalidOperationException("Connection string 'CvMatcherDb' is missing."); } public async Task InitializeAsync(CancellationToken ct) { await EnsureDatabaseExistsAsync(ct); var sql = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "Database", "schema.sql"), ct); await using var connection = new SqlConnection(_connectionString); await connection.OpenAsync(ct); foreach (var commandText in sql.Split("GO", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { await using var command = new SqlCommand(commandText, connection); await command.ExecuteNonQueryAsync(ct); } } public async Task GetMatchAsync(string cvDocumentId, string jobDocumentId, CancellationToken ct) { const string sql = "SELECT ResultJson FROM CvMatchResults WHERE CvDocumentId = @CvDocumentId AND JobDocumentId = @JobDocumentId"; await using var connection = new SqlConnection(_connectionString); await connection.OpenAsync(ct); await using var command = new SqlCommand(sql, connection); command.Parameters.AddWithValue("@CvDocumentId", cvDocumentId); command.Parameters.AddWithValue("@JobDocumentId", jobDocumentId); var json = await command.ExecuteScalarAsync(ct) as string; 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, JobMatchResponse response, CancellationToken ct) { const string sql = """ IF NOT EXISTS (SELECT 1 FROM CvMatchResults WHERE CvDocumentId = @CvDocumentId AND JobDocumentId = @JobDocumentId) INSERT INTO CvMatchResults (Id, CvDocumentId, JobDocumentId, ResultJson, Score, CreatedAt) VALUES (@Id, @CvDocumentId, @JobDocumentId, @ResultJson, @Score, SYSUTCDATETIME()) """; await using var connection = new SqlConnection(_connectionString); await connection.OpenAsync(ct); await using var command = new SqlCommand(sql, connection); command.Parameters.AddWithValue("@Id", Guid.NewGuid().ToString("N")); command.Parameters.AddWithValue("@CvDocumentId", cvDocumentId); command.Parameters.AddWithValue("@JobDocumentId", jobDocumentId); command.Parameters.AddWithValue("@ResultJson", JsonSerializer.Serialize(response, new JsonSerializerOptions(JsonSerializerDefaults.Web))); command.Parameters.AddWithValue("@Score", response.Score); await command.ExecuteNonQueryAsync(ct); } public async Task GetChatCompletionAsync(string cacheKey, CancellationToken ct) { const string sql = "SELECT ResponseText FROM CvMatcherChatCache WHERE CacheKey = @CacheKey"; await using var connection = new SqlConnection(_connectionString); await connection.OpenAsync(ct); await using var command = new SqlCommand(sql, connection); command.Parameters.AddWithValue("@CacheKey", cacheKey); return await command.ExecuteScalarAsync(ct) as string; } public async Task SaveChatCompletionAsync(string cacheKey, string model, decimal temperature, string responseText, CancellationToken ct) { const string sql = """ IF NOT EXISTS (SELECT 1 FROM CvMatcherChatCache WHERE CacheKey = @CacheKey) INSERT INTO CvMatcherChatCache (CacheKey, Model, Temperature, ResponseText, CreatedAt) VALUES (@CacheKey, @Model, @Temperature, @ResponseText, SYSUTCDATETIME()) """; await using var connection = new SqlConnection(_connectionString); await connection.OpenAsync(ct); await using var command = new SqlCommand(sql, connection); command.Parameters.AddWithValue("@CacheKey", cacheKey); command.Parameters.AddWithValue("@Model", model); command.Parameters.AddWithValue("@Temperature", temperature); command.Parameters.AddWithValue("@ResponseText", responseText); await command.ExecuteNonQueryAsync(ct); } private async Task EnsureDatabaseExistsAsync(CancellationToken ct) { var builder = new SqlConnectionStringBuilder(_connectionString); var databaseName = builder.InitialCatalog; if (string.IsNullOrWhiteSpace(databaseName)) return; builder.InitialCatalog = "master"; await using var connection = new SqlConnection(builder.ConnectionString); await connection.OpenAsync(ct); var safeName = databaseName.Replace("]", "]]" ); await using var command = new SqlCommand($"IF DB_ID(@DatabaseName) IS NULL EXEC('CREATE DATABASE [{safeName}]')", connection); command.Parameters.AddWithValue("@DatabaseName", databaseName); await command.ExecuteNonQueryAsync(ct); } }