using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AutopilotMonitor.Shared.DataAccess; using AutopilotMonitor.Shared.Models; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace AutopilotMonitor.Functions.Services { /// /// Service for managing global admin configuration. /// Caching and business logic layer — delegates storage to IConfigRepository. /// public class AdminConfigurationService { private readonly IConfigRepository _configRepo; private readonly ILogger _logger; private readonly IMemoryCache _cache; private const string CacheKey = "admin-config"; private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); public AdminConfigurationService(IConfigRepository configRepo, ILogger logger, IMemoryCache cache) { _configRepo = configRepo; _logger = logger; _cache = cache; } /// /// Gets global admin configuration (uses cache with 5-minute TTL). /// /// Virtual so it can be mocked via Moq in consumer tests that need controlled /// flag values (e.g. IndexReconcileTimer flag-gate tests). /// /// public virtual async Task GetConfigurationAsync() { if (_cache.TryGetValue(CacheKey, out AdminConfiguration? cachedConfig) && cachedConfig != null) { return cachedConfig; } try { // Load from repository var config = await _configRepo.GetAdminConfigurationAsync(); if (config != null) { _cache.Set(CacheKey, config, CacheDuration); return config; } // Configuration not found - create and save default immediately _logger.LogInformation("Admin configuration not found, creating and saving default configuration"); var defaultConfig = AdminConfiguration.CreateDefault(); try { // Save the default configuration via repository await _configRepo.SaveAdminConfigurationAsync(defaultConfig); _cache.Set(CacheKey, defaultConfig, CacheDuration); _logger.LogInformation("Default admin configuration created and saved"); } catch (Exception saveEx) { _logger.LogError(saveEx, "Failed to save default admin configuration"); } return defaultConfig; } catch (Exception ex) { _logger.LogError(ex, "Error loading admin configuration"); // Return default on error (fail-open) return AdminConfiguration.CreateDefault(); } } /// /// Cache-bypassing read of /// (plan §5 PR5 finding 1). The general-purpose /// caches for 5 minutes per instance; that's wrong for an *emergency* switch — a /// flip-ON would not be honored across scaled-out Function-host instances until each /// one's cache expires independently. This helper goes straight to the repository so /// every call observes the current row. /// /// Fail-CLOSED on storage error: if the repo throws, we return true /// (treat as kill-switch active). For an emergency switch, blocking new deletes on a /// transient storage failure is the safe default; the alternative would be silently /// failing to honor an in-progress operator action during the incident the switch was /// flipped to mitigate. /// /// /// Virtual so tests can mock it without going through the repository. /// /// public virtual async Task IsSessionDeletionKillSwitchActiveAsync() { try { var config = await _configRepo.GetAdminConfigurationAsync(); return config?.SessionDeletionKillSwitch ?? false; } catch (Exception ex) { _logger.LogError(ex, "Failed to read SessionDeletionKillSwitch from repository; failing CLOSED (treating as active) — plan §5 PR5 finding 1"); return true; } } /// /// Saves global admin configuration and syncs rate limit to all tenant configurations /// public async Task SaveConfigurationAsync(AdminConfiguration config) { if (config == null) { throw new ArgumentException("Configuration is required"); } try { config.LastUpdated = DateTime.UtcNow; await _configRepo.SaveAdminConfigurationAsync(config); // Invalidate cache _cache.Remove(CacheKey); _logger.LogInformation($"Admin configuration saved by {config.UpdatedBy}"); // NOTE: Rate limits are no longer mirrored into every tenant row. The effective // per-tenant limit is computed at read time as `tenantOverride ?? global` // (see SecurityValidator for the device path and UserRateLimitMiddleware for the // user path). This removes the former background sync job — and the clobbering // foot-gun where a per-tenant edit to the base field was overwritten on the next // global save. } catch (Exception ex) { _logger.LogError(ex, "Error saving admin configuration"); throw; } } /// /// Invalidates cache (forces reload on next request) /// public void InvalidateCache() { _cache.Remove(CacheKey); } } }