using StellaOps.Scheduler.Models; using StellaOps.Scheduler.Persistence.Postgres.Repositories; using System.Collections.Concurrent; using System.Collections.Immutable; namespace StellaOps.Scheduler.WebService.Schedules; internal sealed class InMemoryScheduleRepository : IScheduleRepository { private readonly ConcurrentDictionary _schedules = new(StringComparer.Ordinal); public Task UpsertAsync( Schedule schedule, CancellationToken cancellationToken = default) { _schedules[schedule.Id] = schedule; return Task.CompletedTask; } public Task GetAsync( string tenantId, string scheduleId, CancellationToken cancellationToken = default) { if (_schedules.TryGetValue(scheduleId, out var schedule) && string.Equals(schedule.TenantId, tenantId, StringComparison.Ordinal)) { return Task.FromResult(schedule); } return Task.FromResult(null); } public Task> ListAsync( string tenantId, ScheduleQueryOptions? options = null, CancellationToken cancellationToken = default) { options ??= new ScheduleQueryOptions(); var query = _schedules.Values .Where(schedule => string.Equals(schedule.TenantId, tenantId, StringComparison.Ordinal)); if (!options.IncludeDisabled) { query = query.Where(schedule => schedule.Enabled); } var result = query .OrderBy(schedule => schedule.Name, StringComparer.Ordinal) .Take(options.Limit ?? int.MaxValue) .ToArray(); return Task.FromResult>(result); } public Task SoftDeleteAsync( string tenantId, string scheduleId, string deletedBy, DateTimeOffset deletedAt, CancellationToken cancellationToken = default) { if (_schedules.TryGetValue(scheduleId, out var schedule) && string.Equals(schedule.TenantId, tenantId, StringComparison.Ordinal)) { _schedules.TryRemove(scheduleId, out _); return Task.FromResult(true); } return Task.FromResult(false); } } internal sealed class InMemoryRunSummaryService : IRunSummaryService { private readonly ConcurrentDictionary<(string TenantId, string ScheduleId), RunSummaryProjection> _summaries = new(); public Task ProjectAsync(Run run, CancellationToken cancellationToken = default) { var scheduleId = run.ScheduleId ?? string.Empty; var updatedAt = run.FinishedAt ?? run.StartedAt ?? run.CreatedAt; var counters = new RunSummaryCounters( Total: 0, Planning: 0, Queued: 0, Running: 0, Completed: 0, Error: 0, Cancelled: 0, TotalDeltas: 0, TotalNewCriticals: 0, TotalNewHigh: 0, TotalNewMedium: 0, TotalNewLow: 0); var projection = new RunSummaryProjection( run.TenantId, scheduleId, updatedAt, null, ImmutableArray.Empty, counters); _summaries[(run.TenantId, scheduleId)] = projection; return Task.FromResult(projection); } public Task GetAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default) { _summaries.TryGetValue((tenantId, scheduleId), out var projection); return Task.FromResult(projection); } public Task> ListAsync(string tenantId, CancellationToken cancellationToken = default) { var projections = _summaries.Values .Where(summary => string.Equals(summary.TenantId, tenantId, StringComparison.Ordinal)) .ToArray(); return Task.FromResult>(projections); } } internal sealed class InMemorySchedulerAuditService : ISchedulerAuditService { private readonly TimeProvider _timeProvider; private readonly StellaOps.Determinism.IGuidProvider _guidProvider; public InMemorySchedulerAuditService( TimeProvider? timeProvider = null, StellaOps.Determinism.IGuidProvider? guidProvider = null) { _timeProvider = timeProvider ?? TimeProvider.System; _guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance; } public Task WriteAsync(SchedulerAuditEvent auditEvent, CancellationToken cancellationToken = default) { var occurredAt = auditEvent.OccurredAt ?? _timeProvider.GetUtcNow(); var record = new AuditRecord( auditEvent.AuditId ?? $"audit_{_guidProvider.NewGuid():N}", auditEvent.TenantId, auditEvent.Category, auditEvent.Action, occurredAt, auditEvent.Actor, auditEvent.EntityId, auditEvent.ScheduleId, auditEvent.RunId, auditEvent.CorrelationId, auditEvent.Metadata?.ToImmutableSortedDictionary(StringComparer.OrdinalIgnoreCase) ?? ImmutableSortedDictionary.Empty, auditEvent.Message); return Task.FromResult(record); } }