using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; using C4IT.FASD.Base; using C4IT.FASD.Cockpit.Communication; using C4IT.Logging; using FasdDesktopUi.Basics.Models; using FasdDesktopUi.Basics.Services.Models; #if isDemo using System.Net; using FasdCockpitCommunicationDemo; using System.Text.RegularExpressions; #endif using static C4IT.Logging.cLogManager; namespace FasdDesktopUi.Basics.Services { public sealed class TicketOverviewUpdateService { #region Fields private readonly ITicketOverviewCommunicationSource _communicationSource; private static readonly string[] OverviewKeys = new[] { "TicketsNew", "TicketsActive", "TicketsCritical", "TicketsNewInfo", "IncidentNew", "IncidentActive", "IncidentCritical", "IncidentNewInfo", "UnassignedTickets", "UnassignedTicketsCritical" }; private readonly ITicketOverviewDispatcher _dispatcher; private readonly ITicketOverviewSettingsProvider _settingsProvider; 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 ITicketOverviewTimer _personalTimer; private ITicketOverviewTimer _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 ITicketOverviewTimer _simulationFlushTimer; #endif #endregion #region Construction and Singleton private TicketOverviewUpdateService() : this( new TicketOverviewCommunicationSource(), new TicketOverviewDispatcher(Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher), new TicketOverviewSettingsProvider()) { } internal TicketOverviewUpdateService( ITicketOverviewCommunicationSource communicationSource, ITicketOverviewDispatcher dispatcher) : this(communicationSource, dispatcher, new TicketOverviewSettingsProvider()) { } internal TicketOverviewUpdateService( ITicketOverviewCommunicationSource communicationSource, ITicketOverviewDispatcher dispatcher, ITicketOverviewSettingsProvider settingsProvider) { _communicationSource = communicationSource ?? throw new ArgumentNullException(nameof(communicationSource)); _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); _settingsProvider = settingsProvider ?? throw new ArgumentNullException(nameof(settingsProvider)); foreach (var key in OverviewKeys) { _currentCounts[key] = TileCounts.Empty; } #if isDemo _simulationFlushTimer = _dispatcher.CreateTimer(TimeSpan.FromMilliseconds(SimulationHotkeyDelayMs), SimulationFlushTimer_Tick); #endif } private static readonly Lazy _instance = new Lazy(() => new TicketOverviewUpdateService()); public static TicketOverviewUpdateService Instance => _instance.Value; #endregion #region Public API 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); } #endregion #region Lifecycle 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 = _communicationSource.Resolve()?.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; } }); } #endregion #region Polling and Fetch Pipeline private void InitializeTimers() { _personalTimer = CreateScopeTimer(TileScope.Personal); _roleTimer = CreateScopeTimer(TileScope.Role); _personalTimer?.Start(); _roleTimer?.Start(); } private ITicketOverviewTimer CreateScopeTimer(TileScope scope) { var interval = GetPollingInterval(scope); return _dispatcher.CreateTimer(interval, () => _ = FetchAsync(scope)); } private TimeSpan GetPollingInterval(TileScope scope) { int minutes = _settingsProvider.GetPollingMinutes(scope); 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) { MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); } try { if (!_isEnabled) return; var communication = _communicationSource.Resolve(); if (communication == null) { ScheduleFetchRetry(scope); return; } _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) { LogException(ex); ScheduleFetchRetry(scope); } finally { LogMethodEnd(CM); } } #endregion #region Count Processing 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; bool hasInitializedScope; lock (_fetchLock) { hasInitializedScope = _initializedScopes.Contains(scope); } var result = TicketOverviewCountProcessor.Calculate(_currentCounts, OverviewKeys, scope, newCounts, hasInitializedScope); _currentCounts.Clear(); foreach (var kvp in result.UpdatedCounts) { _currentCounts[kvp.Key] = kvp.Value; } if (result.IsInitialization) { lock (_fetchLock) { _initializedScopes.Add(scope); } var initArgs = new TicketOverviewCountsChangedEventArgs( Array.Empty(), new Dictionary(_currentCounts, StringComparer.OrdinalIgnoreCase), scope); OverviewCountsChanged?.Invoke(this, initArgs); return; } if (result.Changes.Count == 0) return; var args = new TicketOverviewCountsChangedEventArgs(result.Changes, new Dictionary(_currentCounts, StringComparer.OrdinalIgnoreCase)); OverviewCountsChanged?.Invoke(this, args); } #endregion #region Demo - Public public void SimulateDemoTicket() { _isDemo = _communicationSource.Resolve()?.IsDemo() == true; if (!_isDemo) return; #if isDemo if (_demoTemplates.Count == 0) { LoadDemoTemplates(); if (_demoTemplates.Count == 0) return; } Interlocked.Increment(ref _pendingSimulations); if (_simulationFlushTimer != null) { _simulationFlushTimer.Stop(); _simulationFlushTimer.Interval = TimeSpan.FromMilliseconds(SimulationHotkeyDelayMs); _simulationFlushTimer.Start(); } else { var count = Interlocked.Exchange(ref _pendingSimulations, 0); ProcessDemoSimulations(count); } #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(); } #endregion #if isDemo #region Demo - Internals 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() { _simulationFlushTimer.Stop(); var count = Interlocked.Exchange(ref _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, StringComparer.OrdinalIgnoreCase)); 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); _communicationSource.Resolve()?.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 }; } #endregion #endif #region Utilities public Dictionary GetCountsForScope(bool useRoleScope) { return _currentCounts.ToDictionary(kvp => kvp.Key, kvp => useRoleScope ? kvp.Value.Role : kvp.Value.Personal, StringComparer.OrdinalIgnoreCase); } #endregion #region Retry Handling private void ScheduleFetchRetry(TileScope scope) { MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); } try { 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) { LogException(ex); } }); } catch (Exception ex) { LogException(ex); } finally { LogMethodEnd(CM); } } #endregion } }