using Microsoft.Extensions.Configuration; namespace MeetingAssistant.LaunchProfiles; public interface ILaunchProfileOptionsProvider { LaunchProfile GetRequiredProfile(string? name); IReadOnlyList GetHotkeys(); } public sealed record LaunchProfile(string Name, MeetingAssistantOptions Options); public sealed record LaunchProfileHotkey(string ProfileName, string Toggle); public sealed class ConfigurationLaunchProfileOptionsProvider : ILaunchProfileOptionsProvider { public const string DefaultProfileName = "default"; private readonly IConfiguration configuration; public ConfigurationLaunchProfileOptionsProvider(IConfiguration configuration) { this.configuration = configuration; } public LaunchProfile GetRequiredProfile(string? name) { var profileName = NormalizeProfileName(name); var options = BindDefaultOptions(); if (profileName.Equals(DefaultProfileName, StringComparison.OrdinalIgnoreCase)) { return new LaunchProfile(DefaultProfileName, options); } var profileSection = GetProfilesSection().GetSection(profileName); if (!profileSection.Exists()) { throw new KeyNotFoundException($"Launch profile '{profileName}' is not configured."); } profileSection.Bind(options); ApplyArrayOverrides(profileSection, options); return new LaunchProfile(profileName, options); } public IReadOnlyList GetHotkeys() { var hotkeys = GetProfileNames() .Select(profileName => { var profile = GetRequiredProfile(profileName); return new LaunchProfileHotkey(profile.Name, profile.Options.Hotkey.Toggle); }) .Where(hotkey => !string.IsNullOrWhiteSpace(hotkey.Toggle)) .ToList(); var duplicate = hotkeys .GroupBy(hotkey => hotkey.Toggle.Trim(), StringComparer.OrdinalIgnoreCase) .FirstOrDefault(group => group.Count() > 1); if (duplicate is not null) { throw new InvalidOperationException( $"Launch profile hotkey '{duplicate.Key}' is configured more than once for profiles {string.Join(", ", duplicate.Select(hotkey => hotkey.ProfileName))}."); } return hotkeys; } private IReadOnlyList GetProfileNames() { return new[] { DefaultProfileName } .Concat(GetProfilesSection() .GetChildren() .Select(section => section.Key) .Where(key => !key.Equals(DefaultProfileName, StringComparison.OrdinalIgnoreCase))) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); } private MeetingAssistantOptions BindDefaultOptions() { var options = new MeetingAssistantOptions(); GetRootSection().Bind(options); return options; } private IConfigurationSection GetRootSection() { return configuration.GetSection("MeetingAssistant"); } private IConfigurationSection GetProfilesSection() { return GetRootSection().GetSection("LaunchProfiles"); } private static string NormalizeProfileName(string? name) { return string.IsNullOrWhiteSpace(name) || name.Equals(DefaultProfileName, StringComparison.OrdinalIgnoreCase) ? DefaultProfileName : name.Trim(); } private static void ApplyArrayOverrides( IConfigurationSection profileSection, MeetingAssistantOptions options) { var autoDetectLanguages = ReadArrayOverride(profileSection.GetSection("AzureSpeech:AutoDetectLanguages")); if (autoDetectLanguages is not null) { options.AzureSpeech.AutoDetectLanguages = autoDetectLanguages; } var funAsrChunkSize = ReadIntArrayOverride(profileSection.GetSection("FunAsr:ChunkSize")); if (funAsrChunkSize is not null) { options.FunAsr.ChunkSize = funAsrChunkSize; } } private static string[]? ReadArrayOverride(IConfigurationSection section) { if (!section.Exists()) { return null; } return section .GetChildren() .OrderBy(child => int.TryParse(child.Key, out var index) ? index : int.MaxValue) .Select(child => child.Value) .Where(value => !string.IsNullOrWhiteSpace(value)) .Select(value => value!) .ToArray(); } private static int[]? ReadIntArrayOverride(IConfigurationSection section) { if (!section.Exists()) { return null; } return section .GetChildren() .OrderBy(child => int.TryParse(child.Key, out var index) ? index : int.MaxValue) .Select(child => int.TryParse(child.Value, out var value) ? value : (int?)null) .Where(value => value.HasValue) .Select(value => value!.Value) .ToArray(); } }