using System.Collections.Concurrent; using System.Reflection; using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using SVSim.Database; using SVSim.Database.Models.Config; using SVSim.Database.Services; namespace SVSim.EmulatedEntrypoint.Services; /// /// Three-tier resolver for : GameConfigs row → IConfiguration /// section under "GameConfig:<name>" → T.ShippedDefaults() (via reflection) → new T(). /// /// Atomic per section: the first tier that has the section wins entirely; no per-property merging. /// Scoped lifetime — one DB read per request — matches today's GameConfigRoot behavior. /// /// public class GameConfigService : IGameConfigService { private static readonly ConcurrentDictionary _metaCache = new(); private readonly SVSimDbContext _db; private readonly IConfiguration _appSettings; public GameConfigService(SVSimDbContext db, IConfiguration appSettings) { _db = db; _appSettings = appSettings; } public T Get() where T : class, new() { var meta = GetMeta(typeof(T)); // Tier 1: DB row var row = _db.GameConfigs.AsNoTracking().FirstOrDefault(s => s.SectionName == meta.SectionName); if (row is not null) { return (T?)JsonSerializer.Deserialize(row.ValueJson, typeof(T)) ?? throw new InvalidOperationException( $"GameConfigs row '{meta.SectionName}' deserialised to null — corrupt jsonb?"); } // Tier 2: appsettings.json under "GameConfig:" var configSection = _appSettings.GetSection($"GameConfig:{meta.SectionName}"); if (configSection.Exists()) { var fromAppsettings = configSection.Get(); if (fromAppsettings is not null) return fromAppsettings; } // Tier 3: ShippedDefaults() if present, else parameterless ctor if (meta.ShippedDefaultsFactory is not null) { return (T)meta.ShippedDefaultsFactory.Invoke(null, null)!; } return new T(); } private static SectionMetadata GetMeta(Type t) => _metaCache.GetOrAdd(t, static type => { var attr = type.GetCustomAttribute(inherit: false) ?? throw new InvalidOperationException( $"{type.FullName} is not marked with [ConfigSection(...)] — IGameConfigService can't resolve it."); var factory = type.GetMethod("ShippedDefaults", BindingFlags.Public | BindingFlags.Static, binder: null, types: Type.EmptyTypes, modifiers: null); return new SectionMetadata(attr.Name, factory); }); private sealed record SectionMetadata(string SectionName, MethodInfo? ShippedDefaultsFactory); }