using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using IronTelemetry.Client; namespace IronServices.Maui.Controls; #region Enums /// /// Display status for a user journey. /// public enum JourneyDisplayStatus { InProgress, Completed, Failed, Abandoned } /// /// Display status for a step within a journey. /// public enum StepDisplayStatus { InProgress, Completed, Failed, Skipped } /// /// View mode for the UserJourneyView control. /// public enum JourneyViewMode { /// /// Vertical timeline with nested steps shown with indentation. /// Timeline, /// /// Expandable tree hierarchy with collapsible nodes. /// Tree, /// /// Horizontal flowchart-style diagram showing step progression. /// Flow } #endregion #region JourneyItem /// /// Represents a user journey for display in UserJourneyView. /// public class JourneyItem : INotifyPropertyChanged { private bool _isExpanded; private bool _isSelected; /// /// Unique journey identifier. /// public string JourneyId { get; set; } = ""; /// /// Journey name (e.g., "Checkout Flow", "User Onboarding"). /// public string Name { get; set; } = ""; /// /// Associated user ID. /// public string? UserId { get; set; } /// /// Associated user email. /// public string? UserEmail { get; set; } /// /// Current journey status. /// public JourneyDisplayStatus Status { get; set; } = JourneyDisplayStatus.InProgress; /// /// When the journey started. /// public DateTime StartTime { get; set; } /// /// When the journey ended (null if still in progress). /// public DateTime? EndTime { get; set; } /// /// Duration in milliseconds. /// public double? DurationMs { get; set; } /// /// Custom metadata attached to the journey. /// public Dictionary Metadata { get; set; } = new(); /// /// Top-level steps in this journey. /// public ObservableCollection Steps { get; } = new(); /// /// Breadcrumbs captured during this journey. /// public List Breadcrumbs { get; set; } = new(); /// /// Exceptions captured during this journey. /// public List Exceptions { get; set; } = new(); /// /// UI state: whether this journey is expanded in tree view. /// public bool IsExpanded { get => _isExpanded; set { _isExpanded = value; OnPropertyChanged(); } } /// /// UI state: whether this journey is currently selected. /// public bool IsSelected { get => _isSelected; set { _isSelected = value; OnPropertyChanged(); } } #region Computed Properties /// /// Formatted duration for display. /// public string DurationDisplay { get { if (!DurationMs.HasValue) return "In Progress"; if (DurationMs.Value < 1000) return $"{DurationMs:F0}ms"; if (DurationMs.Value < 60000) return $"{DurationMs.Value / 1000:F1}s"; return $"{DurationMs.Value / 60000:F1}m"; } } /// /// Relative time since journey started. /// public string TimeAgo { get { var span = DateTime.UtcNow - StartTime; if (span.TotalMinutes < 1) return "just now"; if (span.TotalMinutes < 60) return $"{(int)span.TotalMinutes}m ago"; if (span.TotalHours < 24) return $"{(int)span.TotalHours}h ago"; if (span.TotalDays < 7) return $"{(int)span.TotalDays}d ago"; return StartTime.ToLocalTime().ToString("MMM d"); } } /// /// Status text for display. /// public string StatusDisplay => Status switch { JourneyDisplayStatus.InProgress => "In Progress", JourneyDisplayStatus.Completed => "Completed", JourneyDisplayStatus.Failed => "Failed", JourneyDisplayStatus.Abandoned => "Abandoned", _ => "Unknown" }; /// /// Color for status badge. /// public Color StatusColor => Status switch { JourneyDisplayStatus.InProgress => Colors.Blue, JourneyDisplayStatus.Completed => Colors.Green, JourneyDisplayStatus.Failed => Colors.Red, JourneyDisplayStatus.Abandoned => Colors.Gray, _ => Colors.Gray }; /// /// Icon/text for status badge. /// public string StatusIcon => Status switch { JourneyDisplayStatus.InProgress => "...", JourneyDisplayStatus.Completed => "OK", JourneyDisplayStatus.Failed => "X", JourneyDisplayStatus.Abandoned => "-", _ => "?" }; /// /// Total number of steps (including nested). /// public int StepCount => CountAllSteps(Steps); /// /// Number of failed steps. /// public int FailedStepCount => CountFailedSteps(Steps); /// /// Whether there are any failed steps. /// public bool HasFailedSteps => FailedStepCount > 0; /// /// Whether there are any exceptions. /// public bool HasExceptions => Exceptions.Count > 0; /// /// Whether a user is associated with this journey. /// public bool HasUser => !string.IsNullOrEmpty(UserId); /// /// Start time formatted for display. /// public string StartTimeDisplay => StartTime.ToLocalTime().ToString("HH:mm:ss"); #endregion #region Helper Methods private static int CountAllSteps(IEnumerable steps) { int count = 0; foreach (var step in steps) { count++; count += CountAllSteps(step.ChildSteps); } return count; } private static int CountFailedSteps(IEnumerable steps) { int count = 0; foreach (var step in steps) { if (step.Status == StepDisplayStatus.Failed) count++; count += CountFailedSteps(step.ChildSteps); } return count; } /// /// Get a flattened list of all steps for timeline display. /// public List GetFlattenedSteps() { var result = new List(); FlattenSteps(Steps, result, 0); return result; } private static void FlattenSteps(IEnumerable steps, List result, int level) { foreach (var step in steps) { step.NestingLevel = level; result.Add(step); FlattenSteps(step.ChildSteps, result, level + 1); } } #endregion #region INotifyPropertyChanged public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); #endregion } #endregion #region StepItem /// /// Represents a step within a user journey. /// public class StepItem : INotifyPropertyChanged { private bool _isExpanded; private bool _isSelected; private int _nestingLevel; /// /// Unique step identifier. /// public string StepId { get; set; } = ""; /// /// Parent step ID for nested steps. /// public string? ParentStepId { get; set; } /// /// Step name (e.g., "Validate Cart", "Process Payment"). /// public string Name { get; set; } = ""; /// /// Step category (e.g., "business", "technical", "navigation"). /// public string? Category { get; set; } /// /// Current step status. /// public StepDisplayStatus Status { get; set; } = StepDisplayStatus.InProgress; /// /// Reason for failure if status is Failed. /// public string? FailureReason { get; set; } /// /// When the step started. /// public DateTime StartTime { get; set; } /// /// When the step ended. /// public DateTime? EndTime { get; set; } /// /// Duration in milliseconds. /// public double? DurationMs { get; set; } /// /// Custom data attached to the step. /// public Dictionary Data { get; set; } = new(); /// /// Child steps nested under this step. /// public ObservableCollection ChildSteps { get; } = new(); /// /// Breadcrumbs captured during this step. /// public List Breadcrumbs { get; set; } = new(); /// /// UI state: nesting level for indentation. /// public int NestingLevel { get => _nestingLevel; set { _nestingLevel = value; OnPropertyChanged(); OnPropertyChanged(nameof(IndentMargin)); } } /// /// UI state: whether this step is expanded in tree view. /// public bool IsExpanded { get => _isExpanded; set { _isExpanded = value; OnPropertyChanged(); } } /// /// UI state: whether this step is currently selected. /// public bool IsSelected { get => _isSelected; set { _isSelected = value; OnPropertyChanged(); } } #region Computed Properties /// /// Formatted duration for display. /// public string DurationDisplay { get { if (!DurationMs.HasValue) return "..."; if (DurationMs.Value < 1000) return $"{DurationMs:F0}ms"; return $"{DurationMs.Value / 1000:F1}s"; } } /// /// Category for display (defaults to "general"). /// public string CategoryDisplay => Category ?? "general"; /// /// Color for status indicator. /// public Color StatusColor => Status switch { StepDisplayStatus.InProgress => Colors.Blue, StepDisplayStatus.Completed => Colors.Green, StepDisplayStatus.Failed => Colors.Red, StepDisplayStatus.Skipped => Colors.Gray, _ => Colors.Gray }; /// /// Icon/text for status indicator. /// public string StatusIcon => Status switch { StepDisplayStatus.InProgress => "...", StepDisplayStatus.Completed => "OK", StepDisplayStatus.Failed => "X", StepDisplayStatus.Skipped => "-", _ => "?" }; /// /// Margin for indentation based on nesting level. /// public Thickness IndentMargin => new(NestingLevel * 20, 0, 0, 0); /// /// Whether this step has child steps. /// public bool HasChildren => ChildSteps.Count > 0; /// /// Whether this step has a failure reason. /// public bool HasFailureReason => !string.IsNullOrEmpty(FailureReason); /// /// Start time formatted for display. /// public string StartTimeDisplay => StartTime.ToLocalTime().ToString("HH:mm:ss.fff"); #endregion #region INotifyPropertyChanged public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); #endregion } #endregion #region BreadcrumbItem /// /// Represents a breadcrumb captured during journey/step execution. /// public class BreadcrumbItem { /// /// When the breadcrumb was captured. /// public DateTime Timestamp { get; set; } /// /// Breadcrumb category. /// public string Category { get; set; } = ""; /// /// Breadcrumb message. /// public string Message { get; set; } = ""; /// /// Log level: "info", "warning", "error", "debug". /// public string Level { get; set; } = "Info"; /// /// Optional additional data. /// public Dictionary? Data { get; set; } #region Computed Properties /// /// Timestamp formatted for display. /// public string TimestampDisplay => Timestamp.ToLocalTime().ToString("HH:mm:ss.fff"); /// /// Color based on log level. /// public Color LevelColor => Level.ToLower() switch { "error" => Colors.Red, "warning" => Colors.Orange, "info" => Colors.Blue, "debug" => Colors.Gray, _ => Colors.Gray }; /// /// Level display text. /// public string LevelDisplay => Level.ToUpper(); #endregion } #endregion #region ExceptionItem /// /// Represents an exception captured during journey execution. /// public class ExceptionItem { /// /// When the exception was captured. /// public DateTime Timestamp { get; set; } /// /// Exception type name. /// public string ExceptionType { get; set; } = ""; /// /// Exception message. /// public string Message { get; set; } = ""; /// /// Stack trace. /// public string? StackTrace { get; set; } /// /// Step ID where the exception was captured. /// public string? StepId { get; set; } #region Computed Properties /// /// Timestamp formatted for display. /// public string TimestampDisplay => Timestamp.ToLocalTime().ToString("HH:mm:ss"); /// /// Whether there is a stack trace. /// public bool HasStackTrace => !string.IsNullOrEmpty(StackTrace); /// /// Short display combining type and message. /// public string ShortDisplay => $"{ExceptionType}: {Message}"; #endregion } #endregion #region JourneyReconstructor /// /// Utility class to reconstruct journey hierarchy from flat EnvelopeItem list. /// public static class JourneyReconstructor { /// /// Reconstruct journey hierarchy from flat EnvelopeItem list. /// public static List ReconstructJourneys(IEnumerable items) { var journeyMap = new Dictionary(); var stepMap = new Dictionary(); var journeyBreadcrumbs = new Dictionary>(); var journeyExceptions = new Dictionary>(); var stepToJourney = new Dictionary(); // stepId -> journeyId // Process items in timestamp order foreach (var item in items.OrderBy(i => i.Timestamp)) { switch (item.Type) { case "journey_start": if (!string.IsNullOrEmpty(item.JourneyId)) { journeyMap[item.JourneyId] = new JourneyItem { JourneyId = item.JourneyId, Name = item.Name ?? "Unknown Journey", UserId = item.UserId, UserEmail = item.UserEmail, StartTime = item.Timestamp, Status = JourneyDisplayStatus.InProgress }; journeyBreadcrumbs[item.JourneyId] = new List(); journeyExceptions[item.JourneyId] = new List(); } break; case "journey_end": if (!string.IsNullOrEmpty(item.JourneyId) && journeyMap.TryGetValue(item.JourneyId, out var journey)) { journey.EndTime = item.Timestamp; journey.Status = ParseJourneyStatus(item.Status); if (item.Metadata.TryGetValue("durationMs", out var durationObj)) { journey.DurationMs = Convert.ToDouble(durationObj); } else { journey.DurationMs = (item.Timestamp - journey.StartTime).TotalMilliseconds; } foreach (var kvp in item.Metadata) { journey.Metadata[kvp.Key] = kvp.Value; } } break; case "step_start": if (!string.IsNullOrEmpty(item.StepId)) { var step = new StepItem { StepId = item.StepId, ParentStepId = item.ParentStepId, Name = item.Name ?? "Unknown Step", Category = item.Category, StartTime = item.Timestamp, Status = StepDisplayStatus.InProgress }; stepMap[item.StepId] = step; if (!string.IsNullOrEmpty(item.JourneyId)) { stepToJourney[item.StepId] = item.JourneyId; } } break; case "step_end": if (!string.IsNullOrEmpty(item.StepId) && stepMap.TryGetValue(item.StepId, out var endStep)) { endStep.EndTime = item.Timestamp; endStep.Status = ParseStepStatus(item.Status); if (item.Data.TryGetValue("durationMs", out var durObj)) { endStep.DurationMs = Convert.ToDouble(durObj); } else { endStep.DurationMs = (item.Timestamp - endStep.StartTime).TotalMilliseconds; } if (item.Data.TryGetValue("failureReason", out var reasonObj)) { endStep.FailureReason = reasonObj?.ToString(); } foreach (var kvp in item.Data) { endStep.Data[kvp.Key] = kvp.Value; } } break; case "exception": if (!string.IsNullOrEmpty(item.JourneyId) && journeyExceptions.TryGetValue(item.JourneyId, out var excList)) { excList.Add(new ExceptionItem { Timestamp = item.Timestamp, ExceptionType = item.ExceptionType ?? "Exception", Message = item.Message ?? "", StackTrace = item.StackTrace, StepId = item.StepId }); } break; } // Collect breadcrumbs if (!string.IsNullOrEmpty(item.JourneyId) && item.Breadcrumbs?.Count > 0 && journeyBreadcrumbs.TryGetValue(item.JourneyId, out var bcList)) { foreach (var bc in item.Breadcrumbs) { bcList.Add(new BreadcrumbItem { Timestamp = bc.Timestamp ?? item.Timestamp, Category = bc.Category, Message = bc.Message, Level = bc.Level, Data = bc.Data }); } } } // Build step hierarchy foreach (var step in stepMap.Values) { if (!string.IsNullOrEmpty(step.ParentStepId) && stepMap.TryGetValue(step.ParentStepId, out var parentStep)) { parentStep.ChildSteps.Add(step); step.NestingLevel = parentStep.NestingLevel + 1; } } // Associate root-level steps with journeys foreach (var step in stepMap.Values) { if (string.IsNullOrEmpty(step.ParentStepId) && stepToJourney.TryGetValue(step.StepId, out var journeyId) && journeyMap.TryGetValue(journeyId, out var ownerJourney)) { ownerJourney.Steps.Add(step); } } // Associate breadcrumbs and exceptions foreach (var journey in journeyMap.Values) { if (journeyBreadcrumbs.TryGetValue(journey.JourneyId, out var bcs)) { journey.Breadcrumbs = bcs.OrderBy(b => b.Timestamp).ToList(); } if (journeyExceptions.TryGetValue(journey.JourneyId, out var excs)) { journey.Exceptions = excs.OrderBy(e => e.Timestamp).ToList(); } } return journeyMap.Values.ToList(); } private static JourneyDisplayStatus ParseJourneyStatus(string? status) => status?.ToLower() switch { "completed" => JourneyDisplayStatus.Completed, "failed" => JourneyDisplayStatus.Failed, "abandoned" => JourneyDisplayStatus.Abandoned, _ => JourneyDisplayStatus.InProgress }; private static StepDisplayStatus ParseStepStatus(string? status) => status?.ToLower() switch { "completed" => StepDisplayStatus.Completed, "failed" => StepDisplayStatus.Failed, "skipped" => StepDisplayStatus.Skipped, _ => StepDisplayStatus.InProgress }; } #endregion