using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; using C4IT.FASD.Base; using C4IT.FASD.Cockpit.Communication; using FasdDesktopUi; using FasdDesktopUi.Basics.Models; using FasdDesktopUi.Basics.Services.Models; #if isDemo using System.Net; using FasdCockpitCommunicationDemo; using System.Text.RegularExpressions; #endif namespace FasdDesktopUi.Basics.Services { public sealed class TicketOverviewUpdateService { private static readonly string[] OverviewKeys = new[] { "TicketsNew", "TicketsActive", "TicketsCritical", "TicketsNewInfo", "IncidentNew", "IncidentActive", "IncidentCritical", "IncidentNewInfo", "UnassignedTickets", "UnassignedTicketsCritical" }; private readonly Dispatcher _dispatcher; private readonly Dictionary _currentCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<(string Key, bool UseRoleScope), List> _demoRelations = new Dictionary<(string, bool), List>(); private readonly HashSet _pendingScopes = new HashSet(); private readonly HashSet _initializedScopes = new HashSet(); private readonly object _fetchLock = new object(); private readonly HashSet _retryScopes = new HashSet(); private DispatcherTimer _personalTimer; private DispatcherTimer _roleTimer; private Task _fetchWorker; private bool _retryScheduled; private bool _isDemo; private bool _initialized; private bool _isEnabled; private readonly Random _random = new Random(); #if isDemo private readonly List _persistedDemoTickets = new List(); private readonly List _demoTemplates = new List(); private readonly HashSet _usedSummaries = new HashSet(StringComparer.OrdinalIgnoreCase); private const int SimulationHotkeyDelayMs = 400; private int _pendingSimulations; private DispatcherTimer _simulationFlushTimer; #endif private TicketOverviewUpdateService() { _dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher; foreach (var key in OverviewKeys) { _currentCounts[key] = TileCounts.Empty; } #if isDemo _simulationFlushTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(SimulationHotkeyDelayMs), DispatcherPriority.Background, SimulationFlushTimer_Tick, _dispatcher) { IsEnabled = false }; #endif } static TicketOverviewUpdateService() { Instance = new TicketOverviewUpdateService(); } public static TicketOverviewUpdateService Instance { get; } public event EventHandler OverviewCountsChanged; public IReadOnlyDictionary CurrentCounts => _currentCounts; public bool IsScopeInitialized(TileScope scope) { lock (_fetchLock) { return _initializedScopes.Contains(scope); } } public bool AreAllScopesInitialized { get { lock (_fetchLock) { return _initializedScopes.Contains(TileScope.Personal) && _initializedScopes.Contains(TileScope.Role); } } } public void Start() { UpdateAvailability(true); } public void Stop() { UpdateAvailability(false); } public void UpdateAvailability(bool isEnabled) { if (isEnabled) { if (!_isEnabled) { _isEnabled = true; StartInternal(); } else { RefreshTimerIntervals(); } } else { if (_isEnabled) StopInternal(); _isEnabled = false; } } private void StartInternal() { if (_initialized) return; _initialized = true; #if isDemo _isDemo = true; LoadPersistedDemoTickets(); #else _isDemo = cFasdCockpitCommunicationBase.Instance?.IsDemo() == true; #endif InitializeTimers(); _ = FetchAsync(); } private void StopInternal() { if (!_initialized) return; _initialized = false; lock (_fetchLock) { _pendingScopes.Clear(); _initializedScopes.Clear(); } lock (_retryScopes) { _retryScheduled = false; _retryScopes.Clear(); } _dispatcher.InvokeAsync(() => { _personalTimer?.Stop(); _roleTimer?.Stop(); _personalTimer = null; _roleTimer = null; foreach (var key in OverviewKeys) { _currentCounts[key] = TileCounts.Empty; } }); } private void InitializeTimers() { _personalTimer = CreateScopeTimer(TileScope.Personal); _roleTimer = CreateScopeTimer(TileScope.Role); _personalTimer?.Start(); _roleTimer?.Start(); } private DispatcherTimer CreateScopeTimer(TileScope scope) { var interval = GetPollingInterval(scope); var timer = new DispatcherTimer(interval, DispatcherPriority.Background, async (s, e) => await FetchAsync(scope).ConfigureAwait(false), _dispatcher) { IsEnabled = false }; return timer; } private TimeSpan GetPollingInterval(TileScope scope) { var ticketConfig = cFasdCockpitConfig.Instance?.Global?.TicketConfiguration; int minutes = scope == TileScope.Role ? cF4sdTicketConfig.DefaultOverviewPollingRole : cF4sdTicketConfig.DefaultOverviewPollingPersonal; if (ticketConfig != null) { minutes = scope == TileScope.Role ? ticketConfig.OverviewPollingRole : ticketConfig.OverviewPollingPersonal; } if (minutes < 1) minutes = 1; return TimeSpan.FromMinutes(minutes); } public Task FetchAsync() { if (!_isEnabled) return Task.CompletedTask; return QueueFetchAsync(new[] { TileScope.Personal, TileScope.Role }); } public Task FetchAsync(TileScope scope) { if (!_isEnabled) return Task.CompletedTask; return QueueFetchAsync(new[] { scope }); } private Task QueueFetchAsync(IEnumerable scopes) { if (!_isEnabled) return Task.CompletedTask; if (scopes == null) return Task.CompletedTask; lock (_fetchLock) { foreach (var scope in scopes) { _pendingScopes.Add(scope); } if (_fetchWorker == null || _fetchWorker.IsCompleted) { _fetchWorker = Task.Run(ProcessFetchQueueAsync); } return _fetchWorker; } } private async Task ProcessFetchQueueAsync() { while (true) { TileScope scope; lock (_fetchLock) { if (_pendingScopes.Count == 0) { _fetchWorker = null; return; } scope = _pendingScopes.First(); _pendingScopes.Remove(scope); } await FetchScopeAsync(scope).ConfigureAwait(false); } } private async Task FetchScopeAsync(TileScope scope) { if (!_isEnabled) return; var communication = cFasdCockpitCommunicationBase.Instance; if (communication == null) { ScheduleFetchRetry(scope); return; } try { _isDemo = communication.IsDemo(); var rawCounts = await communication.GetTicketOverviewCounts(OverviewKeys, scope == TileScope.Role).ConfigureAwait(false); var counts = new Dictionary(StringComparer.OrdinalIgnoreCase); if (rawCounts != null) { foreach (var kvp in rawCounts) { if (string.IsNullOrWhiteSpace(kvp.Key)) continue; counts[kvp.Key] = Math.Max(0, kvp.Value); } } if (!_isEnabled) return; await _dispatcher.InvokeAsync(() => ProcessScopeCounts(scope, counts)); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[TicketOverview] Fetch {scope} failed: {ex}"); ScheduleFetchRetry(scope); } } private void RefreshTimerIntervals() { _dispatcher.InvokeAsync(() => { if (_personalTimer != null) _personalTimer.Interval = GetPollingInterval(TileScope.Personal); if (_roleTimer != null) _roleTimer.Interval = GetPollingInterval(TileScope.Role); }); } private void ProcessScopeCounts(TileScope scope, IDictionary newCounts) { if (newCounts == null) return; var hasInitializedScope = _initializedScopes.Contains(scope); var changes = new List(); foreach (var key in OverviewKeys) { var previous = _currentCounts.TryGetValue(key, out var counts) ? counts : TileCounts.Empty; var incoming = newCounts.TryGetValue(key, out var value) ? value : 0; TileCounts updated; int oldValue; if (scope == TileScope.Role) { updated = new TileCounts(previous.Personal, incoming); oldValue = previous.Role; } else { updated = new TileCounts(incoming, previous.Role); oldValue = previous.Personal; } _currentCounts[key] = updated; if (hasInitializedScope && oldValue != incoming) { changes.Add(new TileCountChange(key, scope, oldValue, incoming)); } } if (!hasInitializedScope) { _initializedScopes.Add(scope); var initArgs = new TicketOverviewCountsChangedEventArgs( Array.Empty(), new Dictionary(_currentCounts, StringComparer.OrdinalIgnoreCase), scope); OverviewCountsChanged?.Invoke(this, initArgs); return; } if (changes.Count == 0) return; var args = new TicketOverviewCountsChangedEventArgs(changes, new Dictionary(_currentCounts, StringComparer.OrdinalIgnoreCase)); OverviewCountsChanged?.Invoke(this, args); } public void SimulateDemoTicket() { _isDemo = cFasdCockpitCommunicationBase.Instance?.IsDemo() == true; if (!_isDemo) return; #if isDemo if (_demoTemplates.Count == 0) { LoadDemoTemplates(); if (_demoTemplates.Count == 0) return; } _pendingSimulations++; if (_simulationFlushTimer != null) { _simulationFlushTimer.Stop(); _simulationFlushTimer.Interval = TimeSpan.FromMilliseconds(SimulationHotkeyDelayMs); _simulationFlushTimer.Start(); } else { ProcessDemoSimulations(_pendingSimulations); _pendingSimulations = 0; } #endif } public IEnumerable GetDemoRelations(string key, bool useRoleScope) { if (!_isDemo) return Enumerable.Empty(); lock (_demoRelations) { if (_demoRelations.TryGetValue((key, useRoleScope), out var list)) return list.ToList(); } return Enumerable.Empty(); } #if isDemo private void LoadPersistedDemoTickets() { var data = TicketOverviewDataStore.LoadData(); _demoTemplates.Clear(); if (data.Templates != null) _demoTemplates.AddRange(data.Templates); _persistedDemoTickets.Clear(); _usedSummaries.Clear(); if (data.Tickets == null) return; foreach (var record in data.Tickets) { if (!string.IsNullOrWhiteSpace(record.Summary)) _usedSummaries.Add(record.Summary); _persistedDemoTickets.Add(record); AddRelationForRecord(record); } } private void LoadDemoTemplates() { var templates = TicketOverviewDataStore.LoadTemplates(); if (templates == null || templates.Count == 0) return; _demoTemplates.Clear(); _demoTemplates.AddRange(templates); } private void SimulationFlushTimer_Tick(object sender, EventArgs e) { _simulationFlushTimer.Stop(); var count = _pendingSimulations; _pendingSimulations = 0; ProcessDemoSimulations(count); } private void ProcessDemoSimulations(int count) { if (count <= 0) return; if (_demoTemplates.Count == 0) { LoadDemoTemplates(); if (_demoTemplates.Count == 0) return; } var appliedChanges = new List(); for (int i = 0; i < count; i++) { var template = _demoTemplates[_random.Next(_demoTemplates.Count)]; var record = CreateDemoTicketRecord(template); if (record == null) continue; if (!TicketOverviewDataStore.AppendTicket(record)) continue; var change = RegisterDemoTicket(record); if (change.HasValue) appliedChanges.Add(change.Value); } if (appliedChanges.Count == 0) return; var args = new TicketOverviewCountsChangedEventArgs(appliedChanges, new Dictionary(_currentCounts)); OverviewCountsChanged?.Invoke(this, args); } private void AddRelationForRecord(DemoTicketRecord record) { if (record == null) return; var relation = CreateRelationFromRecord(record); var scopeKey = (record.TileKey, record.UseRoleScope); lock (_demoRelations) { if (!_demoRelations.TryGetValue(scopeKey, out var list)) { list = new List(); _demoRelations[scopeKey] = list; } if (list.Any(existing => existing.id == relation.id)) return; list.Add(relation); } } private cF4sdApiSearchResultRelation CreateRelationFromRecord(DemoTicketRecord record) { var activityType = ResolveDemoActivityType(record?.ActivityType); var relation = new cF4sdApiSearchResultRelation { Type = enumF4sdSearchResultClass.Ticket, DisplayName = record.DisplayName, Name = record.DisplayName, id = record.TicketId, Status = enumF4sdSearchResultStatus.Active, Infos = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Summary"] = record.Summary ?? string.Empty, ["StatusId"] = record.StatusId ?? string.Empty, ["UserDisplayName"] = record.UserDisplayName ?? string.Empty, ["UserAccount"] = record.UserAccount ?? string.Empty, ["UserDomain"] = record.UserDomain ?? string.Empty, ["ActivityType"] = activityType }, Identities = new cF4sdIdentityList { new cF4sdIdentityEntry { Class = enumFasdInformationClass.Ticket, Id = record.TicketId }, new cF4sdIdentityEntry { Class = enumFasdInformationClass.User, Id = record.UserId } } }; return relation; } private static string ResolveDemoActivityType(string configuredActivityType) { return string.IsNullOrWhiteSpace(configuredActivityType) ? null : configuredActivityType.Trim(); } private DemoTicketDetail CloneDetail(DemoTicketDetail source) { if (source == null) return new DemoTicketDetail(); return new DemoTicketDetail { AffectedUser = source.AffectedUser, Asset = source.Asset, Category = source.Category, Description = source.Description, DescriptionHtml = source.DescriptionHtml, Priority = source.Priority, Solution = source.Solution, SolutionHtml = source.SolutionHtml, Journal = source.Journal?.Select(entry => new DemoTicketJournalEntry { Header = entry?.Header, Description = entry?.Description, DescriptionHtml = entry?.DescriptionHtml, IsVisibleForUser = entry?.IsVisibleForUser ?? true, CreationDate = entry?.CreationDate ?? default }).ToList() ?? new List() }; } private TileCountChange? RegisterDemoTicket(DemoTicketRecord record) { if (record == null) return null; _persistedDemoTickets.Add(record); AddRelationForRecord(record); if (!string.IsNullOrWhiteSpace(record.Summary)) _usedSummaries.Add(record.Summary); if (cFasdCockpitCommunicationBase.Instance is cFasdCockpitCommunicationDemo demoCommunication) { demoCommunication.RegisterGeneratedTicket(record); } if (!_currentCounts.TryGetValue(record.TileKey, out var previousCounts)) previousCounts = TileCounts.Empty; TileCounts updatedCounts; int oldValue; int newValue; TileScope scope; if (record.UseRoleScope) { updatedCounts = new TileCounts(previousCounts.Personal, previousCounts.Role + 1); oldValue = previousCounts.Role; newValue = updatedCounts.Role; scope = TileScope.Role; } else { updatedCounts = new TileCounts(previousCounts.Personal + 1, previousCounts.Role); oldValue = previousCounts.Personal; newValue = updatedCounts.Personal; scope = TileScope.Personal; } _currentCounts[record.TileKey] = updatedCounts; return new TileCountChange(record.TileKey, scope, oldValue, newValue); } private string EnsureUniqueSummary(string preferredSummary) { if (string.IsNullOrWhiteSpace(preferredSummary)) preferredSummary = "Demo Ticket"; if (!_usedSummaries.Contains(preferredSummary)) { _usedSummaries.Add(preferredSummary); return preferredSummary; } var nextFreeSummary = _demoTemplates .Select(t => t?.Summary) .Where(s => !string.IsNullOrWhiteSpace(s)) .FirstOrDefault(s => !_usedSummaries.Contains(s)); if (!string.IsNullOrWhiteSpace(nextFreeSummary)) { _usedSummaries.Add(nextFreeSummary); return nextFreeSummary; } var baseSummary = preferredSummary; var suffix = 2; var candidate = baseSummary; while (_usedSummaries.Contains(candidate)) { candidate = $"{baseSummary} #{suffix}"; suffix++; } _usedSummaries.Add(candidate); return candidate; } private DemoTicketRecord CreateDemoTicketRecord(DemoTicketTemplate template) { if (template == null) return null; var relationId = Guid.NewGuid(); var createdAt = DateTime.UtcNow; var prefix = string.IsNullOrWhiteSpace(template.DisplayNamePrefix) ? "TCK" : template.DisplayNamePrefix.Trim(); prefix = prefix.ToUpperInvariant(); var displayName = TicketOverviewDataStore.GetNextDisplayName(prefix); var summary = EnsureUniqueSummary(template.Summary ?? string.Empty); var detail = CloneDetail(template.Detail); if (string.IsNullOrWhiteSpace(detail.AffectedUser)) detail.AffectedUser = template.UserDisplayName ?? "Ticket, Timo"; if (string.IsNullOrWhiteSpace(detail.Description) && !string.IsNullOrWhiteSpace(detail.DescriptionHtml)) { detail.Description = Regex.Replace(detail.DescriptionHtml, "<.*?>", string.Empty); } if (string.IsNullOrWhiteSpace(detail.Description)) detail.Description = summary; if (string.IsNullOrWhiteSpace(detail.DescriptionHtml)) { detail.DescriptionHtml = $"

