using Microsoft.Extensions.Logging; using StellaOps.Plugin.Abstractions; using StellaOps.Plugin.Abstractions.Execution; using StellaOps.Plugin.Abstractions.Health; using StellaOps.Plugin.Abstractions.Lifecycle; using StellaOps.Plugin.Registry.Models; using System.Collections.Concurrent; using System.Text.Json; namespace StellaOps.Plugin.Registry; /// /// In-memory implementation of the plugin registry for testing and development. /// public sealed class InMemoryPluginRegistry : IPluginRegistry { private readonly ConcurrentDictionary _plugins = new(); private readonly ConcurrentDictionary _capabilities = new(); private readonly ConcurrentDictionary _instances = new(); private readonly ConcurrentDictionary> _healthHistory = new(); private readonly ILogger _logger; private readonly TimeProvider _timeProvider; /// /// Creates a new in-memory plugin registry instance. /// public InMemoryPluginRegistry( ILogger logger, TimeProvider timeProvider) { _logger = logger; _timeProvider = timeProvider; } /// public Task RegisterAsync(LoadedPlugin plugin, CancellationToken ct) { var now = _timeProvider.GetUtcNow(); var id = Guid.NewGuid(); // Get current health status from last check result if available var healthStatus = plugin.LastHealthCheckResult?.Status ?? HealthStatus.Unknown; var record = new PluginRecord { Id = id, PluginId = plugin.Info.Id, Name = plugin.Info.Name, Version = plugin.Info.Version, Vendor = plugin.Info.Vendor, Description = plugin.Info.Description, LicenseId = plugin.Info.LicenseId, TrustLevel = plugin.TrustLevel, Capabilities = plugin.Capabilities, Status = plugin.State, HealthStatus = healthStatus, Source = "installed", AssemblyPath = plugin.Manifest?.AssemblyPath, EntryPoint = plugin.Manifest?.EntryPoint, CreatedAt = now, UpdatedAt = now, LoadedAt = plugin.LoadedAt }; _plugins[id] = record; _logger.LogDebug("Registered plugin {PluginId} with ID {Id}", plugin.Info.Id, id); return Task.FromResult(record); } /// public Task UpdateStatusAsync(string pluginId, PluginLifecycleState status, string? message = null, CancellationToken ct = default) { var record = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId); if (record != null) { var updated = new PluginRecord { Id = record.Id, PluginId = record.PluginId, Name = record.Name, Version = record.Version, Vendor = record.Vendor, Description = record.Description, LicenseId = record.LicenseId, TrustLevel = record.TrustLevel, Capabilities = record.Capabilities, Status = status, StatusMessage = message, HealthStatus = record.HealthStatus, LastHealthCheck = record.LastHealthCheck, HealthCheckFailures = record.HealthCheckFailures, Source = record.Source, AssemblyPath = record.AssemblyPath, EntryPoint = record.EntryPoint, Manifest = record.Manifest, CreatedAt = record.CreatedAt, UpdatedAt = _timeProvider.GetUtcNow(), LoadedAt = record.LoadedAt }; _plugins[record.Id] = updated; } return Task.CompletedTask; } /// public Task UpdateHealthAsync(string pluginId, HealthStatus status, HealthCheckResult? result = null, CancellationToken ct = default) { var record = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId); if (record != null) { var now = _timeProvider.GetUtcNow(); var updated = new PluginRecord { Id = record.Id, PluginId = record.PluginId, Name = record.Name, Version = record.Version, Vendor = record.Vendor, Description = record.Description, LicenseId = record.LicenseId, TrustLevel = record.TrustLevel, Capabilities = record.Capabilities, Status = record.Status, StatusMessage = record.StatusMessage, HealthStatus = status, LastHealthCheck = now, HealthCheckFailures = status == HealthStatus.Healthy ? 0 : record.HealthCheckFailures + 1, Source = record.Source, AssemblyPath = record.AssemblyPath, EntryPoint = record.EntryPoint, Manifest = record.Manifest, CreatedAt = record.CreatedAt, UpdatedAt = now, LoadedAt = record.LoadedAt }; _plugins[record.Id] = updated; if (result != null) { RecordHealthCheckAsync(pluginId, result, ct).GetAwaiter().GetResult(); } } return Task.CompletedTask; } /// public Task UnregisterAsync(string pluginId, CancellationToken ct) { var record = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId); if (record != null) { _plugins.TryRemove(record.Id, out _); // Remove capabilities var capsToRemove = _capabilities.Values.Where(c => c.PluginId == record.Id).ToList(); foreach (var cap in capsToRemove) { _capabilities.TryRemove(cap.Id, out _); } // Remove instances var instancesToRemove = _instances.Values.Where(i => i.PluginId == record.Id).ToList(); foreach (var instance in instancesToRemove) { _instances.TryRemove(instance.Id, out _); } _logger.LogDebug("Unregistered plugin {PluginId}", pluginId); } return Task.CompletedTask; } /// public Task GetAsync(string pluginId, CancellationToken ct) { var record = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId); return Task.FromResult(record); } /// public Task> GetAllAsync(CancellationToken ct) { var result = _plugins.Values.OrderBy(p => p.Name).ToList(); return Task.FromResult>(result); } /// public Task> GetByStatusAsync(PluginLifecycleState status, CancellationToken ct) { var result = _plugins.Values .Where(p => p.Status == status) .OrderBy(p => p.Name) .ToList(); return Task.FromResult>(result); } /// public Task> GetByCapabilityAsync(PluginCapabilities capability, CancellationToken ct) { var result = _plugins.Values .Where(p => (p.Capabilities & capability) != 0 && p.Status == PluginLifecycleState.Active) .OrderBy(p => p.Name) .ToList(); return Task.FromResult>(result); } /// public Task> GetByCapabilityTypeAsync(string capabilityType, string? capabilityId = null, CancellationToken ct = default) { var matchingCaps = _capabilities.Values .Where(c => c.CapabilityType == capabilityType && c.IsEnabled) .Where(c => capabilityId == null || c.CapabilityId == capabilityId); var pluginIds = matchingCaps.Select(c => c.PluginId).ToHashSet(); var result = _plugins.Values .Where(p => pluginIds.Contains(p.Id) && p.Status == PluginLifecycleState.Active) .OrderBy(p => p.Name) .ToList(); return Task.FromResult>(result); } /// public Task RegisterCapabilitiesAsync(Guid pluginDbId, IEnumerable capabilities, CancellationToken ct) { foreach (var cap in capabilities) { var record = cap with { PluginId = pluginDbId }; _capabilities[cap.Id] = record; } return Task.CompletedTask; } /// public Task> GetCapabilitiesAsync(string pluginId, CancellationToken ct) { var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId); if (plugin == null) { return Task.FromResult>([]); } var result = _capabilities.Values .Where(c => c.PluginId == plugin.Id) .OrderBy(c => c.CapabilityType) .ThenBy(c => c.CapabilityId) .ToList(); return Task.FromResult>(result); } /// public Task CreateInstanceAsync(CreatePluginInstanceRequest request, CancellationToken ct) { var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == request.PluginId) ?? throw new InvalidOperationException($"Plugin {request.PluginId} not found"); var now = _timeProvider.GetUtcNow(); var id = Guid.NewGuid(); var record = new PluginInstanceRecord { Id = id, PluginId = plugin.Id, PluginStringId = request.PluginId, TenantId = request.TenantId, InstanceName = request.InstanceName, Config = request.Config, SecretsPath = request.SecretsPath, ResourceLimits = request.ResourceLimits, Enabled = true, Status = "pending", CreatedAt = now, UpdatedAt = now }; _instances[id] = record; _logger.LogDebug("Created instance {InstanceId} for plugin {PluginId}", id, request.PluginId); return Task.FromResult(record); } /// public Task GetInstanceAsync(Guid instanceId, CancellationToken ct) { _instances.TryGetValue(instanceId, out var record); return Task.FromResult(record); } /// public Task> GetInstancesForTenantAsync(Guid tenantId, CancellationToken ct) { var result = _instances.Values .Where(i => i.TenantId == tenantId) .OrderBy(i => i.CreatedAt) .ToList(); return Task.FromResult>(result); } /// public Task> GetInstancesForPluginAsync(string pluginId, CancellationToken ct) { var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId); if (plugin == null) { return Task.FromResult>([]); } var result = _instances.Values .Where(i => i.PluginId == plugin.Id) .OrderBy(i => i.CreatedAt) .ToList(); return Task.FromResult>(result); } /// public Task UpdateInstanceConfigAsync(Guid instanceId, JsonDocument config, CancellationToken ct) { if (_instances.TryGetValue(instanceId, out var existing)) { var updated = existing with { Config = config, UpdatedAt = _timeProvider.GetUtcNow() }; _instances[instanceId] = updated; } return Task.CompletedTask; } /// public Task SetInstanceEnabledAsync(Guid instanceId, bool enabled, CancellationToken ct) { if (_instances.TryGetValue(instanceId, out var existing)) { var updated = existing with { Enabled = enabled, UpdatedAt = _timeProvider.GetUtcNow() }; _instances[instanceId] = updated; } return Task.CompletedTask; } /// public Task DeleteInstanceAsync(Guid instanceId, CancellationToken ct) { _instances.TryRemove(instanceId, out _); return Task.CompletedTask; } /// public Task RecordHealthCheckAsync(string pluginId, HealthCheckResult result, CancellationToken ct) { var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId); if (plugin == null) return Task.CompletedTask; var now = _timeProvider.GetUtcNow(); var healthRecord = new PluginHealthRecord { Id = Guid.NewGuid(), PluginId = plugin.Id, PluginStringId = pluginId, CheckedAt = now, Status = result.Status, ResponseTimeMs = result.Duration.HasValue ? (int)result.Duration.Value.TotalMilliseconds : null, ErrorMessage = result.Message, CreatedAt = now }; _healthHistory.AddOrUpdate( plugin.Id, _ => [healthRecord], (_, list) => { list.Add(healthRecord); return list; }); return Task.CompletedTask; } /// public Task> GetHealthHistoryAsync( string pluginId, DateTimeOffset since, int limit = 100, CancellationToken ct = default) { var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId); if (plugin == null || !_healthHistory.TryGetValue(plugin.Id, out var history)) { return Task.FromResult>([]); } var result = history .Where(h => h.CheckedAt >= since) .OrderByDescending(h => h.CheckedAt) .Take(limit) .ToList(); return Task.FromResult>(result); } /// /// Clears all data from the registry. For testing purposes. /// public void Clear() { _plugins.Clear(); _capabilities.Clear(); _instances.Clear(); _healthHistory.Clear(); } }