using System.Text.Json; namespace StellaOps.Concelier.WebService.Services; internal static class IncidentFileStore { private static readonly JsonSerializerOptions SerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false, }; public static string GetIncidentFilePath(ConcelierOptions.EvidenceBundleOptions evidenceOptions, string tenant, string advisoryKey) { ArgumentNullException.ThrowIfNull(evidenceOptions); ArgumentNullException.ThrowIfNull(tenant); ArgumentNullException.ThrowIfNull(advisoryKey); var root = evidenceOptions.RootAbsolute ?? evidenceOptions.Root ?? string.Empty; return Path.Combine(root, tenant.Trim(), advisoryKey.Trim(), "incident.json"); } public static async Task WriteAsync( ConcelierOptions.EvidenceBundleOptions evidenceOptions, string tenant, string advisoryKey, string reason, int cooldownMinutes, string? pipelineVersion, DateTimeOffset now, CancellationToken cancellationToken) { var path = GetIncidentFilePath(evidenceOptions, tenant, advisoryKey); Directory.CreateDirectory(Path.GetDirectoryName(path)!); var activatedAt = now.ToUniversalTime(); var cooldownUntil = activatedAt.AddMinutes(cooldownMinutes); var payload = new IncidentFile { AdvisoryKey = advisoryKey.Trim(), Tenant = tenant.Trim(), Reason = string.IsNullOrWhiteSpace(reason) ? "unspecified" : reason.Trim(), ActivatedAt = activatedAt, CooldownUntil = cooldownUntil, PipelineVersion = pipelineVersion, }; var json = JsonSerializer.Serialize(payload, SerializerOptions); await File.WriteAllTextAsync(path, json, cancellationToken).ConfigureAwait(false); } public static async Task ReadAsync( ConcelierOptions.EvidenceBundleOptions evidenceOptions, string tenant, string advisoryKey, DateTimeOffset now, CancellationToken cancellationToken) { var path = GetIncidentFilePath(evidenceOptions, tenant, advisoryKey); if (!File.Exists(path)) { return null; } await using var stream = File.OpenRead(path); var payload = await JsonSerializer.DeserializeAsync(stream, SerializerOptions, cancellationToken).ConfigureAwait(false); if (payload is null) { return null; } var active = payload.CooldownUntil > now.ToUniversalTime(); return new IncidentStatusResponse( payload.AdvisoryKey, payload.Tenant, payload.Reason, payload.ActivatedAt.ToUniversalTime().ToString("O"), payload.CooldownUntil.ToUniversalTime().ToString("O"), payload.PipelineVersion, active); } public static Task DeleteAsync(ConcelierOptions.EvidenceBundleOptions evidenceOptions, string tenant, string advisoryKey, CancellationToken cancellationToken) { var path = GetIncidentFilePath(evidenceOptions, tenant, advisoryKey); if (File.Exists(path)) { File.Delete(path); } return Task.CompletedTask; } private sealed record IncidentFile { public string AdvisoryKey { get; init; } = string.Empty; public string Tenant { get; init; } = string.Empty; public string Reason { get; init; } = "unspecified"; public DateTimeOffset ActivatedAt { get; init; } public DateTimeOffset CooldownUntil { get; init; } public string? PipelineVersion { get; init; } } }