{WebUtility.HtmlEncode(detail.Description)}

"; } if (string.IsNullOrWhiteSpace(detail.Solution) && !string.IsNullOrWhiteSpace(detail.SolutionHtml)) { detail.Solution = Regex.Replace(detail.SolutionHtml, "<.*?>", string.Empty); } if (string.IsNullOrWhiteSpace(detail.Solution)) detail.Solution = string.Empty; if (string.IsNullOrWhiteSpace(detail.SolutionHtml) && !string.IsNullOrWhiteSpace(detail.Solution)) { detail.SolutionHtml = $"

{WebUtility.HtmlEncode(detail.Solution)}

"; } if (detail.Journal == null || detail.Journal.Count == 0) { detail.Journal = new List { new DemoTicketJournalEntry { Header = "Ticket erstellt", Description = detail.Description ?? "Automatisch generiertes Demoticket.", DescriptionHtml = detail.DescriptionHtml ?? "

Automatisch generiertes Demoticket.

", IsVisibleForUser = true, CreationDate = createdAt } }; } foreach (var entry in detail.Journal) { if (entry.CreationDate == default) entry.CreationDate = createdAt; } return new DemoTicketRecord { TicketId = relationId, TileKey = string.IsNullOrWhiteSpace(template.TileKey) ? "TicketsNew" : template.TileKey, UseRoleScope = template.UseRoleScope, ActivityType = ResolveDemoActivityType(template.ActivityType), DisplayName = displayName, Summary = summary, StatusId = string.IsNullOrWhiteSpace(template.StatusId) ? "New" : template.StatusId, UserDisplayName = template.UserDisplayName ?? detail.AffectedUser ?? "Ticket, Timo", UserAccount = template.UserAccount ?? "TT007", UserDomain = template.UserDomain ?? "CONTOSO", UserId = template.UserId ?? Guid.Parse("42c760d6-90e8-469f-b2fe-ac7d4cc6cb0a"), CreatedAt = createdAt, Detail = detail }; } #endif public Dictionary GetCountsForScope(bool useRoleScope) { return _currentCounts.ToDictionary(kvp => kvp.Key, kvp => useRoleScope ? kvp.Value.Role : kvp.Value.Personal, StringComparer.OrdinalIgnoreCase); } private int GetDemoRelationCount(string key, bool useRoleScope) { lock (_demoRelations) { if (_demoRelations.TryGetValue((key, useRoleScope), out var list)) return list.Count; } return 0; } private void ScheduleFetchRetry(TileScope scope) { if (!_isEnabled) return; lock (_retryScopes) { _retryScopes.Add(scope); if (_retryScheduled) return; _retryScheduled = true; } _ = _dispatcher.InvokeAsync(async () => { try { await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false); TileScope[] scopes; lock (_retryScopes) { scopes = _retryScopes.ToArray(); _retryScopes.Clear(); _retryScheduled = false; } foreach (var pendingScope in scopes) { await FetchAsync(pendingScope).ConfigureAwait(false); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[TicketOverview] Retry scheduling failed: {ex}"); } }, DispatcherPriority.Background); } } }