using System.Text.Json; using StellaOps.TimelineIndexer.Core.Models; namespace StellaOps.TimelineIndexer.Infrastructure.Subscriptions; /// /// Normalises incoming orchestrator/notification envelopes into instances. /// public sealed class TimelineEnvelopeParser { private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) { WriteIndented = false, PropertyNameCaseInsensitive = true }; public bool TryParse(string rawJson, out TimelineEventEnvelope envelope, out string? failureReason) { if (string.IsNullOrWhiteSpace(rawJson)) { envelope = default!; failureReason = "Payload was empty"; return false; } try { using var doc = JsonDocument.Parse(rawJson); var root = doc.RootElement; var eventId = FirstString(root, "eventId", "event_id", "id", "messageId"); var tenantId = FirstString(root, "tenant", "tenantId", "tenant_id"); var eventType = FirstString(root, "kind", "eventType", "event_type", "type"); var source = FirstString(root, "source", "producer") ?? "unknown"; var correlationId = FirstString(root, "correlationId", "correlation_id"); var traceId = FirstString(root, "traceId", "trace_id"); var actor = ExtractActor(root); var severity = (FirstString(root, "severity") ?? "info").ToLowerInvariant(); var occurredAt = FirstDateTime(root, "occurredAt", "occurred_at", "timestamp", "ts") ?? DateTimeOffset.UtcNow; var normalizedPayload = ExtractNormalizedPayload(root); var attributes = ExtractAttributes(root); envelope = new TimelineEventEnvelope { EventId = eventId ?? throw new InvalidOperationException("event_id is required"), TenantId = tenantId ?? throw new InvalidOperationException("tenant_id is required"), EventType = eventType ?? throw new InvalidOperationException("event_type is required"), Source = source, OccurredAt = occurredAt, CorrelationId = correlationId, TraceId = traceId, Actor = actor, Severity = severity, RawPayloadJson = JsonSerializer.Serialize(root, SerializerOptions), NormalizedPayloadJson = normalizedPayload, Attributes = attributes, BundleDigest = FirstString(root, "bundleDigest"), BundleId = FirstGuid(root, "bundleId"), AttestationSubject = FirstString(root, "attestationSubject"), AttestationDigest = FirstString(root, "attestationDigest"), ManifestUri = FirstString(root, "manifestUri") }; failureReason = null; return true; } catch (Exception ex) { envelope = default!; failureReason = ex.Message; return false; } } private static string? ExtractActor(JsonElement root) { if (TryGetProperty(root, "actor", out var actorElement)) { if (actorElement.ValueKind == JsonValueKind.String) { return actorElement.GetString(); } if (actorElement.ValueKind == JsonValueKind.Object) { if (actorElement.TryGetProperty("subject", out var subject) && subject.ValueKind == JsonValueKind.String) { return subject.GetString(); } if (actorElement.TryGetProperty("user", out var user) && user.ValueKind == JsonValueKind.String) { return user.GetString(); } } } return null; } private static string? ExtractNormalizedPayload(JsonElement root) { if (TryGetProperty(root, "payload", out var payload)) { return JsonSerializer.Serialize(payload, SerializerOptions); } if (TryGetProperty(root, "data", out var data) && data.ValueKind is JsonValueKind.Object) { return JsonSerializer.Serialize(data, SerializerOptions); } return null; } private static IDictionary? ExtractAttributes(JsonElement root) { if (!TryGetProperty(root, "attributes", out var attributes) || attributes.ValueKind != JsonValueKind.Object) { return null; } var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var property in attributes.EnumerateObject()) { var value = property.Value.ValueKind switch { JsonValueKind.String => property.Value.GetString(), JsonValueKind.Number => property.Value.ToString(), JsonValueKind.True => "true", JsonValueKind.False => "false", _ => null }; if (!string.IsNullOrWhiteSpace(value)) { dict[property.Name] = value!; } } return dict.Count == 0 ? null : dict; } private static bool TryGetProperty(JsonElement root, string name, out JsonElement value) { if (root.TryGetProperty(name, out value)) { return true; } foreach (var property in root.EnumerateObject()) { if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) { value = property.Value; return true; } } value = default; return false; } private static string? FirstString(JsonElement root, params string[] names) { foreach (var name in names) { if (TryGetProperty(root, name, out var value) && value.ValueKind == JsonValueKind.String) { var str = value.GetString(); if (!string.IsNullOrWhiteSpace(str)) { return str; } } } return null; } private static Guid? FirstGuid(JsonElement root, params string[] names) { var text = FirstString(root, names); if (Guid.TryParse(text, out var guid)) { return guid; } return null; } private static DateTimeOffset? FirstDateTime(JsonElement root, params string[] names) { foreach (var name in names) { if (TryGetProperty(root, name, out var value) && value.ValueKind == JsonValueKind.String) { var text = value.GetString(); if (!string.IsNullOrWhiteSpace(text) && DateTimeOffset.TryParse(text, out var dto)) { return dto; } } } return null; } }