using Npgsql; using Backend.Interface; namespace Backend.DatabaseHandler; public sealed class PostgresUserInterestStore : IUserInterestStore { private readonly string _connectionString; public PostgresUserInterestStore(IConfiguration configuration) { _connectionString = configuration.GetConnectionString("MarketDataDb") ?? throw new InvalidOperationException("Missing connection string: MarketDataDb"); } public async Task> FetchAllAsync(string user, string type) { ValidateUser(user); string normalizedType = NormalizeType(type); string sql = normalizedType == "symbol" ? """ SELECT symbol FROM user_interest_symbols WHERE username = @user ORDER BY symbol ASC; """ : """ SELECT topic FROM user_interest_topics WHERE username = @user ORDER BY topic ASC; """; var results = new List(); await using var conn = new NpgsqlConnection(_connectionString); await conn.OpenAsync(); await using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("user", user.Trim()); await using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { results.Add(reader.GetString(0)); } return results; } public async Task> FetchAllDistinctValuesAsync(CancellationToken cancellationToken = default) { const string sql = """ SELECT symbol AS value FROM user_interest_symbols UNION SELECT topic AS value FROM user_interest_topics; """; var results = new List(); await using var conn = new NpgsqlConnection(_connectionString); await conn.OpenAsync(cancellationToken); await using var cmd = new NpgsqlCommand(sql, conn); await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { results.Add(reader.GetString(0)); } return results; } public async Task AddAsync(string user, string type, string value) { ValidateUser(user); ValidateValue(value); string normalizedType = NormalizeType(type); string normalizedValue = normalizedType == "symbol" ? value.Trim().ToUpperInvariant() : value.Trim(); string sql = normalizedType == "symbol" ? """ INSERT INTO user_interest_symbols (username, symbol) VALUES (@user, @value) ON CONFLICT (username, symbol) DO NOTHING; """ : """ INSERT INTO user_interest_topics (username, topic) VALUES (@user, @value) ON CONFLICT (username, topic) DO NOTHING; """; await using var conn = new NpgsqlConnection(_connectionString); await conn.OpenAsync(); await using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("user", user.Trim()); cmd.Parameters.AddWithValue("value", normalizedValue); int rows = await cmd.ExecuteNonQueryAsync(); return rows > 0; } public async Task RemoveAsync(string user, string type, string value) { ValidateUser(user); ValidateValue(value); string normalizedType = NormalizeType(type); string normalizedValue = normalizedType == "symbol" ? value.Trim().ToUpperInvariant() : value.Trim(); string sql = normalizedType == "symbol" ? """ DELETE FROM user_interest_symbols WHERE username = @user AND symbol = @value; """ : """ DELETE FROM user_interest_topics WHERE username = @user AND topic = @value; """; await using var conn = new NpgsqlConnection(_connectionString); await conn.OpenAsync(); await using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("user", user.Trim()); cmd.Parameters.AddWithValue("value", normalizedValue); int rows = await cmd.ExecuteNonQueryAsync(); return rows > 0; } private static string NormalizeType(string type) { string normalized = type.Trim().ToLowerInvariant(); return normalized switch { "symbol" => "symbol", "topic" => "topic", _ => throw new ArgumentException("Type must be either 'symbol' or 'topic'.") }; } private static void ValidateUser(string user) { if (string.IsNullOrWhiteSpace(user)) { throw new ArgumentException("User cannot be empty."); } } private static void ValidateValue(string value) { if (string.IsNullOrWhiteSpace(value)) { throw new ArgumentException("Value cannot be empty."); } } }