using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FinlyticCore.Database; using FinlyticCore.Dtos.Settings; using FinlyticCore.Entities.Settings; using FinlyticCore.Models.Settings; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace FinlyticCore.Services; public interface ISettingsService { // --- 1. Typsicherer Zugriff über SettingKey (Empfohlen) --- Task GetSettingAsync(SettingKey key, CancellationToken cancellationToken = default); Task SetSettingAsync(SettingKey key, T value, CancellationToken cancellationToken = default); // --- 2. Dynamischer Zugriff über Enum-Key --- Task GetSettingAsync(TEnum enumKey, T defaultValue = default!, CancellationToken cancellationToken = default) where TEnum : struct, Enum; Task SetSettingAsync(TEnum enumKey, T value, CancellationToken cancellationToken = default) where TEnum : struct, Enum; // --- 3. Dynamischer Zugriff über String-Key --- Task GetSettingAsync(string key, T defaultValue = default!, CancellationToken cancellationToken = default); Task SetSettingAsync(string key, T value, CancellationToken cancellationToken = default); // --- 4. Reflection-Erkennung & Bulk-Verwaltung für Web UI / MQTT --- Task> GetAllRegisteredSettingsAsync(IEnumerable? customKeyHolders = null, CancellationToken cancellationToken = default); Task UpdateSettingsAsync(Dictionary updatedSettings, CancellationToken cancellationToken = default); } public class SettingsService : ISettingsService { private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger? _logger; // Fast In-Memory Cache: Key Schema: "KeyName" -> JSON string private readonly ConcurrentDictionary _cache = new(StringComparer.OrdinalIgnoreCase); public SettingsService( IServiceScopeFactory scopeFactory, ILogger? logger = null) { _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); _logger = logger; } #region SettingKey Overloads public Task GetSettingAsync(SettingKey key, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(key); return GetSettingInternalAsync(key.Name, key.DefaultValue, cancellationToken); } public Task SetSettingAsync(SettingKey key, T value, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(key); return SetSettingInternalAsync(key.Name, value, cancellationToken); } #endregion #region Enum-Key Overloads public Task GetSettingAsync(TEnum enumKey, T defaultValue = default!, CancellationToken cancellationToken = default) where TEnum : struct, Enum { var keyName = $"{typeof(TEnum).Name}.{enumKey}"; return GetSettingInternalAsync(keyName, defaultValue, cancellationToken); } public Task SetSettingAsync(TEnum enumKey, T value, CancellationToken cancellationToken = default) where TEnum : struct, Enum { var keyName = $"{typeof(TEnum).Name}.{enumKey}"; return SetSettingInternalAsync(keyName, value, cancellationToken); } #endregion #region String-Key Overloads public Task GetSettingAsync(string key, T defaultValue = default!, CancellationToken cancellationToken = default) { return GetSettingInternalAsync(key, defaultValue, cancellationToken); } public Task SetSettingAsync(string key, T value, CancellationToken cancellationToken = default) { return SetSettingInternalAsync(key, value, cancellationToken); } #endregion #region Bulk & Reflection Discovery public async Task> GetAllRegisteredSettingsAsync( IEnumerable? customKeyHolders = null, CancellationToken cancellationToken = default) { var isCustomScoped = customKeyHolders != null && customKeyHolders.Any(); var holderTypes = isCustomScoped ? customKeyHolders!.ToList() : new List { typeof(CoreSettingKeys) }; var resultList = new List(); var seenKeys = new HashSet(StringComparer.OrdinalIgnoreCase); // 1. Reflection auf allen SettingKey Feldern foreach (var type in holderTypes.Distinct()) { var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); foreach (var field in fields) { var fieldType = field.FieldType; if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(SettingKey<>)) { var valType = fieldType.GetGenericArguments()[0]; var settingKeyObj = field.GetValue(null); if (settingKeyObj == null) continue; var nameProp = fieldType.GetProperty("Name"); var defaultProp = fieldType.GetProperty("DefaultValue"); var keyName = nameProp?.GetValue(settingKeyObj)?.ToString() ?? field.Name; if (seenKeys.Contains(keyName)) continue; seenKeys.Add(keyName); var defVal = defaultProp?.GetValue(settingKeyObj); var typeName = MapToSimpleTypeName(valType); // Aktuellen Wert aus DB / Cache lesen var currentRawValue = await GetSettingInternalObjectAsync(keyName, valType, defVal, cancellationToken); resultList.Add(new DynamicSettingDto( Key: keyName, Value: currentRawValue, Type: typeName, Description: FormatDescriptionFromKey(keyName), UpdatedAt: DateTime.UtcNow )); } } } // 2. Prüfen, ob in der DB weitere gespeicherte Settings existieren (nur wenn nicht strikt auf custom KeyHolders begrenzt) if (!isCustomScoped) { try { await using var scope = _scopeFactory.CreateAsyncScope(); var dbContext = scope.ServiceProvider.GetService(); if (dbContext != null) { var dbSettings = await dbContext.DynamicSettings.AsNoTracking().ToListAsync(cancellationToken); foreach (var dbSetting in dbSettings) { if (!seenKeys.Contains(dbSetting.Key)) { seenKeys.Add(dbSetting.Key); var (inferredVal, inferredType) = InferJsonValueAndType(dbSetting.ValueJson); resultList.Add(new DynamicSettingDto( Key: dbSetting.Key, Value: inferredVal, Type: inferredType, Description: FormatDescriptionFromKey(dbSetting.Key), UpdatedAt: dbSetting.LastUpdatedUtc )); } } } } catch (Exception ex) { _logger?.LogWarning(ex, "[SettingsService] Error reading database settings during GetAllRegisteredSettingsAsync."); } } return resultList.OrderBy(s => s.Key).ToList(); } public async Task UpdateSettingsAsync(Dictionary updatedSettings, CancellationToken cancellationToken = default) { if (updatedSettings == null || updatedSettings.Count == 0) return; await using var scope = _scopeFactory.CreateAsyncScope(); var dbContext = scope.ServiceProvider.GetService(); foreach (var (key, rawValue) in updatedSettings) { if (string.IsNullOrWhiteSpace(key)) continue; string jsonValue = NormalizeJsonValue(rawValue); _cache[key] = jsonValue; if (dbContext != null) { var entity = await dbContext.DynamicSettings.FirstOrDefaultAsync(s => s.Key == key, cancellationToken); if (entity == null) { entity = new SettingEntity { Key = key, ValueJson = jsonValue, LastUpdatedUtc = DateTime.UtcNow }; dbContext.DynamicSettings.Add(entity); } else { entity.ValueJson = jsonValue; entity.LastUpdatedUtc = DateTime.UtcNow; } } } if (dbContext != null) { await dbContext.SaveChangesAsync(cancellationToken); } } #endregion #region Internal Engine Logic private async Task GetSettingInternalAsync(string key, T defaultValue, CancellationToken cancellationToken) { if (_cache.TryGetValue(key, out var cachedJson)) { return DeserializeValue(cachedJson, defaultValue); } try { await using var scope = _scopeFactory.CreateAsyncScope(); var dbContext = scope.ServiceProvider.GetService(); if (dbContext == null) { var defaultJson = NormalizeJsonValue(defaultValue); _cache[key] = defaultJson; return defaultValue; } var entity = await dbContext.DynamicSettings .FirstOrDefaultAsync(s => s.Key == key, cancellationToken); if (entity == null) { var defaultJson = NormalizeJsonValue(defaultValue); entity = new SettingEntity { Key = key, ValueJson = defaultJson, LastUpdatedUtc = DateTime.UtcNow }; dbContext.DynamicSettings.Add(entity); await dbContext.SaveChangesAsync(cancellationToken); _cache[key] = defaultJson; return defaultValue; } _cache[key] = entity.ValueJson; return DeserializeValue(entity.ValueJson, defaultValue); } catch (Exception ex) { _logger?.LogWarning(ex, "[SettingsService] Setting '{Key}' could not be loaded or initialized in DB. Using default value.", key); _cache[key] = NormalizeJsonValue(defaultValue); return defaultValue; } } private async Task GetSettingInternalObjectAsync(string key, Type valueType, object? defaultValue, CancellationToken cancellationToken) { if (_cache.TryGetValue(key, out var cachedJson)) { return DeserializeObject(cachedJson, valueType, defaultValue); } try { await using var scope = _scopeFactory.CreateAsyncScope(); var dbContext = scope.ServiceProvider.GetService(); if (dbContext == null) return defaultValue; var entity = await dbContext.DynamicSettings.FirstOrDefaultAsync(s => s.Key == key, cancellationToken); if (entity == null) { var defaultJson = NormalizeJsonValue(defaultValue); entity = new SettingEntity { Key = key, ValueJson = defaultJson, LastUpdatedUtc = DateTime.UtcNow }; dbContext.DynamicSettings.Add(entity); await dbContext.SaveChangesAsync(cancellationToken); _cache[key] = defaultJson; return defaultValue; } _cache[key] = entity.ValueJson; return DeserializeObject(entity.ValueJson, valueType, defaultValue); } catch { return defaultValue; } } private async Task SetSettingInternalAsync(string key, T value, CancellationToken cancellationToken) { var jsonValue = NormalizeJsonValue(value); _cache[key] = jsonValue; try { await using var scope = _scopeFactory.CreateAsyncScope(); var dbContext = scope.ServiceProvider.GetService(); if (dbContext == null) return; var entity = await dbContext.DynamicSettings .FirstOrDefaultAsync(s => s.Key == key, cancellationToken); if (entity == null) { entity = new SettingEntity { Key = key, ValueJson = jsonValue, LastUpdatedUtc = DateTime.UtcNow }; dbContext.DynamicSettings.Add(entity); } else { entity.ValueJson = jsonValue; entity.LastUpdatedUtc = DateTime.UtcNow; } await dbContext.SaveChangesAsync(cancellationToken); } catch (Exception ex) { _logger?.LogWarning(ex, "[SettingsService] Setting '{Key}' could not be saved to DB.", key); } } private static string NormalizeJsonValue(object? rawValue) { if (rawValue == null) return "null"; if (rawValue is JsonElement jsonElem) { if (jsonElem.ValueKind == JsonValueKind.String) { var str = jsonElem.GetString()?.Trim() ?? string.Empty; var unquoted = str.Trim('\"', ' '); if (bool.TryParse(unquoted, out var b)) return b ? "true" : "false"; if (long.TryParse(unquoted, out var l)) return l.ToString(); if (double.TryParse(unquoted, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) return d.ToString(CultureInfo.InvariantCulture); return JsonSerializer.Serialize(str); } return jsonElem.GetRawText(); } if (rawValue is string s) { var unquoted = s.Trim('\"', ' '); if (bool.TryParse(unquoted, out var b)) return b ? "true" : "false"; if (long.TryParse(unquoted, out var l)) return l.ToString(); if (double.TryParse(unquoted, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) return d.ToString(CultureInfo.InvariantCulture); return JsonSerializer.Serialize(s); } if (rawValue is bool bVal) return bVal ? "true" : "false"; if (rawValue is int or long or short or byte) return rawValue.ToString()!; if (rawValue is double or float or decimal) return Convert.ToString(rawValue, CultureInfo.InvariantCulture)!; return JsonSerializer.Serialize(rawValue); } private static T DeserializeValue(string json, T defaultValue) { if (string.IsNullOrWhiteSpace(json)) return defaultValue; try { var unquoted = json.Trim('\"', ' '); if (typeof(T) == typeof(bool)) { if (bool.TryParse(unquoted, out var b)) { return (T)(object)b; } } else if (typeof(T) == typeof(int)) { if (int.TryParse(unquoted, out var i)) { return (T)(object)i; } } else if (typeof(T) == typeof(long)) { if (long.TryParse(unquoted, out var l)) { return (T)(object)l; } } else if (typeof(T) == typeof(double)) { if (double.TryParse(unquoted, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) { return (T)(object)d; } } else if (typeof(T) == typeof(string)) { var trimmed = json.Trim(); if (trimmed.StartsWith("\"") && trimmed.EndsWith("\"") && trimmed.Length >= 2) { try { return (T)(object)(JsonSerializer.Deserialize(trimmed) ?? trimmed.Trim('\"')); } catch { return (T)(object)trimmed.Trim('\"'); } } return (T)(object)trimmed; } var result = JsonSerializer.Deserialize(json); return result ?? defaultValue; } catch { return defaultValue; } } private static object? DeserializeObject(string json, Type valueType, object? defaultValue) { if (string.IsNullOrWhiteSpace(json)) return defaultValue; try { var unquoted = json.Trim('\"', ' '); if (valueType == typeof(bool)) { if (bool.TryParse(unquoted, out var b)) return b; } else if (valueType == typeof(int)) { if (int.TryParse(unquoted, out var i)) return i; } else if (valueType == typeof(long)) { if (long.TryParse(unquoted, out var l)) return l; } else if (valueType == typeof(double)) { if (double.TryParse(unquoted, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) return d; } else if (valueType == typeof(string)) { var trimmed = json.Trim(); if (trimmed.StartsWith("\"") && trimmed.EndsWith("\"") && trimmed.Length >= 2) { try { return JsonSerializer.Deserialize(trimmed) ?? trimmed.Trim('\"'); } catch { return trimmed.Trim('\"'); } } return trimmed; } return JsonSerializer.Deserialize(json, valueType) ?? defaultValue; } catch { return defaultValue; } } private static string MapToSimpleTypeName(Type type) { if (type == typeof(bool)) return "bool"; if (type == typeof(int) || type == typeof(short) || type == typeof(byte) || type == typeof(long)) return "int"; if (type == typeof(double) || type == typeof(float) || type == typeof(decimal)) return "double"; return "string"; } private static (object? Value, string Type) InferJsonValueAndType(string json) { if (string.IsNullOrWhiteSpace(json)) return (string.Empty, "string"); try { var unquoted = json.Trim('\"', ' '); if (bool.TryParse(unquoted, out var b)) return (b, "bool"); if (long.TryParse(unquoted, out var l)) return (l, "int"); if (double.TryParse(unquoted, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) return (d, "double"); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; return root.ValueKind switch { JsonValueKind.True => (true, "bool"), JsonValueKind.False => (false, "bool"), JsonValueKind.Number when root.TryGetInt64(out var i) => (i, "int"), JsonValueKind.Number => (root.GetDouble(), "double"), JsonValueKind.String => (root.GetString(), "string"), _ => (json, "string") }; } catch { return (json, "string"); } } private static string FormatDescriptionFromKey(string key) { if (key.StartsWith("Logging.Channel.", StringComparison.OrdinalIgnoreCase)) { return $"Logging-Kanal für {key.Substring(16)} (Ein/Aus)"; } if (key.StartsWith("Feature.", StringComparison.OrdinalIgnoreCase)) { return $"Feature-Toggle für {key.Substring(8)}"; } if (key.StartsWith("Cache.", StringComparison.OrdinalIgnoreCase)) { return $"Cache-Konfiguration ({key.Substring(6)})"; } if (key.StartsWith("Scraper.", StringComparison.OrdinalIgnoreCase)) { return $"Scraper-Konfiguration ({key.Substring(8)})"; } return key; } #endregion }