using System.Text.Json; using Intendant.Shared.Services; namespace Intendant.Web.Services; public sealed class OpenAiSettingsService : IOpenAiSettingsService { private readonly SemaphoreSlim _lock = new(1, 1); private readonly OpenAiResponsesOptions _defaults; private readonly string _filePath; private OpenAiResponsesOptions? _cache; public OpenAiSettingsService(IConfiguration configuration, IWebHostEnvironment environment) { _defaults = configuration.GetSection(OpenAiResponsesOptions.SectionName).Get() ?? new OpenAiResponsesOptions(); string directoryPath = Path.Combine(environment.ContentRootPath, "App_Data"); _filePath = Path.Combine(directoryPath, "openai-settings.json"); } public async Task GetAsync(CancellationToken cancellationToken = default) { await _lock.WaitAsync(cancellationToken); try { if (_cache is not null) { return Clone(_cache); } if (!File.Exists(_filePath)) { _cache = Clone(_defaults); return Clone(_cache); } await using FileStream stream = File.OpenRead(_filePath); OpenAiResponsesOptions? stored = await JsonSerializer.DeserializeAsync(stream, cancellationToken: cancellationToken); _cache = Normalize(stored ?? new OpenAiResponsesOptions()); return Clone(_cache); } finally { _lock.Release(); } } public async Task SaveAsync(OpenAiResponsesOptions options, CancellationToken cancellationToken = default) { OpenAiResponsesOptions normalized = Normalize(options); await _lock.WaitAsync(cancellationToken); try { string? directory = Path.GetDirectoryName(_filePath); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } await using FileStream stream = File.Create(_filePath); await JsonSerializer.SerializeAsync(stream, normalized, cancellationToken: cancellationToken); _cache = Clone(normalized); } finally { _lock.Release(); } } private OpenAiResponsesOptions Normalize(OpenAiResponsesOptions options) { return new OpenAiResponsesOptions { ApiKey = options.ApiKey?.Trim() ?? string.Empty, Endpoint = options.Endpoint?.Trim() ?? string.Empty, Model = string.IsNullOrWhiteSpace(options.Model) ? _defaults.Model : options.Model.Trim(), }; } private static OpenAiResponsesOptions Clone(OpenAiResponsesOptions source) { return new OpenAiResponsesOptions { ApiKey = source.ApiKey, Endpoint = source.Endpoint, Model = source.Model, }; } }