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.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 TimeSpan RefreshInterval = TimeSpan.FromMinutes(5); private static readonly string[] OverviewKeys = new[] { "TicketsNew", "TicketsActive", "TicketsCritical", "TicketsNewInfo", "IncidentNew", "IncidentActive", "IncidentCritical", "IncidentNewInfo", "UnassignedTickets", "UnassignedTicketsCritical" }; private const string DemoTicketDetailsKey = "Demo.HasTicketDetails"; 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 DispatcherTimer _timer; private bool _isFetching; private bool _fetchRetryPending; private bool _isDemo; private bool _initialized; 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() { #if isDemo Instance = new TicketOverviewUpdateService(); #endif } public static TicketOverviewUpdateService Instance { get; } = null; public event EventHandler OverviewCountsChanged; public IReadOnlyDictionary CurrentCounts => _currentCounts; public void Start() { if (_initialized) return; _initialized = true; #if isDemo _isDemo = true; LoadPersistedDemoTickets(); #else _isDemo = cFasdCockpitCommunicationBase.Instance?.IsDemo() == true; #endif if (!_isDemo) { _timer = new DispatcherTimer(RefreshInterval, DispatcherPriority.Background, async (s, e) => await FetchAsync().ConfigureAwait(false), _dispatcher); _timer.Start(); _ = FetchAsync(); } else { _ = FetchAsync(); } } public async Task FetchAsync() { if (_isFetching) return; var communication = cFasdCockpitCommunicationBase.Instance; if (communication == null) { ScheduleFetchRetry(); return; } _isFetching = true; try { _isDemo = communication?.IsDemo() == true; if (_isDemo && _timer != null) { _timer.Stop(); _timer = null; } var counts = await Task.Run(() => RetrieveCountsAsync()).ConfigureAwait(false); if (counts != null) { await _dispatcher.InvokeAsync(() => ProcessCounts(counts)); } } finally { _isFetching = false; } } private Dictionary RetrieveCountsAsync() { var communication = cFasdCockpitCommunicationBase.Instance; if (communication == null) return null; var result = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var key in OverviewKeys) { var personalTask = communication.GetTicketOverviewRelations(key, useRoleScope: false, count: 0); var roleTask = communication.GetTicketOverviewRelations(key, useRoleScope: true, count: 0); Task.WaitAll(personalTask, roleTask); int personalCount = personalTask.Result?.Count ?? 0; int roleCount = roleTask.Result?.Count ?? 0; if (_isDemo) { personalCount += GetDemoRelationCount(key, useRoleScope: false); roleCount += GetDemoRelationCount(key, useRoleScope: true); } result[key] = new TileCounts(personalCount, roleCount); } return result; } private void ProcessCounts(Dictionary newCounts) { var changes = new List(); bool hasPrevious = _currentCounts.Values.Any(tc => tc.Personal > 0 || tc.Role > 0); foreach (var key in OverviewKeys) { var previous = _currentCounts[key]; var current = newCounts.TryGetValue(key, out var value) ? value : TileCounts.Empty; if (previous.Personal != current.Personal) { changes.Add(new TileCountChange(key, TileScope.Personal, previous.Personal, current.Personal)); } if (previous.Role != current.Role) { changes.Add(new TileCountChange(key, TileScope.Role, previous.Role, current.Role)); } _currentCounts[key] = current; } if (!hasPrevious) return; if (changes.Count == 0) return; var args = new TicketOverviewCountsChangedEventArgs(changes, new Dictionary(_currentCounts)); 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 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, [DemoTicketDetailsKey] = bool.TrueString }, Identities = new cF4sdIdentityList { new cF4sdIdentityEntry { Class = enumFasdInformationClass.Ticket, Id = record.TicketId }, new cF4sdIdentityEntry { Class = enumFasdInformationClass.User, Id = record.UserId } } }; return relation; } private DemoTicketDetail CloneDetail(DemoTicketDetail source) { if (source == null) return new DemoTicketDetail(); return new DemoTicketDetail { AffectedUser = source.AffectedUser, Asset = source.Asset, Category = source.Category, Classification = source.Classification, 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, 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() { if (_fetchRetryPending) return; _fetchRetryPending = true; _ = _dispatcher.InvokeAsync(async () => { try { await Task.Delay(250).ConfigureAwait(false); await FetchAsync().ConfigureAwait(false); } finally { _fetchRetryPending = false; } }, DispatcherPriority.Background); } } }